本文将介绍 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,代码如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80
| #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 中新增该目标文件:
1 2 3 4 5 6 7 8
|
@@ -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 模拟器进行启动,内核启动的打印信息如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42
| ... [ 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,此时模块所依赖的子系统已经完全初始化完成。至于其他的一些初始化接口,可能一般不太会用到,等需要的时候再来研究吧。