Linux 内核里,为了解决驱动初始化的难题,引入了 initcall 机制。简单来说,initcall 机制就是一套规范化、自动化的驱动初始化函数管理方案。它在内核编译阶段 “大显身手”,通过一系列精心设计的宏定义,巧妙地将不同驱动的初始化函数按照预设的优先级顺序,依次存放到特定的内存段中。当内核启动时,就如同一位训练有素的指挥官,有条不紊地遍历这些内存段,精准地调用各个初始化函数,确保每个驱动都能在恰当的时机完成初始化,顺利 “上岗”,为系统的稳定运行保驾护航。这一机制不仅让驱动初始化变得井井有条,还极大地减轻了开发者的负担,提升了内核的可维护性,可谓是 Linux 内核中的一大 “得力助手”。
深入到 Linux 内核源码中,initcall 机制的实现可谓精妙绝伦。在 include/linux/init.h 文件里,藏着一系列让人眼花缭乱却又逻辑严密的宏定义,它们是 initcall 机制的 “幕后操控者”。
对于静态加载的驱动,内核定义了诸如 early_initcall、pure_initcall、core_initcall 等众多宏。就拿 core_initcall 来说,它背后其实是 __define_initcall 宏在发挥关键作用。展开来看,__define_initcall(fn, 1)(这里以 core_initcall 的参数 1 为例),经过层层解析,就像是一场奇妙的 “魔术表演”:先是定义了一个静态的函数指针 initcall_t __initcall_##fn##1,这里的 ## 是连接符号的 “胶水”,把函数名 fn 和等级标识 1 紧紧粘在一起,变成一个独一无二的函数指针名称。而 __attribute__((__section__(".initcall1.init"))) 则像是一个精准的 “导航仪”,告诉编译器把这个函数指针变量放到名为 .initcall1.init 的特定代码段中,这个代码段就像是一个为初始化函数精心准备的 “候车室”,等待内核启动时的 “召唤”。并且,__used 这个属性也很关键,它像是给函数指针穿上了一层 “保护衣”,防止编译器在优化过程中,把这个看似暂时没被用到的符号给无情 “抛弃”,确保了机制的完整性。
再看动态加载的情况,以常用的 module_init 宏为例,当我们在驱动代码里写下 module_init(xxx_init) 时,这背后的故事同样精彩。module_init 宏在 include/linux/module.h 中被定义为 __initcall(x),而进一步追溯,它其实就是 device_initcall(x),{BANNED}最佳终也会导向 __define_initcall(x, 6)。这意味着,通过 module_init 修饰的驱动初始化函数,会被安排到优先级为 6 的 .initcall6.init 这个 “候车室” 里,等待内核按部就班地来 “检票上车”,完成初始化流程。
在内核启动流程的 init/main.c 文件中,有一个至关重要的函数 do_initcalls,它就是那位掌控全局的 “指挥官”。当内核启动进入这个环节,do_initcalls 函数开始施展它的 “魔法”。它会依据预先设定好的优先级顺序,如同一位严谨的列车调度员,依次 “调度” 各个等级的初始化函数。从早期初始化的 early_initcall 开始,逐步到后续各级别的 initcall,逐个检查每个优先级对应的代码段,一旦发现有初始化函数指针 “候车”,就立即调用执行,确保驱动们有序地完成初始化,为系统正常运行搭建好坚实的基础。
总体来说,initcall是基于以下思路设计出来的:
-
在生成vmlinux的链接阶段为initcall创建特定的section
-
开发者创建相关的initcall函数,并使用xxx_initcall声明为不同类型
-
每一类initcall对应一组section
-
遍历执行initcall section中的initcalls
xxx_initcall的定义位于include/linux/init.h中,从这个文件的名字也可以看出xxx_initcall是针对初始化操作的。
#define pure_initcall(fn) __define_initcall(fn, 0)
#define core_initcall(fn) __define_initcall(fn, 1)
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall(fn) __define_initcall(fn, 2)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall(fn) __define_initcall(fn, 3)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall(fn) __define_initcall(fn, 4)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall(fn) __define_initcall(fn, 5)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
#define device_initcall(fn) __define_initcall(fn, 6)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall(fn) __define_initcall(fn, 7)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)
从上面的宏定义可以发现,所有的xxx_initcall都是基于__define_initcall的,后者的定义位于同一个文件中,通过__define_initcall将各个xxx_initcall统一到一起,基于ID编号链接到不同的subsection,在同一个subsection中各个initcall的排序以链接的顺序为准。另外,__define_initcall中的ID编号还有另外一个作用,就是防止不同类型的xxx_initcall调用相同的符号引起编译错误。
#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn; \
LTO_REFERENCE_INITCALL(__initcall_##fn##id)
同一类的initcall执行顺序由编译顺序决定,不同类的initcall执行顺序在init/main.c中定义,如下所示:
static initcall_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
在实际执行时,内核必须知道xxx_initcall section所在的位置,而在include/asm-generic/vmlinux.lds.h中将xxx_start和.initcall*.init链接到了一起,这样的话,do_initcalls()遍历不同ID的initcall时,基于xxx_start便可以找到想对应的.initcall entry,然后循环遍历里面的各个initcalls。
#define INIT_CALLS_LEVEL(level) \
VMLINUX_SYMBOL(__initcall##level##_start) = .; \
*(.initcall##level##.init) \
*(.initcall##level##s.init) \
#define INIT_CALLS \
VMLINUX_SYMBOL(__initcall_start) = .; \
*(.initcallearly.init) \
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(rootfs) \
INIT_CALLS_LEVEL(6) \
INIT_CALLS_LEVEL(7) \
VMLINUX_SYMBOL(__initcall_end) = .;
在arch/arm64/kernel/vmlinux.lds中可以看到initcall的符号排布如下图所示,基于*_start可以定位到各个initcall函数所对应的符号。
initcall 机制里,函数的优先级可是 “暗藏玄机”。它一共划分为 8 个等级,从 0 到 7,数字越小,优先级越高,执行顺序也就越早。像 pure_initcall 对应的优先级是 0,意味着它会在内核启动的早期,抢在很多初始化任务之前被调用,适用于那些没有复杂依赖、纯粹进行变量初始化的函数,能快速完成一些基础准备工作;而 late_initcall 优先级为 7,属于 “慢性子”,要等到系统大部分关键初始化都完成,快接近稳定运行状态时才登场,通常用来处理一些对启动顺序不太敏感、可以稍后进行的辅助性初始化,避免过早执行影响系统前期关键流程。
其中,还有些特殊标记,比如 arch_initcall 里的 “arch”,表明和硬件架构紧密相关,这类初始化函数在系统启动初期,硬件初始化阶段就会被调用,确保硬件能快速进入可用状态,为后续驱动和软件运行搭好硬件 “舞台”;rootfs_initcall 涉及根文件系统相关初始化,它的优先级介于 5 和 6 之间,在文件系统相关的初始化流程里找准时机切入,保障文件系统布局、挂载等操作有序完成,让系统能顺利读写文件,为各种应用程序和服务提供数据存储 “根基”。
而且,带 “sync” 后缀的,像 core_initcall_sync 相较于 core_initcall,多了同步操作的意味。它会在执行完前一级初始化后,等待一些关键条件达成或资源准备好,才继续后续操作,保证系统状态的一致性,避免因异步执行可能带来的资源竞争、数据不一致等隐患,让初始化流程更加稳健。
假设我们现在有三个驱动:i2c_driver、video_driver 和 audio_driver。
i2c_driver 负责管理系统中的 I2C 总线设备,它需要在系统启动早期就完成初始化,以便后续挂载在 I2C 总线上的各类传感器、控制器等设备能及时被识别和配置,那我们就可以使用 arch_initcall(i2c_driver_init),把它的初始化函数优先级设高,确保硬件层面的通信基础尽早搭建好。
video_driver 用于驱动显卡,让显示器能正常输出图像,但它依赖一些内核子系统的基本框架搭建完成,比如内存管理子系统要先准备好显存分配的机制,此时使用 subsys_initcall(video_driver_init) 较为合适,在子系统初始化中期阶段介入,与依赖的子系统协同初始化,保障视频输出功能顺利启用。
audio_driver 相对来说,对启动及时性要求没那么高,只要在系统快要进入用户交互阶段,能正常播放声音即可,所以采用 late_initcall(audio_driver_init),放在较晚的优先级,避免过早初始化占用资源,还可能因其他关键系统组件未就绪而出现异常,确保音频服务在合适的时候 “低调登场”。
当内核启动,执行到 do_initcalls 函数时,就会按照 arch_initcall、subsys_initcall、late_initcall 的优先级顺序,依次检查对应的代码段。先找到存放 i2c_driver_init 函数指针的 .initcall3.init 段(假设 arch_initcall 对应 3,实际依内核版本和架构而定),执行 i2c_driver_init;接着在轮到 subsys_initcall 优先级时,从 .initcall4.init 段调用 video_driver_init;{BANNED}最佳后在其他大部分初始化都收尾时,从 .initcall7.init 段执行 audio_driver_init,有条不紊地让各个驱动在恰当的时机 “闪亮登场”,开启各自的使命,保障系统从启动到稳定运行的每一步都稳稳当当。
Linux 内核提供了一组来自头文件 include/linux/init.h 的宏,来标记给定的函数为 initcall。所有这些宏都相当简单:
#define early_initcall(fn) __define_initcall(fn, early)
#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)
我们可以看到,这些宏只是从同一个头文件的 __define_initcall 宏的调用扩展而来。此外,__define_initcall 宏有两个参数:
既然 Linux 内核可以调用 initcalls,我们就来看下 Linux 内核是如何做的。这个过程从 init/main.c 头文件的 do_basic_setup 函数开始:
该函数在 Linux 内核初始化过程中调用,调用时机是主要的初始化步骤,比如内存管理器相关的初始化、CPU子系统等完成之后。do_initcalls函数只是遍历initcall级别数组,并调用每个级别的do_initcall_level函数:
static void __init do_initcalls(void)
{
int level;
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level);
}
通过这种方式,Linux 内核以正确的顺序完成了很多子系统的初始化。现在我们知道 Linux 内核的 initcall 机制是怎么回事了。在这部分中,我们介绍了 initcall 机制的主要部分,但遗留了一些重要的概念。让我们来简单看下这些概念。
首先,我们错过了一个级别的 initcalls,就是 rootfs initcalls。和我们在本部分看到的很多宏类似,你可以在 include/linux/init.h 头文件中找到 rootfs_initcall 的定义:
#define rootfs_initcall(fn) __define_initcall(fn, rootfs)
从这个宏的名字我们可以理解到,它的主要目的是保存和 rootfs 相关的回调。除此之外,只有在与设备相关的东西没被初始化时,在文件系统级别初始化以后再初始化一些其它东西时才有用。例如,发生在源码文件 init/initramfs.c 中 populate_rootfs 函数里的解压 initramfs:
rootfs_initcall(populate_rootfs);
除了 rootfs_initcall 级别,还有其它的 console_initcall、 security_initcall 和其他辅助的 initcall 级别。我们遗漏的{BANNED}最佳后一件事,是 *_initcall_sync 级别的集合。在这部分我们看到的几乎每个 *_initcall 宏,都有 _sync 前缀的宏伴随:
#define core_initcall_sync(fn) __define_initcall(fn, 1s)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall_sync(fn) __define_initcall(fn, 3s)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall_sync(fn) __define_initcall(fn, 5s)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall_sync(fn) __define_initcall(fn, 7s)
这些附加级别的主要目的是,等待所有某个级别的与模块相关的初始化例程完成。