本文将介绍 Linux 内核的初始化函数结构,其通过 initcall 机制来实现,其核心是一系列优先级不同的宏,用于控制初始化函数的执行顺序。本文的实验的内核环境基于 Linux 5.10。

initcall 的级别与执行顺序

Linux 内核定义了多个初始化级别(按照执行顺序从高到低排列):

宏定义 级别名 优先级值 用途说明
early_initcall(fn) early - 早期的初始化,在 SMP 初始化之前
pure_initcall(fn) pure 0 用于初始化不能被静态初始化的变量
core_initcall(fn) core 1 核心子系统初始化
postcore_initcall(fn) postcore 2 核心子系统之后的初始化
arch_initcall(fn) arch 3 架构相关初始化
subsys_initcall(fn) subsys 4 子系统初始化
fs_initcall(fn) fs 5 文件系统初始化
rootfs_initcall(fn) rootfs - 根文件系统初始化
device_initcall(fn) device 6 设备驱动初始化(最常用
late_initcall(fn) late 7 晚期初始化(依赖其他子系统就绪)
console_initcall(fn) console - 终端初始化

每个级别都有对应的 _sync 版本(如 core_initcall_sync),用于等待该级别所有异步初始化完成。

为了直观的看到不同 initcall 的先后顺序,可以编写一个简单的测试程序加以验证。

目标架构以 RISC-V 为例,新建一个 C 源文件 arch/riscv/kernel/initcall_test.c,代码如下:

#include <linux/init.h>
#include <linux/kernel.h>
#include <linux/module.h>

static int __init early_test_init(void)
{
    printk(KERN_INFO "[RISC-V Initcall] early_initcall executed\n");
    return 0;
}
early_initcall(early_test_init);

static int __init pure_test_init(void)
{
    printk(KERN_INFO "[RISC-V Initcall] pure_initcall executed\n");
    return 0;
}
pure_initcall(pure_test_init);

static int __init core_test_init(void)
{
    printk(KERN_INFO "[RISC-V Initcall] core_initcall executed\n");
    return 0;
}
core_initcall(core_test_init);

static int __init postcore_test_init(void)
{
    printk(KERN_INFO "[RISC-V Initcall] postcore_initcall executed\n");
    return 0;
}
postcore_initcall(postcore_test_init);

static int __init arch_test_init(void)
{
    printk(KERN_INFO "[RISC-V Initcall] arch_initcall executed\n");
    return 0;
}
arch_initcall(arch_test_init);

static int __init subsys_test_init(void)
{
    printk(KERN_INFO "[RISC-V Initcall] subsys_initcall executed\n");
    return 0;
}
subsys_initcall(subsys_test_init);

static int __init fs_test_init(void)
{
    printk(KERN_INFO "[RISC-V Initcall] fs_initcall executed\n");
    return 0;
}
fs_initcall(fs_test_init);

static int __init rootfs_test_init(void)
{
    printk(KERN_INFO "[RISC-V Initcall] rootfs_initcall executed\n");
    return 0;
}
rootfs_initcall(rootfs_test_init);

static int __init device_test_init(void)
{
    printk(KERN_INFO "[RISC-V Initcall] device_initcall executed\n");
    return 0;
}
device_initcall(device_test_init);

static int __init late_test_init(void)
{
    printk(KERN_INFO "[RISC-V Initcall] late_initcall executed\n");
    return 0;
}
late_initcall(late_test_init);

static int __init console_test_init(void)
{
    printk(KERN_INFO "[RISC-V Initcall] console_initcall executed\n");
    return 0;
}
console_initcall(console_test_init);

同时在 arch/riscv/kernel/Makefile 中新增该目标文件:

--- a/arch/riscv/kernel/Makefile
+++ b/arch/riscv/kernel/Makefile
@@ -29,6 +29,7 @@ obj-y += riscv_ksyms.o
 obj-y  += stacktrace.o
 obj-y  += cacheinfo.o
 obj-y  += patch.o
+obj-y  += initcall_test.o
 obj-$(CONFIG_MMU) += vdso.o vdso/

修改完成后编译内核,并使用 QEMU 模拟器进行启动,内核启动的打印信息如下:

...
[    0.001681] [⭐RISC-V Initcall] console_initcall executed
[    0.003134] Console: colour dummy device 80x25
[    0.010536] printk: console [tty0] enabled
[    0.013249] Calibrating delay loop (skipped), value calculated using timer frequency.. 20.00 BogoMIPS (lpj=40000)
[    0.013440] pid_max: default: 32768 minimum: 301
[    0.014683] Mount-cache hash table entries: 16384 (order: 5, 131072 bytes, linear)
[    0.014842] Mountpoint-cache hash table entries: 16384 (order: 5, 131072 bytes, linear)
[    0.037256] [⭐RISC-V Initcall] early_initcall executed
[    0.037908] rcu: Hierarchical SRCU implementation.
[    0.039356] EFI services will not be available.
[    0.041471] smp: Bringing up secondary CPUs ...
[    0.049906] smp: Brought up 1 node, 4 CPUs
[    0.061418] devtmpfs: initialized
[    0.066783] [⭐RISC-V Initcall] pure_initcall executed
[    0.068512] [⭐RISC-V Initcall] core_initcall executed
[    0.070325] clocksource: jiffies: mask: 0xffffffff max_cycles: 0xffffffff, max_idle_ns: 7645041785100000 ns
[    0.070544] futex hash table entries: 1024 (order: 4, 65536 bytes, linear)
[    0.074988] NET: Registered protocol family 16
[    0.076708] [⭐RISC-V Initcall] postcore_initcall executed
[    0.079466] [⭐RISC-V Initcall] arch_initcall executed
[    0.088406] [⭐RISC-V Initcall] subsys_initcall executed
[    0.128276] vgaarb: loaded
[    0.129447] SCSI subsystem initialized
[    0.131491] usbcore: registered new interface driver usbfs
[    0.132061] usbcore: registered new interface driver hub
[    0.132356] usbcore: registered new device driver usb
[    0.139530] [⭐RISC-V Initcall] fs_initcall executed
[    0.140768] clocksource: Switched to clocksource riscv_clocksource
[    0.171715] NET: Registered protocol family 2
...
[    0.190188] PCI: CLS 0 bytes, default 64
[    0.194343] [⭐RISC-V Initcall] rootfs_initcall executed
[    0.194487] [⭐RISC-V Initcall] device_initcall executed
[    0.196698] workingset: timestamp_bits=62 max_order=21 bucket_order=0
[    0.208411] NFS: Registering the id_resolver key type
...
[    0.561583] 9pnet: Installing 9P2000 support
[    0.562203] Key type dns_resolver registered
[    0.562514] [⭐RISC-V Initcall] late_initcall executed
[    0.563478] debug_vm_pgtable: [debug_vm_pgtable         ]: Validating architecture page table helpers
...

有关 Linux 内核如何实现这样的初始化机制,可以参考这篇文章:Linux 各种 initcall 的调用原理-CSDN博客

初始化级别的选择

选择 initcall 级别的核心原则是:你的初始化函数所依赖的子系统必须已经初始化完成

对于个人当前可能涉及到的内核开发工作来说,device_initcall 最常用,适用于大多数设备驱动程序的初始化,此时核心子系统、总线都已准备就绪。其次是 arch_initcall,进行处理器架构相关的初始化。如果初始化时机并不那么重要,又为了保险起见,可以选择 late_initcall,此时模块所依赖的子系统已经完全初始化完成。至于其他的一些初始化接口,可能一般不太会用到,等需要的时候再来研究吧。