可加载的内核模块

Android 8.0 中引入的模块化内核要求规定,所有系统芯片 (SoC) 内核都必须支持可加载的内核模块。

内核配置选项

为了支持可加载的内核模块,所有通用内核中的 android-base.config 都包含以下内核配置选项(或其内核版本等效选项):

CONFIG_MODULES=y
CONFIG_MODULE_UNLOAD=y
CONFIG_MODVERSIONS=y

所有设备内核都必须启用这些选项。内核模块还应尽可能支持卸载和重新加载。

模块签名

GKI 供应商模块不支持模块签名。在需要支持启动时验证的设备上,Android 要求内核模块位于启用了 dm-verity 的分区中。这样便无需为各个模块签名来辨识其真实性。 Android 13 引入了 GKI 模块的概念。GKI 模块使用内核的构建时签名基础架构在运行时区分 GKI 模块和其他模块。允许加载未签名模块,前提是它们仅使用许可名单中显示的符号或由其他未签名模块提供的符号。为了方便在 GKI 构建过程中使用内核的构建时密钥对 GKI 模块进行签名,GKI 内核配置启用了 CONFIG_MODULE_SIG_ALL=y。为避免在设备内核构建过程中为非 GKI 模块进行签名,您必须将 # CONFIG_MODULE_SIG_ALL is not set 添加为内核配置 fragment 的一部分。

文件位置

Android 7.x 及更低版本对内核模块(包括对 insmodrmmod 的支持)没有强制要求,而 Android 8.x 及更高版本建议在生态系统中使用内核模块。下表显示了 Android 的 3 种启动模式所需的潜在板专用外设支持:

启动模式 存储 显示 拨号键盘 电池 PMIC 触摸屏 NFC、WLAN、
蓝牙
传感器 相机
恢复
充电
Android

除了按 Android 启动模式下的可用性进行分类之外,内核模块还可以按所有者(SoC 供应商或 ODM)进行分类。如果使用了内核模块,它们在文件系统中的放置位置的要求如下:

  • 所有内核都应内置对启动和装载分区的支持。
  • 必须从只读分区加载内核模块。
  • 对于需要支持启动时验证的设备,应从已验证的分区加载内核模块。
  • 内核模块不应位于 /system 中。
  • 应从 /system/lib/modules(指向 /system_dlkm/lib/modules 的符号链接)加载设备所需的 GKI 模块。
  • 完整 Android 模式或充电模式所需的 SoC 供应商内核模块应位于 /vendor/lib/modules 中。
  • 如果存在 ODM 分区,完整 Android 模式或充电模式所需的 ODM 内核模块应位于 /odm/lib/modules 中。如果不存在,这些模块应位于 /vendor/lib/modules 中。
  • 恢复模式所需的 SoC 供应商内核模块和 ODM 内核模块应位于恢复 ramfs/lib/modules 中。
  • 恢复模式和完整 Android/充电模式都需要的内核模块应同时位于恢复 rootfs 以及 /vendor/odm 分区中(如上所述)。
  • 恢复模式所用的内核模块不应依赖仅位于 /vendor/odm 中的模块,因为这些分区在恢复模式下没有装载。
  • SoC 供应商内核模块不应依赖 ODM 内核模块。

在 Android 7.x 及更低版本中,/vendor/odm 分区不会提前装载。在 Android 8.x 及更高版本中,为使模块能够从这些分区加载,已进行相关配置,以便为非 A/B 设备和 A/B 设备提前装载分区。这样还能确保在 Android 模式和充电模式下都会装载分区。

Android 构建系统支持

BoardConfig.mk 中,Android 构建系统定义了 BOARD_VENDOR_KERNEL_MODULES 变量,此变量提供了用于供应商映像的内核模块的完整列表。此变量中列出的模块会被复制到供应商映像的 /lib/modules/ 中,在 Android 中装载后会出现在 /vendor/lib/modules 中(根据上述要求)。下面是一个供应商内核模块的配置示例:

vendor_lkm_dir := device/$(vendor)/lkm-4.x
BOARD_VENDOR_KERNEL_MODULES := \
  $(vendor_lkm_dir)/vendor_module_a.ko \
  $(vendor_lkm_dir)/vendor_module_b.ko \
  $(vendor_lkm_dir)/vendor_module_c.ko

在此示例中,一个供应商内核模块预构建代码库映射到了 Android 构建系统中的上述位置。

恢复映像可能包含一部分供应商模块。Android 构建系统为这些模块定义了 BOARD_RECOVERY_KERNEL_MODULES 变量。示例:

vendor_lkm_dir := device/$(vendor)/lkm-4.x
BOARD_RECOVERY_KERNEL_MODULES := \
  $(vendor_lkm_dir)/vendor_module_a.ko \
  $(vendor_lkm_dir)/vendor_module_b.ko

Android build 负责运行 depmod,以在 /vendor/lib/modules/lib/modules (recovery ramfs) 中生成所需的 modules.dep 文件。

模块加载和版本控制

您可以通过调用 modprobe -ainit.rc* 一次加载所有内核模块。这样可以避免重复初始化 modprobe 二进制文件的 C 运行时环境所产生的开销。您可以修改 early-init 事件以调用 modprobe

on early-init
    exec u:r:vendor_modprobe:s0 -- /vendor/bin/modprobe -a -d \
        /vendor/lib/modules module_a module_b module_c ...

通常,内核模块必须使用将与此模块结合使用的内核进行编译,否则,内核会拒绝加载此模块。CONFIG_MODVERSIONS 通过检测破坏应用二进制接口 (ABI) 合规性的问题提供了一种解决方法。此功能会为内核中导出的每个符号的原型计算循环冗余校验 (CRC) 值,并将这些值作为内核的一部分存储起来;对于内核模块所使用的符号,相应的值也会存储在内核模块中。模块加载完成后,模块所用符号的值将与内核中的相应值进行比较。如果这些值相互匹配,系统将加载模块;如果不匹配,模块加载会失败。

若要使内核映像的更新独立于供应商映像,请启用 CONFIG_MODVERSIONS。这样做可以在确保与供应商映像中的现有内核模块保持兼容的同时,对内核进行小幅度更新(例如 LTS 提供的 bug 修复)。不过,CONFIG_MODVERSIONS 本身并不会解决破坏 ABI 合规性的问题。如果内核中某个导出的符号的原型由于源代码的修改或内核配置更改而发生了变化,会破坏与使用该符号的内核模块的兼容性。在此类情况下,必须重新编译内核模块。

例如,内核中的 task_struct 结构(在 include/linux/sched.h 中定义)包含很多字段,具体包含的字段根据相关条件取决于内核配置。仅当启用了 CONFIG_SCHED_INFO 时(在启用了 CONFIG_SCHEDSTATSCONFIG_TASK_DELAY_ACCT 时发生),才会有 sched_info 字段。如果这些配置选项的状态发生变化,task_struct 结构的布局也将发生变化,同时,从内核导出的使用 task_struct 的所有接口都会发生变化(例如,kernel/sched/core.c 中的 set_cpus_allowed_ptr)。与使用这些接口的之前编译的内核模块的兼容性将会被破坏,这就需要使用新的内核配置重新构建这些模块。

如需详细了解 CONFIG_MODVERSIONS,请参阅内核树中的相关文档 (Documentation/kbuild/modules.rst)。