2. GKI 基础概念

在 Android 内核演进过程中,GKI(Generic Kernel Image,通用内核镜像)是理解后续所有迁移与兼容问题的起点。它并不是一个简单的“换内核”动作,而是把内核拆分为“通用核心 + 可插拔模块”的工程体系。本节围绕四个关键词展开:GKI 定义、KMI 稳定接口、ABI 与符号版本、模块化内核设计思想。

一、GKI 定义:把内核变成可复用的“底座”

GKI 的核心思想是:Google 提供一份经过统一编译、统一验证的通用内核镜像,负责调度、内存管理、文件系统、网络协议栈、电源管理等通用能力;而 SoC 厂商、OEM 厂商的差异化驱动与硬件适配,则以内核模块(.ko)的形式动态加载。这样,内核镜像可以独立于具体硬件进行升级,厂商模块则跟随设备版本发布。

从系统结构看,GKI 把传统“一个设备一份内核源码”的模式,转变为“一份 GKI 内核 + 多份厂商模块”的模式。其直接收益是:安全补丁可以更快下发,Android 版本升级不再必须等待 SoC 厂商重新适配整个内核树,内核碎片化显著降低。

一句话理解:GKI 是 Android 官方维护的通用内核底座,厂商通过模块在其上扩展硬件能力,从而实现内核与硬件解耦。

二、KMI 稳定接口:GKI 与模块之间的“契约”

KMI(Kernel Module Interface,内核模块接口)是 GKI 与厂商模块之间必须保持稳定的接口集合。它规定了模块可以调用哪些内核函数、访问哪些数据结构、使用哪些符号。只要 KMI 不变,厂商模块就可以在同一个 GKI 大版本内继续使用,而不必随每次内核小版本更新重新编译。

KMI 的稳定性通过符号列表(symbol list)来约束。Android 官方会发布 abi_gki_aarch64 等符号清单,厂商模块只能使用清单中允许的符号。若模块引用了未导出或未列入清单的符号,加载时会直接失败。因此,KMI 既是兼容性保障,也是模块开发的边界。

# 查看当前内核导出的符号
cat /proc/kallsyms | grep "module_layout"

# 查看 GKI 符号清单(源码树中)
android/abi_gki_aarch64
android/abi_gki_aarch64.xml

# 模块加载失败时常见日志
[  12.345678] my_driver: Unknown symbol gki_private_func (err -2)
[  12.345679] my_driver: disagrees about version of symbol module_layout

三、ABI 与符号版本:兼容性的“指纹”

ABI(Application Binary Interface)描述的是二进制层面的调用约定,包括函数参数传递方式、结构体布局、符号名称与版本等。对于内核模块而言,ABI 兼容意味着:模块在编译时看到的符号签名、结构体大小、字段偏移,与运行时内核实际提供的一致。

Linux 内核通过 module_layout 和符号 CRC 校验来检查 ABI。GKI 进一步引入符号版本机制:每个导出符号带有版本信息,模块加载时内核会比对版本。若版本不匹配,会出现 disagrees about version of symbol 错误。这也是 GKI 迁移中最常见的兼容性故障之一。

概念 关注层面 典型检查点 不匹配后果
KMI 接口集合是否稳定 符号是否在 abi_gki 清单中 模块无法加载,Unknown symbol
ABI 二进制调用约定是否一致 结构体布局、函数签名 运行崩溃、数据错乱
符号版本 符号 CRC 是否匹配 module_layout、CRC 校验 disagrees about version
模块化设计 功能是否可插拔 驱动是否编译为 .ko 无法独立升级、耦合过重

四、模块化内核设计思想:从“单体”到“可插拔”

模块化设计是 GKI 能够成立的前提。传统 Android 内核把大量驱动直接编译进 vmlinux,导致任何硬件改动都要重新编译整个内核。GKI 要求把非通用功能尽量下沉为模块,通过 insmod、modprobe 在启动阶段按需加载。

模块化带来的关键变化包括:

  • 编译解耦:厂商模块可独立编译,不必重新构建 GKI 内核。
  • 升级解耦:GKI 内核可单独更新,模块保持二进制兼容即可继续使用。
  • 验证解耦:Google 验证 GKI 通用部分,厂商验证自身模块,责任边界清晰。
  • 启动依赖:模块加载顺序、依赖关系需要显式声明,否则会出现 probe 失败。

下面这张图展示了 GKI 的整体结构关系:上层是 Android 框架与 Vendor 模块,中间是 KMI 稳定接口层,下层是 GKI 通用内核镜像。厂商模块通过 KMI 与 GKI 交互,ABI 与符号版本则作为校验机制贯穿其中。

GKI 通用内核与厂商模块的结构关系 Android Framework / Vendor 应用与 HAL 通过标准系统调用与设备节点访问内核能力 厂商内核模块 .ko 显示 / 摄像头 / 音频 / 电源 SoC 驱动、外设适配 模块加载与依赖管理 insmod / modprobe / modules.dep 启动阶段按序加载 KMI 稳定接口层 导出符号清单 · 结构体约定 · 函数签名 · 版本约束 GKI 通用内核镜像(vmlinux / Image) 调度 · 内存管理 · 文件系统 · 网络协议栈 · 电源管理 ABI 与符号版本校验 模块通过 KMI 调用 GKI 能力;ABI 与符号版本保证二进制兼容

五、案例场景:模块加载失败如何定位

假设某厂商在迁移到 GKI 后,摄像头驱动模块加载失败,日志如下:

[   8.120345] cam_sensor: loading out-of-tree module taints kernel
[   8.120567] cam_sensor: Unknown symbol gki_camera_clock_enable (err -2)
[   8.120789] cam_sensor: disagrees about version of symbol module_layout

定位思路分三步:第一,确认 gki_camera_clock_enable 是否在 abi_gki_aarch64 符号清单中;第二,检查模块编译时使用的内核头文件与当前 GKI 内核是否一致;第三,核对 module_layout 的 CRC 是否匹配。若符号未导出,需要改用 KMI 允许的替代接口;若版本不匹配,则需要用当前 GKI 源码重新编译模块。

实用命令:使用 modinfo cam_sensor.ko 查看模块的 vermagic 与依赖;使用 dmesg | grep -i "unknown symbol\|disagrees" 快速筛选兼容性错误。

六、常见误区与复盘

  • 误区一:GKI 就是“统一内核源码”。实际上 GKI 是统一编译产物与接口约束,厂商仍可维护自己的模块代码。
  • 误区二:KMI 稳定等于所有符号都能用。KMI 只保证清单内符号稳定,清单外符号随时可能变化。
  • 误区三:ABI 兼容只看函数名。结构体大小、字段偏移、对齐方式同样影响 ABI,必须整体校验。
  • 误区四:模块化只是编译方式变化。模块化还涉及加载顺序、依赖声明、启动时序,是系统级设计。

迁移提醒:在 GKI 迁移初期,应优先梳理厂商模块对内核符号的引用清单,区分“KMI 允许符号”与“私有符号”。私有符号依赖越多,后续升级风险越大,应尽早替换为稳定接口或上移到用户态实现。

理解 GKI、KMI、ABI 与模块化设计,是后续进行版本迁移、兼容调试和符号冲突排查的基础。只有把“通用内核”和“厂商模块”的边界划清楚,才能在升级时快速判断问题出在接口、版本还是模块自身实现。