工作时候碰到需要修改一下自己linux板子上面的io的驱动代码,一般来说没什么人会从头到尾去写一个有框架结构的内核驱动,都是找Linux内核里面的相类似的文件作为借鉴然后加上自己板子的具体硬件情况然后修改。在写的时候我发现了一个初始化的宏很有意思:late_init_call()。像是以前自己学习linux驱动的时候,大部分自己写的模块驱动都是module_init(),这两个都是驱动的申明初始化有什么差异?以下是通过自己看书还有找AI总结的结论:
late_initcall 和 module_init 都是 Linux 内核中用于标记模块初始化入口的宏,它们的核心区别在于初始化时机的早晚。
简单来说,内核初始化过程像一个分阶段的启动仪式:module_init 在设备驱动加载的"常规时间"被调用;而 late_initcall 则安排在"尾声阶段",此时绝大多数核心功能和驱动都已就绪。
核心差异对比
| 特性 | module_init | late_initcall |
|---|---|---|
| 执行优先级 | 高 (较早) :在大部分子系统(如PCI、USB核心)初始化完成后就执行。 | 低 (较晚) :在 module_init 级别的所有驱动都加载完成之后才执行。 |
| 适用场景 | 普通设备驱动 :如网卡、硬盘、I2C/SPI控制器等,它们需要尽早为系统提供基础功能。 | 依赖特定设备的驱动 :需要等待常规驱动加载、设备发现甚至固件加载完成后才能工作。 |
| 典型例子 | drivers/net/ethernet/ 下的网卡驱动, drivers/ata/ 下的SATA驱动。 | 输入设备 (如 gpio_keys )、 电源管理 、 需要设备树的平台设备 等。 |
为什么 gpio_keys 要用 late_initcall?
drivers/input/keyboard/gpio_keys.c 使用 late_initcall,是因为它作为输入设备,有一个关键依赖:它所对应的 GPIO 引脚,很可能在 module_init 阶段还没有被正确配置。
下图展示了 gpio_keys 可能依赖的流程:
flowchart TD A[module_init 阶段
GPIO 控制器驱动加载] --> B[GPIO 控制器驱动 probe
分配 gpio_chip 结构] B --> C[GPIO 子系统可用
gpio_request 能成功] C --> D[late_initcall 阶段
gpio_keys 驱动初始化] D --> E[gpio_keys 调用
gpio_request/ gpiod_get] E --> F[成功申请 GPIO 并注册
input 设备]
late_initcall 能确保:
- GPIO 控制器驱动已经加载并完成 probe:gpio_keys 才能通过 gpio_request 获取所需的 GPIO 引脚。如果 GPIO 控制器的 module_init 调用晚于 gpio_keys 的初始化,引脚申请就会失败。
- 设备树中的平台设备已被注册:很多基于设备树的 GPIO 按键,是由系统在 module_init 阶段扫描设备树并创建的平台设备。使用 late_initcall 可以确保这些设备已经存在。
因此,late_initcall 在这里扮演了一个"让出机会"的角色,让那些可能更慢初始化的基础服务(如GPIO、I2C、SPI控制器)先行一步。
内核初始化顺序全景
为了让你更清晰地理解 late_initcall 在整个内核初始化流程中的位置,可以将整个启动过程想象成一张"调用表"。这张表展示了不同类型的 initcall 的执行顺序:
| 优先级 (从高到低) | 宏名称 | 简单理解 |
|---|---|---|
| 最早 | pure_initcall | 最纯粹的初始化,通常在架构相关的最早阶段。 |
| core_initcall | 核心子系统初始化,如调度器、内存管理等。 | |
| postcore_initcall | 核心子系统之后的初始化。 | |
| arch_initcall | 架构相关的初始化。 | |
| subsys_initcall | 各子系统(如PCI、USB核心)的初始化。 | |
| fs_initcall | 文件系统相关的初始化。 | |
| device_initcall | 这就是 module_init 的别名 ,绝大多数设备驱动在此阶段加载。 | |
| late_initcall | 较晚的初始化,依赖上述所有阶段的驱动。 | |
| 最晚 | *_initcall_sync | 在上述每个阶段后,立即同步执行剩下的同类 initcall。 |
可以看到,late_initcall 在执行顺序上确实非常靠后,它保证了依赖的完备性,但也牺牲了启动的及时性。
选择参考
在实际开发中,如何选择?这里有一个简单的判断逻辑:
-
默认情况:几乎所有的驱动都应该使用 module_init。它是内核驱动的标准入口点。
-
特殊情况:只有当你明确发现,你的驱动因依赖另一个 module_init 级别的驱动而加载失败时,可以考虑降级到 late_initcall。反之,如果你的驱动是其他设备的基础,甚至可以提升到 subsys_initcall 或 arch_initcall。
另外,这些机制通常仅对编译进内核(y)的驱动有效。对于编译成模块(m)的驱动,无论是 module_init 还是 late_initcall,都会在用户空间 insmod 的那一刻执行,其时序主要取决于加载命令的执行时机。
如果想了解某个特定驱动为何选择某个 initcall 等级,可以直接在内核源码中搜索该驱动,并查看其初始化函数末尾使用的宏。同时,结合内核启动日志,可以观察到不同级别驱动的加载顺序。