2. GKI(通用内核映像)架构详解
2.1 GKI 架构演进背景
GKI(Generic Kernel Image)是 Google 为解决 Android 碎片化问题而推出的统一内核映像方案。其核心目标是将内核核心与设备驱动解耦,使 SoC 厂商和 OEM 厂商能够独立更新内核版本,而无需等待驱动适配。
GKI 1.0 随 Android 11 引入,GKI 2.0 随 Android 12 全面推行。两者在架构设计、模块化程度、ABI 稳定性方面存在显著差异,直接影响驱动开发者的移植策略。
2.2 GKI 1.0 架构特点与局限
GKI 1.0 主要解决了内核源码树统一的问题,但并未完全冻结内核接口。其架构特点如下:
- 统一内核源码: 所有设备使用同一份 Android 通用内核源码(ACK),但允许 SoC 厂商在内核中修改核心代码。
- 模块化驱动: 驱动以可加载内核模块(LKM)形式存在,但模块与内核之间的接口未做严格约束。
- ABI 未冻结: 内核内部函数、数据结构、头文件允许修改,导致模块在不同内核版本间不兼容。
- vendor_boot 分区: 引入 vendor_boot 分区存放设备树和 vendor 模块,但内核映像仍包含大量 vendor 定制代码。
2.3 GKI 2.0 架构核心变化
GKI 2.0 引入了 稳定 ABI(KMI,Kernel Module Interface) 概念,将内核接口冻结为二进制兼容的契约。这是与 GKI 1.0 最本质的区别。
| 对比维度 | GKI 1.0 | GKI 2.0 |
|---|---|---|
| ABI 稳定性 | 未冻结,接口可修改 | KMI 冻结,接口二进制兼容 |
| 内核核心 | 允许 vendor 修改核心代码 | 核心代码只读,vendor 只能通过钩子扩展 |
| 驱动模块 | 与内核版本强绑定 | 跨内核版本兼容(同 KMI 基线) |
| vendor 定制 | 直接修改内核源码 | 通过 vendor_hooks 和 vendor_sysfs 扩展 |
| 更新策略 | 全量内核映像替换 | 仅更新 GKI 内核,模块无需重编 |
| 分区结构 | boot + vendor_boot | boot(GKI)+ vendor_boot(模块+DT) |
2.4 GKI 2.0 架构分层
GKI 2.0 将系统分为三个层次,每层职责清晰,接口通过 KMI 契约连接:
- GKI 内核层: 包含核心调度、内存管理、文件系统、网络栈等通用功能。由 Google 维护,只读不可修改。
- KMI 接口层: 定义内核导出的符号集、数据结构、回调函数。通过
kmi_symbol_list文件管理,任何修改需经 Google 审核。 - vendor 驱动层: 以可加载模块形式实现设备驱动、电源管理、传感器等 vendor 特有功能。只能使用 KMI 导出的接口。
2.5 对驱动移植的具体影响
GKI 2.0 的架构变化对驱动移植工作产生了深远影响,主要体现在以下方面:
2.5.1 驱动接口使用限制
在 GKI 2.0 下,驱动只能使用 KMI 导出的符号。未导出的内核函数、内联函数、静态函数均不可直接调用。驱动开发者需要:
- 通过
/proc/kallsyms或kmi_symbol_list文件确认可用符号。 - 使用
EXPORT_SYMBOL_GPL导出的函数,避免使用EXPORT_SYMBOL未导出的内部接口。 - 对于必须使用的未导出函数,需向 Google 提交 KMI 扩展请求。
// GKI 2.0 合规的驱动接口使用示例
#include <linux/module.h>
#include <linux/platform_device.h>
// 只能使用 KMI 导出的符号
extern int __kmi_exported_function(void); // 在 kmi_symbol_list 中声明
static int my_driver_probe(struct platform_device *pdev)
{
int ret;
// 调用 KMI 导出的函数
ret = __kmi_exported_function();
if (ret < 0)
return ret;
// 错误做法:直接调用未导出的内核内部函数
// ret = internal_kernel_function(); // 编译通过但运行时符号未解析
return 0;
}
2.5.2 vendor_hooks 机制
GKI 2.0 引入 vendor_hooks 作为内核核心与 vendor 驱动之间的回调机制。驱动可以通过注册钩子函数,在内核关键路径上插入自定义逻辑,而无需修改内核代码。
| 钩子类型 | 触发时机 | 典型用途 |
|---|---|---|
trace_android_vh_xxx |
调度、内存、文件系统等关键路径 | 性能优化、功耗管理、安全策略 |
trace_android_rvh_xxx |
调度器相关路径 | CPU 频率调节、任务优先级调整 |
trace_android_vh_xxx (网络) |
网络协议栈路径 | 流量整形、防火墙规则 |
2.5.3 模块加载与版本校验
GKI 2.0 内核在加载模块时会进行严格的版本校验。模块的 vermagic 字符串必须与内核的 KMI 版本完全匹配,否则加载失败。
# 查看内核 KMI 版本
$ cat /proc/version
Linux version 5.10.107-android12-9-00001-g12345678abcde (build@host) ...
# 查看模块 vermagic
$ modinfo my_driver.ko
filename: my_driver.ko
vermagic: 5.10.107-android12-9-00001-g12345678abcde SMP preempt mod_unload modversions aarch64
# 加载模块
$ insmod my_driver.ko
# 如果 vermagic 不匹配,会报错:
# insmod: ERROR: could not insert module my_driver.ko: Invalid module format
2.6 GKI 1.0 到 GKI 2.0 的移植路径
对于已有 GKI 1.0 驱动的团队,迁移到 GKI 2.0 需要遵循以下步骤:
- 审计驱动接口: 列出驱动使用的所有内核符号,对照 KMI 符号表,标记未导出符号。
- 替换内部调用: 将未导出符号的调用替换为 vendor_hooks 或 KMI 导出接口。
- 重构数据结构: 如果驱动直接访问内核结构体成员(如
task_struct内部字段),需改为通过访问器函数操作。 - 更新 Makefile: 确保模块编译时链接正确的 KMI 头文件路径。
- 验证 KMI 兼容性: 使用
kmi_check工具验证模块是否与目标 KMI 基线兼容。
2.7 常见移植问题与解决方案
| 问题 | 原因 | 解决方案 |
|---|---|---|
模块加载报 Unknown symbol |
驱动使用了未导出的内核符号 | 替换为 KMI 导出符号或 vendor_hooks |
模块加载报 Invalid module format |
vermagic 与内核 KMI 版本不匹配 | 使用与目标内核相同的 KMI 基线编译 |
| 驱动功能异常但无报错 | 数据结构布局变化导致偏移错误 | 使用访问器函数替代直接结构体访问 |
| vendor_hooks 未触发 | 钩子注册时机错误或钩子名称不匹配 | 检查内核版本对应的 vendor_hooks 列表 |
| 性能下降 | vendor_hooks 引入额外上下文切换 | 优化钩子函数执行路径,减少锁竞争 |
2.8 小结
GKI 2.0 通过 KMI 冻结机制,实现了内核核心与 vendor 驱动的解耦,使驱动模块能够跨内核版本兼容。对于驱动移植工作,核心要点是:
- 理解 KMI 契约的约束范围,避免使用未导出接口。
- 熟练使用 vendor_hooks 机制替代内核代码修改。
- 建立 KMI 兼容性验证流程,确保模块在不同内核版本间可加载运行。
- 关注 KMI 基线的演进,及时适配新版本的变化。