菜单

kaoqi
kaoqi
发布于 2026-09-16 / 9 阅读
0
0

kernel的initcall机制与ACPI--DRIVER过程

首先需要明确一下kernel的ACPI解析发生在哪里,以及驱动如何获取ACPI命名空间中的变量。

首先是需要介绍一下kernel编译过程中的initcall机制。这个机制大意是说,在kernel的C语言文件中,通过宏展开的方式可以将一些函数带上特定的标记。然后,在kernel编译完进行链接的阶段,可以使用lds链接脚本,将不同initcall注册函数整理为一个于链接器中约定好的函数指针数组,并在kernel的init阶段中以约定的数组名称进行调用。

比如,首先的ACPI的初始化,就是在/drivers/acpi/bus.c文件中使用的subsys_initcall函数:

又比如,acpi的init结束之后,会在串口char设备的初始化时,执行ttyprintk_init用以加载printk函数:

这引出了initcall机制的相关问题。在各个地方星罗棋布的initcall注册的函数指针,将最终变成一个在启动过程中逐步实现的函数点火机。

对于kernel来说,这个initcall在do_initcalls这个很靠后,但在启动前的函数进行展开并执行(在/init/main.c这个kernel的启动主函数中):

可以从此处看见,initcall本身分为多个层级,并依靠每个层级逐步执行:

这里就可以明显看出initcall的轮询执行顺序是:

1、pure

2、core

3、postcore

4、arch

5、subsys

6、fs

7、device

8、late

注意,这里的__initcall0_start变量,是依照extern进行的一种展开:

相应的,可以在/arch/arm64/kernel/vmlinux.lds.S转/include/asm-generic/vmlinux.lds.h文件中,找到对应的链接描述:

在具体的代码中,宏展开依照/include/linux/init.h文件进行宏展开:

这样,就只需要在各个模块中单独进行各类型initcall的调用,就可以在各个模块中“撒豆成兵”式的执行各个模块的初始化函数了。

这里有必要提及的一点是,设备的最终上线往往不止取决于驱动的注册,也有赖于总线感知到设备后进行的probe等的初始化操作,不是说我驱动完成init之后事情就结束了,还需要搭配总线机制的动态监控才行。

这种initcall函数,更多的是完成了驱动的注册,而非最终的设备驱动落地。

ACPI中的initcall

首先是,在ACPI(这是一个被当作驱动用的代码模块)中查找全部的initcall位置(按顺序排序):

1、/acpi_adxl.c中的subsys_initcall(adxl_init);

adxl的意思是ACPI地址的转换,向ACPI子系统注册驱动程序。

用于注册一个物理内存地址向具体硬件地址的转换的工具。

2、/bus.c中的subsys_initcall(acpi_init);

这是ACPI总线子系统核心初始化入口。ACPI核心的初始化(解析表、命名空间建立)由更早的acpi_earlt_init()完成(core_initcall阶段)。acpi_init()函数的作用是在ACPI核心基础设施就绪后,将ACPI设备模型集成到Linux通用的设别驱动模型中,并启动ACPI事件处理机制。

3、/event.c中的fs_initcall(acpi_event_init);

‌在内核启动的文件系统初始化阶段,建立 ACPI 事件到用户空间的通信桥梁。它创建 /proc/acpi/event 接口并初始化内部事件队列,使得用户空间守护进程(如 acpid)能够监听和处理来自 BIOS/UEFI 的电源管理、热插拔等硬件事件。。

4、/osl.c中的fs_initcall_sync(acpi_reserve_resources);

这里的sync代表异步的意思,本质跟别的initcall一样,都是位于fs的启动宏。

‌根据 ACPI FADT 表提供的信息,向内核资源管理器注册并锁定 ACPI 电源管理寄存器(如事件块、控制块、定时器块)所占用的 I/O 端口或内存区域,确保这些关键资源不被其他驱动程序非法占用,从而保障 ACPI 电源管理功能的正常运行。

5、/arm64/gtdt.c中的device_initcall(gtdt_sbsa_gwdt_init);

gtdt_sbsa_gwdt_init 的作用是充当‌ACPI 固件与 Linux 平台设备模型之间的桥梁‌。它专门负责从 ACPI GTDT 表中提取 SBSA 看门狗定时器的硬件资源,并将其封装为标准 Linux platform_device 注册到系统中,从而使通用的 sbsa_gwdt 驱动程序能够接管并控制该硬件看门狗。

6、/numa/hmat.c中的device_initcall(hmat_init);

‌在系统启动的设备初始化阶段,解析 ACPI HMAT 表,提取系统中不同内存域之间的延迟和带宽属性,构建内核内部的异构内存拓扑模型。这为内核的 NUMA 调度、内存分配优化以及 CXL/持久内存管理提供了关键的性能感知数据支持。

7、/apei/erst.c中的device_initcall(erst_init);

‌在系统启动的设备初始化阶段,解析 ACPI ERST 表,建立与非易失性错误存储介质的通信通道,并向内核 APEI 框架注册错误记录读写接口。这使得系统在发生严重硬件故障时,能够将详细的错误日志持久化保存,供重启后进行分析和问题定位。

8、/bgrt.c中的device_initcall(bgrt_init);

在系统启动的设备初始化阶段,解析 ACPI BGRT 表,提取固件中嵌入的启动 Logo(BMP 图像),并将其集成到 Linux 的图形子系统(Framebuffer 或 DRM)中。这使得 Linux 能够在启动过程中显示制造商定制的开机画面,提供更平滑的用户体验。

9、/apei/bert.c中的late_initcall(bert_init);

‌在系统启动的最后阶段,解析 ACPI BERT 表,读取固件保留的非易失性存储区域中的历史硬件错误记录,并将其上报给内核日志和错误处理子系统。这使得系统能够在重启后诊断并报告导致前一次启动失败或异常的严重硬件故障。

device中的initcall

由于各个驱动各自有各自的难处,所以这里以char这个设备举例。

1、/ipmi/ipmi_dmi.c中的subsys_initcall(scan_for_dmi_ipmi);

在系统启动的子系统初始化阶段,扫描 DMI/SMBIOS 表以发现主板集成的 IPMI 控制器,解析其通信接口类型和资源地址,并将其注册为 Linux 平台设备。这使得后续的 IPMI 驱动程序能够绑定并初始化硬件,从而为用户提供服务器硬件监控和管理能力。

2、/misc.c中的subsys_initcall(misc_init);

建立 Linux 内核中的杂项字符设备管理框架。它注册了主设备号为 10 的通用字符驱动,创建了 sysfs 类以支持自动设备节点生成,并提供 proc 接口用于查看注册状态。这使得后续的各种小型、非标准字符设备驱动可以简单地通过 misc_register() 接入系统,而无需各自申请独立的主设备号。

3、/tpm/tpm-interface.c中的subsys_initcall(tpm_init);

‌建立 Linux 内核中的 TPM 子系统框架。它创建 sysfs 设备类(tpm/tpmrm),初始化资源管理器基础设施,并为后续的具体 TPM 硬件驱动提供注册接口。这使得系统能够统一管理不同类型的 TPM 硬件,并向用户空间提供标准的设备节点(/dev/tpmX)和安全共享机制。

4、/mem.c中的fs_initcall(chr_dev_init);

在系统启动的文件系统初始化阶段,注册主设备号为 1 的核心字符设备驱动,并创建 /dev/mem、/dev/null、/dev/zero 等基础系统设备节点。这些设备提供了对物理内存的直接访问能力以及标准化的数据源/汇接口,是 Linux 系统底层调试、资源管理和用户空间程序正常运行的基石。

5、/hpet.c中的device_initcall(hpet_init);

在系统启动的设备初始化阶段,建立 HPET 子系统的用户空间接口框架。它注册了 HPET 字符设备驱动和 sysfs 类,为后续探测到的 HPET 硬件实例提供标准的 /dev/hpet 访问接口。这使得用户空间应用程序能够利用 HPET 硬件的高精度定时功能,而无需直接操作硬件寄存器。

6、/ttyprintk.c中的device_initcall(ttyprintk_init);

‌在系统启动的设备初始化阶段,注册 ttyprintk TTY 驱动,创建 /dev/ttyprintk 设备节点。该设备提供了一种植入式的日志记录机制,允许用户空间程序通过将数据写入该设备,直接将消息发送到内核日志系统中,便于统一管理和调试。


评论