一、文件位置
在讲述模块加载顺序控制前,我们需要了解一些基础知识点,也就是模块位置。
启动模式 | 存储 | 显示 | 拨号键盘 | 电池 | PMIC | 触摸屏 | NFC/WLAN/蓝牙 | 传感器 | 相机 |
---|---|---|---|---|---|---|---|---|---|
恢复 | Y | Y | Y | Y | Y | N | N | N | N |
充电 | Y | Y | Y | Y | Y | N | N | N | N |
Android | Y | Y | Y | Y | Y | Y | Y | Y | Y |
如果使用了内核模块,它们在文件系统中的放置位置的要求如下:
-
所有内核都应内置对启动和装载分区的支持。
-
必须从只读分区加载内核模块。
-
对于需要支持启动时验证的设备,应从已验证的分区加载内核模块。
-
内核模块不应位于
/system
中。 -
完整 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针对GKI2.0定义的,对研发(小米项目)来说,我们所需要关注的只有下面几个:
- ACK内的属于GKI的build-in tree的驱动,这部分驱动模块在defconfig中被配置为y
- build-out tree的驱动分为两部分,一部分会被编译到vendor_boot.img中(非GKI项目在boot.img中),另一部分会被编译到vendor.img中
上图就是GKI项目和非GKI项目的一些区别。
二、Android构建系统支持
在 BoardConfig.mk
中,Android 构建系统定义了 BOARD_VENDOR_KERNEL_MODULES
变量,此变量提供了用于供应商映像的内核模块的完整列表。此变量中列出的模块会被复制到供应商映像的 /lib/modules/
中,在 Android 中装载后会出现在 /vendor/lib/modules
中(根据上述要求)。
下面是一个供应商内核模块的配置示例:
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 构建系统为这些模块定义了 BOARD_RECOVERY_KERNEL_MODULES
变量。示例:
BOARD_RECOVERY_KERNEL_MODULES := \
$(vendor_lkm_dir)/vendor_module_a.ko \
$(vendor_lkm_dir)/vendor_module_b.ko
Android 构建系统负责运行 depmod
,以在 /vendor/lib/modules
和 /lib/modules
(recovery ramfs
) 中生成所需的 modules.dep
文件。
2.1 模块位置
ramdisk 是第一阶段 init,
的文件系统,也是 A/B 设备和虚拟 A/B 设备上的恢复/fastboot 映像的文件系统。这是一个 initramfs
。
-
第一阶段
init
供应商内核模块,位于/lib/modules/
。 -
modprobe
配置文件,位于/lib/modules/
:modules.dep
、modules.softdep
、modules.alias
、modules.options
。 -
一个
modules.load
文件,用于指示在第一阶段 init 期间加载的模块及相应的加载顺序,位于/lib/modules/
。 -
供应商恢复内核模块,用于 A/B 和虚拟 A/B 设备,位于
/lib/modules/
。 -
modules.load.recovery
,用于指示要加载的模块及相应的加载顺序,用于 A/B 和虚拟 A/B 设备,位于/lib/modules
。
2.2 启动阶段
第一阶段 init
首先从 ramdisk 上的 /lib/modules/
读取 modprobe 配置文件。接下来,读取 /lib/modules/modules.load
(在恢复过程中,则为 /lib/modules/modules.load.recovery
)中指定的模块列表,并尝试按照之前加载的文件中指定的配置,依次加载各个模块。为了满足硬依赖项或软依赖项的要求,实际执行顺序可以不同于请求的顺序。
2.2.1 build 支持,第一阶段 init
如需指定要复制到供应商 ramdisk cpio 的内核模块,请在 BOARD_VENDOR_RAMDISK_KERNEL_MODULES
中将其列出。build 会对这些模块运行 depmod
,并将生成的 modprobe 配置文件放在供应商 ramdisk cpio 中。
build 还会创建一个 modules.load
文件,并将其存储在供应商 ramdisk cpio 中。默认情况下,该文件包含 BOARD_VENDOR_RAMDISK_KERNEL_MODULES
中列出的所有模块。如需替换该文件的内容,请使用 BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD
,如以下示例所示:
BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD := \
device/vendor/mydevice-kernel/first.ko \
device/vendor/mydevice-kernel/second.ko \
device/vendor/mydevice-kernel/third.ko
2.2.2 第二阶段 init、vendor 或 vendor_dlkm 分区
由于第一阶段 init 过程是序列化的,因此并行化启动过程的机会并不多。如果一个模块在完成第一阶段 init 时用不到,请将模块放入 vendor 或 vendor_dlkm
分区,从而将其移至第二阶段 init
BOARD_VENDOR_KERNEL_MODULES_LOAD
:要在第二阶段 init 中加载的模块列表
BOARD_VENDOR_KERNEL_MODULES
中列出的内核模块会由 Android 平台 build 复制到 vendor 分区中的 /vendor/lib/modules
。平台 build 会对这些模块运行 depmod
,并将 depmod
输出文件复制到 vendor 分区中的相同位置。从 /vendor
加载内核模块的机制与以前的 Android 版本相同。您可以决定如何以及何时加载这些模块,尽管这通常是使用 init.rc
脚本来完成的。
2.3 recovery模式
在之前的 Android 版本中,恢复所需的内核模块在 BOARD_RECOVERY_KERNEL_MODULES
中指定。在 Android 11 中,恢复所需的内核模块仍使用该宏指定。但是,恢复内核模块会被复制到供应商 ramdisk cpio,而不是通用的 ramdisk cpio。默认情况下,BOARD_RECOVERY_KERNEL_MODULES
中列出的所有内核模块都会在第一阶段 init
期间加载。如果您只想加载这些模块中的一部分,请在 BOARD_RECOVERY_KERNEL_MODULES_LOAD
中指定这一部分的内容。
2.4 模块加载逻辑总结
宏 | 解释 |
---|---|
BOARD_VENDOR_RAMDISK_KERNEL_MODULES | 要复制到 ramdisk 的模块列表 |
BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD | 要在第一阶段 init 中加载的模块列表 |
BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD | 从 ramdisk 中选择 recovery 或 fastbootd 时要加载的模块列表 |
BOARD_VENDOR_KERNEL_MODULES | 要复制到 vendor 或 vendor_dlkm 分区中的 /vendor/lib/modules/ 目录下的模块列表 |
BOARD_VENDOR_KERNEL_MODULES_LOAD | 要在第二阶段 init 中加载的模块列表 |
三、MTK平台下的模块加载控制
mtk设计模块加载控制也是在android规定下进行的,他也必须满足第2.4节中的这些宏的定义。
3.1 ko_order_table.csv
在mtk平台适配驱动时,我们无论如何都避不开的一个文件,就是ko_order_table.csv
,这个表格文件很简单,一行一个驱动,以逗号分割每个变量的意义。下面详细描述一下每个变量的意义!
KO name (*.ko),KO path,Vendor/Ramdisk in Normal(vendor/ramdisk),Need loaded before BootTime.completed(Y/N),Recovery(Y/N),Build mode
bootprof.ko,/drivers/misc/mediatek/mtprof/bootprof.ko,ramdisk,Y,Y,user/userdebug/eng
irq-dbg.ko,/drivers/misc/mediatek/sda/irq-dbg.ko,ramdisk,Y,Y,user/userdebug/eng
dbgtop-drm.ko,/drivers/misc/mediatek/sda/dbgtop-drm.ko,ramdisk,Y,Y,user/userdebug/eng
mrdump.ko,/drivers/misc/mediatek/aee/mrdump/mrdump.ko,ramdisk,Y,Y,user/userdebug/eng
变量 | 解释 |
---|---|
KO name (*.ko) | 驱动模块的名字,注意:填写时需要注意需要加上尾缀 .ko |
KO path | 驱动模块的路径 |
Vendor/Ramdisk | 控制模块编译到何处! |
Need loaded before BootTime.comleted(Y/N) | |
Recovery(Y/N) | 控制驱动是否需要在Recovery模式加载 |
Build mode | user/userdebug/eng,控制该驱动哪些版本需要编译 |
3.2 编译逻辑
在gki_ko.mk中,会对ko_order_table.csv
进行解析
3.2.1 csv文件的解析
ifneq ($(wildcard $(VEXT_TARGET_PROJECT_FOLDER)/ko_order_table.csv),)
ko_order_table := $(VEXT_TARGET_PROJECT_FOLDER)/ko_order_table.csv
else
ko_order_table := $(VEXT_PROJECT_FOLDER)/ko_order_table.csv
endif // 上面这一段是找到ko_order_table.csv的路径
ko_order_table_contents := $(shell cat $(ko_order_table) | cut -d , -f 1-6 | sed 's/ //g') // 按行解析csv文件,并以','作为分隔符,并去除所有的空格
ko_comma := ,
$(foreach ko,$(ko_order_table_contents), \ //遍历这个列表
$(eval ko_line := $(subst $(ko_comma), ,$(ko))) \
$(eval ko_name := $(word 1,$(ko_line))) \ //每一行的第一个参数,赋值给ko_name,也就是模块名
$(eval ko_path := $(word 2,$(ko_line))) \ //每一行的第二个参数,赋值给ko_path,也就是模块路径
$(eval ko_partition := $(word 3,$(ko_line))) \ //每一行的第三个参数,赋值给ko_partition,也就是模块位置
$(eval ko_loaded := $(word 4,$(ko_line))) \ //每一行的第四个参数,赋值给ko_loaded,也就是是否需要加载
$(eval ko_recovery := $(word 5,$(ko_line))) \ //每一行的第五个参数,赋值给ko_recovery
$(eval ko_mode := $(subst /, ,$(word 6,$(ko_line)))) \ //每一行的第五个参数,赋值给ko_mode,也就是user/userdebug
$(if $(filter $(TARGET_BUILD_VARIANT),$(ko_mode)),\ //判断ko_mode和android编译的版本是否一致,不一致舍弃
$(if $(filter vendor,$(ko_partition)),\ // 如果ko_mode匹配上,判断ko_partition是不是vendor
$(eval BOARD_VENDOR_KERNEL_MODULES += $(KERNEL_OUT)$(ko_path))\ //如果被设置为vendor,则设置BOARD_VENDOR_KERNEL_MODULES
$(if $(filter Y,$(ko_loaded)),\
$(eval BOARD_VENDOR_KERNEL_MODULES_LOAD += $(KERNEL_OUT)$(ko_path))))\ //同时如果ko_loaded被设置为Y,则设置BOARD_VENDOR_KERNEL_MODULES_LOAD
$(if $(filter ramdisk,$(ko_partition)),\
$(eval BOARD_VENDOR_RAMDISK_KERNEL_MODULES += $(KERNEL_OUT)$(ko_path))\ //如果ko_partition被设置为ramdisk,则设置BOARD_VENDOR_RAMDISK_KERNEL_MODULES
$(if $(filter Y,$(ko_loaded)),\
$(eval BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD += $(KERNEL_OUT)$(ko_path))))\ ///同时如果ko_loaded被设置为Y,则设置BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD
$(if $(filter Y,$(ko_recovery)),\
$(eval BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD += $(KERNEL_OUT)$(ko_path)))\ //如果ko_recovery被设置,则设置BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD
) \
)
BOARD_VENDOR_RAMDISK_KERNEL_MODULES += $(BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD) //BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD的值追加到BOARD_VENDOR_RAMDISK_KERNEL_MODULES
这个就是ko_order_table.csv
的作用,它的目的只是在Android的规定下做了一层转化,方便研发添加模块的控制。
3.2.2 模块顺序的编译逻辑
在BoardConfig.mk中,有如下的代码:
MTK_SPLIT_BOARD_RECOVERY_KERNEL_MODULES := $(patsubst $(KERNEL_OUT)/%,%,$(BOARD_RECOVERY_KERNEL_MODULES))
MTK_SPLIT_BOARD_RECOVERY_KERNEL_MODULES_LOAD := $(patsubst $(KERNEL_OUT)/%,%,$(BOARD_RECOVERY_KERNEL_MODULES_LOAD))
MTK_SPLIT_BOARD_VENDOR_RAMDISK_KERNEL_MODULES := $(patsubst $(KERNEL_OUT)/%,%,$(BOARD_VENDOR_RAMDISK_KERNEL_MODULES))
MTK_SPLIT_BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD := $(patsubst $(KERNEL_OUT)/%,%,$(BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD))
MTK_SPLIT_BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD := $(patsubst $(KERNEL_OUT)/%,%,$(BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD))
MTK_SPLIT_BOARD_VENDOR_KERNEL_MODULES := $(patsubst $(KERNEL_OUT)/%,%,$(BOARD_VENDOR_KERNEL_MODULES))
MTK_SPLIT_BOARD_VENDOR_KERNEL_MODULES_LOAD := $(patsubst $(KERNEL_OUT)/%,%,$(BOARD_VENDOR_KERNEL_MODULES_LOAD))
MTK_SPLIT_BOARD_ODM_KERNEL_MODULES := $(patsubst $(KERNEL_OUT)/%,%,$(BOARD_ODM_KERNEL_MODULES))
MTK_SPLIT_BOARD_ODM_KERNEL_MODULES_LOAD := $(patsubst $(KERNEL_OUT)/%,%,$(BOARD_ODM_KERNEL_MODULES_LOAD))
这步操作会将第3.2.1节中解析出来的csv文件中的内容,再分别赋值给MTK_SPLIT_XXXXX
系列变量。
再split_vext.mk中定义的 vext.target_files.zip的编译规则中,有如下的步骤:
$(hide) $(foreach ko,$(MTK_SPLIT_BOARD_VENDOR_KERNEL_MODULES),echo $(ko) >>$(zip_root)/META_TEMP/VENDOR_KERNEL_MODULES.txt;)
$(hide) $(foreach ko,$(MTK_SPLIT_BOARD_VENDOR_RAMDISK_KERNEL_MODULES),echo $(ko) >>$(zip_root)/META_TEMP/VENDOR_RAMDISK_KERNEL_MODULES.txt;)
$(hide) $(foreach ko,$(MTK_SPLIT_BOARD_RECOVERY_KERNEL_MODULES),echo $(ko) >>$(zip_root)/META_TEMP/RECOVERY_KERNEL_MODULES.txt;)
$(hide) $(foreach ko,$(MTK_SPLIT_BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES),echo $(ko) >>$(zip_root)/META_TEMP/VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES.txt;)
$(hide) $(foreach ko,$(MTK_SPLIT_BOARD_ODM_KERNEL_MODULES),echo $(ko) >>$(zip_root)/META_TEMP/ODM_KERNEL_MODULES.txt;)
$(hide) $(foreach ko,$(MTK_OUT_OF_TREE_KERNEL_MODULES),echo $(ko) >>$(zip_root)/META_TEMP/OUT_OF_TREE_KERNEL_MODULES.txt;)
这些代码的目的就是遍历这些变量,将变量的内容写入到相应的文本文件中。
注意:这里就涉及到顺序了!
vendor/mediatek/proprietary/scripts/releasetools/merge_boot_image.py
注意:这个脚本不详细介绍,如果需要研究,请一定要阅读全部的代码,这会使你对于MTK平台boot/vendor_boot/init_boot等镜像的编译逻辑有深刻的理解
if has_vendor_boot:
if 'MTK_SPLIT_BOARD_RECOVERY_KERNEL_MODULES_LOAD' in project_build_vars['vext']:
WriteModuleMeta(temp_dir,
'RECOVERY/RAMDISK/lib/modules/modules.load.recovery',
project_build_vars['vext']['MTK_SPLIT_BOARD_RECOVERY_KERNEL_MODULES_LOAD'],
True)
if 'MTK_SPLIT_BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD' in project_build_vars['vext']:
WriteModuleMeta(temp_dir,
'VENDOR_BOOT/RAMDISK/lib/modules/modules.load',
project_build_vars['vext']['MTK_SPLIT_BOARD_VENDOR_RAMDISK_KERNEL_MODULES_LOAD'],
True)
if 'MTK_SPLIT_BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD' in project_build_vars['vext']:
WriteModuleMeta(temp_dir,
'VENDOR_BOOT/RAMDISK/lib/modules/modules.load.recovery',
project_build_vars['vext']['MTK_SPLIT_BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES_LOAD'],
True)
if 'MTK_SPLIT_BOARD_VENDOR_KERNEL_MODULES_LOAD' in project_build_vars['vext']:
WriteModuleMeta(temp_dir,
'VENDOR/lib/modules/modules.load',
project_build_vars['vext']['MTK_SPLIT_BOARD_VENDOR_KERNEL_MODULES_LOAD'],
True)
if 'MTK_SPLIT_BOARD_VENDOR_KERNEL_MODULES' in project_build_vars['vext']:
WriteModuleMeta(temp_dir,
'META_TEMP/VENDOR_KERNEL_MODULES.txt',
project_build_vars['vext']['MTK_SPLIT_BOARD_VENDOR_KERNEL_MODULES'],
False)
if 'MTK_SPLIT_BOARD_VENDOR_RAMDISK_KERNEL_MODULES' in project_build_vars['vext']:
WriteModuleMeta(temp_dir,
'META_TEMP/VENDOR_RAMDISK_KERNEL_MODULES.txt',
project_build_vars['vext']['MTK_SPLIT_BOARD_VENDOR_RAMDISK_KERNEL_MODULES'],
False)
if 'MTK_SPLIT_BOARD_RECOVERY_KERNEL_MODULES' in project_build_vars['vext']:
WriteModuleMeta(temp_dir,
'META_TEMP/RECOVERY_KERNEL_MODULES.txt',
project_build_vars['vext']['MTK_SPLIT_BOARD_RECOVERY_KERNEL_MODULES'],
False)
if 'MTK_SPLIT_BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES' in project_build_vars['vext']:
WriteModuleMeta(temp_dir,
'META_TEMP/VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES.txt',
project_build_vars['vext']['MTK_SPLIT_BOARD_VENDOR_RAMDISK_RECOVERY_KERNEL_MODULES'],
False)
vendor_ramdisk_copy_files = post_process_target_files.prepare_split_kernel_modules(
temp_dir,
os.path.join(project_build_vars['mgk']['PRODUCT_OUT'], 'obj'),
temp_dir,
'VENDOR_RAMDISK',
project_build_vars['mgvi']['HOST_OUT'])
if vendor_ramdisk_copy_files is None:
logger.error('No vendor boot kernel module generated')
ret = 1
else:
android_llvm_binutils = 'prebuilts/clang/host/linux-x86/llvm-binutils-stable'
llvm_objcopy = os.path.join(android_llvm_binutils, 'llvm-objcopy')
for key, value in vendor_ramdisk_copy_files.items():
target_file = os.path.join(temp_dir, key)
target_dir = os.path.dirname(target_file)
if not os.path.exists(target_dir):
os.makedirs(target_dir)
logger.info('%s -> %s', value, key)
shutil.copy(value, target_file)
if key.endswith('.ko'):
common.RunAndCheckOutput([llvm_objcopy, '--strip-debug', target_file])
RepackBootImage(output_dir, 'vendor_boot.img', project_build_vars)
这段的目的其实很简单,将上一个步骤写入的数据的文本文件再次根据不同的规则写入到不同的路径下:VENDOR_BOOT/VENDOR/META_EMP
等等。这些路径会在编译各个镜像时用到,此处以VENDOR_BOOT
为例
'VENDOR_BOOT/RAMDISK/lib/modules/modules.load'这个文件会执行
vendor_ramdisk_copy_files = post_process_target_files.prepare_split_kernel_modules(
temp_dir,
os.path.join(project_build_vars['mgk']['PRODUCT_OUT'], 'obj'),
temp_dir,
'VENDOR_RAMDISK',
project_build_vars['mgvi']['HOST_OUT'])
再prepare_split_kernel_modules函数中会执行depmod指令,来处理依赖,生成相关的文件
注意:如果需要很详细的了解,一定需要看一下这部分的代码!尤其是内核的同学
3.3 树外驱动的编译控制
树外驱动,比如:vendor/mediatek/kernel_modules/connectivity/wlan/core/gen4m/
那这些驱动是如何控制加载的呢?
事实上,我们可以注意到一点,这些树外驱动的编译是受Android.mk文件控制的,而在这些Android.mk中都有这么一行
include $(MTK_KERNEL_MODULE)
这个MTK_KERNEL_MODULE
是MTK平台设置的一个模板,可以方便研发能够在树外快速的编译模块(树外驱动的编译逻辑,本篇不做拓展)。
所以我们可以知道,树外驱动只能够被编译到vendor或者vendor_dlkm中,然后会在prepare_split_kernel_modules
做统一处理。(树内树外驱动的处理请自行学习拓展)。
四、总结
对于研发来设置模块加载顺序的时候,可以通过设置ko_order_table.csv
里的顺序来控制模块加载的顺序!
简单来说有下面几个原则:
{% tip success %}
- ko_partition设置为ramdisk的先加载,设置为vendor的后加载(ramdisk加载是第一阶段加载,会导致开机时间增加,不是必须一般不加到ramdisk)
- ko_partition一致的,在前面行的模块先于后面行的模块加载!
{% endtip %}