从一行
module_init到内核自动调用,揭秘驱动初始化完整链路
写 Linux 驱动时,几乎每个驱动都会写 module_init(xxx_init)。这个宏像一层 “魔法”,只需定义,驱动初始化函数就能在开机阶段被内核自动执行。
你是否思考过这几个核心问题: 这个宏底层做了什么?内核如何发现并执行它?我们从未手动调用,函数为何能自动运行?
本文完整拆解 Initcall 整套机制,把整条链路讲透。同时,这套机制也是排查Linux Kernel 启动缓慢问题的重要依据,后文将结合实战场景详细说明具体排查方法。
举个实例:系统存在触摸屏、摄像头两个驱动,代码中分别注册: module_init(touch_init)、module_init(sensor_init)
.initcall6.inittouch.c:module_init(touch_init) → 生成指针,放入 .initcall6.init
sensor.c:module_init(sensor_init) → 生成指针,放入 .initcall6.init
.initcall6.init 段内存布局┌────────────────────────────────────────────┐│ touch_init 函数指针 ││ sensor_init 函数指针 ││ 其他所有 module_init 注册的函数指针 │└────────────────────────────────────────────┘
取数组第 0 项:执行 touch_init()
取数组第 1 项:执行 sensor_init()
循环遍历剩余所有初始化函数
编译时:
module_init将函数指针挂载到对应优先级.initcallN.init段链接时:链接脚本按优先级合并分段,生成有序连续数组,并导出段起止地址符号
运行时:内核按优先级顺序遍历数组,串行执行全部初始化函数
理清整体框架后,我们开始逐层拆解内核底层实现细节。首先需要解决一个核心问题:内核究竟是通过什么方式,把零散的驱动初始化函数统一收集、归类分级?这一能力完全依靠内核初始化宏体系实现,下面详细说明。
内核将开机初始化函数划分为 8 个执行优先级(0~7),数字越小执行越早。所有注册宏定义在 include/linux/init.h。
#define __define_initcall(fn, id) \static initcall_t __initcall_##fn##id __used \__attribute__((__section__(".initcall" #id ".init"))) = fn// 8级初始化优先级注册宏#define pure_initcall(fn) __define_initcall(fn, 0)#define core_initcall(fn) __define_initcall(fn, 1)#define postcore_initcall(fn) __define_initcall(fn, 2)#define arch_initcall(fn) __define_initcall(fn, 3)#define subsys_initcall(fn) __define_initcall(fn, 4)#define fs_initcall(fn) __define_initcall(fn, 5)#define device_initcall(fn) __define_initcall(fn, 6)#define late_initcall(fn) __define_initcall(fn, 7)// 驱动通用 module_init 默认等价 device_initcall,优先级6#define module_init(x) __initcall(x)#define __initcall(fn) device_initcall(fn)
以 core_initcall(foo) 为例,预处理后生成代码:
static initcall_t __initcall_foo1 __used__attribute__((__section__(".initcall1.init"))) = foo;
逻辑说明:
定义静态函数指针变量 __initcall_foo1
通过 __attribute__((section)) 指定变量存放至 .initcall1.init 段
指针赋值为待初始化函数 foo
结合上面宏定义可以精准得知:我们日常开发驱动使用的 module_init 本质等价 device_initcall,所有通过该宏注册的驱动初始化函数指针,都会统一收集到 .initcall6.init 段中。
除此之外,内核还划分了 0~7 共八个初始化优先级,配套不同注册宏,各类内核子系统、架构、核心组件的初始化函数,会分别存入 .initcall0.init ~ .initcall7.init 对应的专属分段。
编译完成后,所有初始化函数指针都分散存储在各个目标文件(驱动源码编译生成的 .o 文件)的独立分段中,内存地址杂乱不连续,内核无法统一遍历执行。因此,内核依靠专属链接脚本,在链接阶段完成所有分散分段的优先级排序、合并规整,最终生成一段地址连续、有序的函数指针数组,为内核运行时调用提供基础。
各个源文件编译生成目标文件后,分散的初始化分段需要通过内核链接脚本统一整合处理,核心规则定义在 include/asm-generic/vmlinux.lds.h 中:
#define INIT_CALLS_LEVEL(level) \.initcall##level##.init : { \__initcall##level##_start = .; \KEEP(*(.initcall##level##.init)) \__initcall##level##_end = .; \}// 按 0~7 从小到大顺序拼接所有初始化分段#define INIT_CALLS \INIT_CALLS_LEVEL(0) \INIT_CALLS_LEVEL(1) \INIT_CALLS_LEVEL(2) \INIT_CALLS_LEVEL(3) \INIT_CALLS_LEVEL(4) \INIT_CALLS_LEVEL(5) \INIT_CALLS_LEVEL(6) \INIT_CALLS_LEVEL(7)
.initcall0.init : {__initcall0_start = .; // 记录本段起始内存地址KEEP(*(.initcall0.init)) // 保留所有文件内该段内容,防止链接器丢弃__initcall0_end = .; // 记录本段结束内存地址}.initcall1.init : {__initcall1_start = .; // 紧接上一级段末尾连续排布KEEP(*(.initcall1.init))__initcall1_end = .;}// 2~7级分段排布规则完全一致
┌─────────────────────────────────────────────────────────────┐│ 0级段 │ 1级段 │ 2级段 │ ... │ 6级段(module_init) │ 7级段 ││ 函数1 │ 函数1 │ 函数1 │ │ touch_init │ 函数1 ││ 函数2 │ 函数2 │ │ │ sensor_init │ │└─────────────────────────────────────────────────────────────┘▲ ▲ ▲__initcall0_start __initcall6_start __initcall7_end
至此,链接脚本完成了所有初始化函数的「有序收纳」,在内核镜像中生成了地址连续、优先级有序的函数指针数组,并暴露起止符号供内核调用。接下来内核便可依托这些边界符号,在开机阶段遍历并执行所有初始化函数。关键点:
分段严格按优先级顺序连续排布,天然构成有序数组
导出 __initcallX_start / __initcallX_end 全局符号,C 内核代码可直接读取段边界
链接脚本导出段边界符号后,init/main.c 内代码通过双层循环遍历、执行全部 initcall 函数。
// 链接脚本导出的各级段起始地址extern initcall_entry_t __initcall0_start[];extern initcall_entry_t __initcall1_start[];extern initcall_entry_t __initcall2_start[];extern initcall_entry_t __initcall3_start[];extern initcall_entry_t __initcall4_start[];extern initcall_entry_t __initcall5_start[];extern initcall_entry_t __initcall6_start[];extern initcall_entry_t __initcall7_start[];extern initcall_entry_t __initcall_end[];// 各级段起始地址数组,用于循环遍历static initcall_entry_t *initcall_levels[] = {__initcall0_start,__initcall1_start,__initcall2_start,__initcall3_start,__initcall4_start,__initcall5_start,__initcall6_start,__initcall7_start,__initcall_end, // 全局数组结束标记};
级别0:[__initcall0_start, __initcall1_start)级别1:[__initcall1_start, __initcall2_start)...级别6:[__initcall6_start, __initcall7_start)级别7:[__initcall7_start, __initcall_end)
static void __initdo_initcalls(void){int level;// 外层循环:按优先级从小到大遍历8个等级for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) {initcall_entry_t *fn;// 内层循环:遍历当前等级内所有初始化函数for (fn = initcall_levels[level];fn < initcall_levels[level + 1];fn++) {int ret = do_one_initcall(initcall_from_entry(fn));// 开启initcall_debug时,打印初始化失败日志if (ret && ret != -ENODEV && initcall_debug)pr_err("initcall %pS failed with error %d\n",initcall_from_entry(fn), ret);}}}
所有初始化函数执行完毕后,内核调用 free_initmem() 一次性释放整个 .init 段内存,还给系统。
__init 的函数代码 | |
__initdata 的全局变量 | |
内核镜像加载 → do_initcalls 分级执行所有初始化 → free_initmem 释放.init段│ │ │▼ ▼ ▼.init段载入内存 串行执行全部驱动初始化 .init段内存回收,可重新分配│▼开机后访问__init函数 → 内核Oops崩溃
这里介绍开发中最常见、最容易踩的坑:被 __init 修饰的函数,仅允许在内核初始化阶段调用,开机完成后该函数所在内存会被整体释放,后续绝对不能再次调用。
很多新手会误以为 __init 函数常驻内存,在业务流程中二次调用已回收的 init 函数,最终引发内核崩溃。示例如下:
// 仅内核开机初始化阶段可执行static int __init buggy_init(void){// 驱动初始化逻辑return 0;}// 开机完成后,业务线程/后续流程再次调用void later_func(void){buggy_init(); // 严重错误:函数内存已被 free_initmem 回收,触发内核崩溃}
核心规范:所有带 __init 的函数,生命周期仅限内核开机初始化阶段,初始化结束后内存立即回收,不允许任何后续业务逻辑再次调用。
方案一:删除开机完成后对该函数 / 资源的所有调用逻辑;
方案二:移除函数的 __init 修饰符,让代码常驻内核内存,不参与回收。
Initcall 机制最常用的调试场景:排查驱动拖慢开机速度问题。 每个初始化函数执行都会进入 do_one_initcall,内部预埋两个追踪打点:
int __init_or_module do_one_initcall(initcall_t fn){do_trace_initcall_start(fn); // 执行前打点,记录起始时间ret = fn(); // 执行驱动初始化函数do_trace_initcall_finish(fn, ret); // 执行后打点,计算总耗时return ret;}
在内核启动 cmdline 添加参数 initcall_debug(或者把initcall_debug变量设置为1然后重新编译kernel),重启后通过 dmesg 查看日志,示例输出:
[ 1.480817] initcall touch_init+0x0/0x44 returned 0 after 242775 usecs[ 1.983763] initcall sensor1_init_module+0x0/0x30 returned 0 after 455168 usecs[ 2.242179] initcall sensor2_init_module+0x0/0x30 returned 0 after 252128 usecs
日志直接输出每个初始化函数耗时(单位微秒),快速定位耗时驱动。示例中摄像头驱动累计耗时超 700ms,为开机优化重点对象。
回到开篇核心疑问:module_init 如何被内核自动调用?整套 Initcall 机制完整链路如下:
编译期:通过 __define_initcall 宏,将初始化函数指针存入对应优先级 .initcallN.init 段;
链接期:链接脚本按 0~7 优先级顺序拼接所有分段,导出段起止符号,形成有序指针数组;
运行期:内核双层循环遍历数组,串行执行全部初始化函数;执行完毕调用 free_initmem 释放 .init 段内存;
调试工具:initcall_debug 在执行前后打点计时,打印每个驱动初始化耗时,用于开机性能排查。
module_init 不只是简单注册宏,而是一套编译收集、链接排序、运行遍历的内核底层基础设施。理解这套机制,就能彻底看懂 Linux 驱动自动初始化的底层逻辑。
下面写个迷你 Initcall用来说明这套机制,敬请关注。