Linux内核驱动初始化流程认识(关于late_initcall和modul_init驱动初始化宏差异)

 工作时候碰到需要修改一下自己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 能确保:

  1. GPIO 控制器驱动已经加载并完成 probe:gpio_keys 才能通过 gpio_request 获取所需的 GPIO 引脚。如果 GPIO 控制器的 module_init 调用晚于 gpio_keys 的初始化,引脚申请就会失败。
  2. 设备树中的平台设备已被注册:很多基于设备树的 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 等级,可以直接在内核源码中搜索该驱动,并查看其初始化函数末尾使用的宏。同时,结合内核启动日志,可以观察到不同级别驱动的加载顺序。

相关推荐
zzzzzz3102 小时前
9K Star 炸裂开源!这个 C 语言写的代码知识图谱,把 Linux 内核索引压缩到了 3 分钟
linux·服务器·sql
XIAOHEZIcode2 小时前
Linux系统鼠标偏移常见原因以及修复方案
linux·运维·游戏
A小辣椒2 天前
TShark:Wireshark CLI 功能
linux
A小辣椒2 天前
TShark:基础知识
linux
AlfredZhao2 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao3 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户9718356334663 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪3 天前
linux 拷贝文件或目录到指定的位置
linux
摇滚侠4 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
bush44 天前
嵌入式linux学习记录十四、术语
linux·嵌入式