Linux内核initcall机制深度解析

1. initcall机制概述

1.1 基本概念

在Linux内核开发中,驱动和子系统的初始化函数需要通过特定的机制注册到内核启动流程中。最常见的注册方式是使用 module_init 宏,但内核中还存在多种类似的声明方式,如 device_initcall() 、 arch_initcall() 等,这些本质上都是对 initcall 机制的不同应用。

initcall 是Linux内核用于管理初始化函数调用顺序的核心机制,它允许开发者将初始化函数按照优先级分组,确保内核启动时各模块按正确的依赖顺序初始化。

1.2 典型使用场景

以下是两种典型的 initcall 使用方式:

static int __init qcom_iommu_init(void)
{
    int ret;
    ret = platform_driver_register(&qcom_iommu_ctx_driver);
    if (ret)
        return ret;
    ret = platform_driver_register(&qcom_iommu_driver);
    if (ret)
        platform_driver_unregister(&qcom_iommu_ctx_driver);
    return ret;
}
device_initcall(qcom_iommu_init);

static int __init ebsa110_init(void)
{
    arm_pm_idle = ebsa110_idle;
    return platform_add_devices(ebsa110_devices, ARRAY_SIZE(ebsa110_devices));
}
arch_initcall(ebsa110_init);

2. initcall等级划分

2.1 等级定义

Linux内核将 initcall 划分为8个等级(0-7),等级数值越小优先级越高。这些等级定义在 include/linux/init.h 文件中:

#define pure_initcall(fn)       __define_initcall(fn, 0)
#define core_initcall(fn)       __define_initcall(fn, 1)
#define core_initcall_sync(fn)  __define_initcall(fn, 1s)
#define postcore_initcall(fn)   __define_initcall(fn, 2)
#define postcore_initcall_sync(fn) __define_initcall(fn, 2s)
#define arch_initcall(fn)       __define_initcall(fn, 3)
#define arch_initcall_sync(fn)  __define_initcall(fn, 3s)
#define subsys_initcall(fn)     __define_initcall(fn, 4)
#define subsys_initcall_sync(fn) __define_initcall(fn, 4s)
#define fs_initcall(fn)         __define_initcall(fn, 5)
#define fs_initcall_sync(fn)    __define_initcall(fn, 5s)
#define rootfs_initcall(fn)     __define_initcall(fn, rootfs)
#define device_initcall(fn)     __define_initcall(fn, 6)
#define device_initcall_sync(fn) __define_initcall(fn, 6s)
#define late_initcall(fn)       __define_initcall(fn, 7)
#define late_initcall_sync(fn)  __define_initcall(fn, 7s)

2.2 各等级用途

  1. pure_initcall (0) : 纯初始化调用,不依赖任何其他子系统
  2. core_initcall (1) : 核心子系统初始化
  3. postcore_initcall (2) : 核心子系统后的初始化
  4. arch_initcall (3) : 架构相关初始化
  5. subsys_initcall (4) : 子系统初始化
  6. fs_initcall (5) : 文件系统初始化
  7. device_initcall (6) : 设备驱动初始化
  8. late_initcall (7) : 后期初始化

这种分级机制确保了内核启动时,基础架构先初始化,然后是核心子系统,最后才是具体的设备驱动。

3. initcall实现机制

3.1 底层宏定义

所有 initcall 宏最终都通过 __define_initcall 宏实现:

#define __define_initcall(fn, id) \
    static initcall_t __initcall_##fn##id __used \
    __attribute__((__section__(".initcall" #id ".init"))) = fn; \
    LTO_REFERENCE_INITCALL(__initcall_##fn##id)

这个宏的关键作用是将初始化函数指针放置到特定的ELF段中。每个等级对应一个特定的段名,例如:

  • .initcall0.init 对应 pure_initcall
  • .initcall6.init 对应 device_initcall

3.2 链接脚本处理

内核链接脚本(vmlinux.lds.S)会将这些段按顺序排列,确保在二进制镜像中,高优先级的初始化函数位于低优先级函数之前。

4. initcall执行流程

4.1 调用入口

initcall 的执行始于内核启动流程中的 start_kernel() 函数,调用链如下:

start_kernel()
    -> kernel_init()
        -> kernel_init_freeable()
            -> do_basic_setup()
                -> do_initcalls()

4.2 do_initcalls()实现

do_initcalls() 是执行所有初始化函数的核心函数:

static void __init do_initcalls(void)
{
    int level;
    size_t len = strlen(saved_command_line) + 1;
    char *command_line;
    
    command_line = kzalloc(len, GFP_KERNEL);
    if (!command_line)
        panic("%s: Failed to allocate %zu bytes\n", __func__, len);
    
    for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) {
        strcpy(command_line, saved_command_line);
        do_initcall_level(level, command_line);
    }
    
    kfree(command_line);
}

4.3 按等级执行

do_initcall_level() 负责执行特定等级的所有初始化函数:

static void __init do_initcall_level(int level, char *command_line)
{
    initcall_entry_t *fn;
    
    parse_args(initcall_level_names[level],
              command_line, __start___param,
              __stop___param - __start___param,
              level, level,
              NULL, ignore_unknown_bootoption);
    
    trace_initcall_level(initcall_level_names[level]);
    
    for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
        do_one_initcall(initcall_from_entry(fn));
}

5. module_init与initcall的关系

5.1 module_init实现

module_init 宏实际上是 device_initcall 的别名:

#define device_initcall(fn)     __define_initcall(fn, 6)
#define __initcall(fn)          device_initcall(fn)
#define module_init(x)          __initcall(x);

这意味着使用 module_init 声明的驱动初始化函数会被归类到等级6,即设备驱动初始化阶段。

5.2 直接使用initcall的优势

在某些情况下,开发者会直接使用特定等级的 initcall 宏而非 module_init ,主要原因包括:

  1. 需要确保初始化顺序早于或晚于常规设备驱动
  2. 初始化函数属于内核核心部分而非可加载模块
  3. 需要明确表达初始化函数的依赖关系

6. 工程实践建议

6.1 选择适当的initcall等级

  • 架构相关代码使用 arch_initcall (3)
  • 核心子系统使用 core_initcall (1)或 subsys_initcall (4)
  • 普通设备驱动使用 device_initcall (6)或 module_init
  • 最后阶段的初始化使用 late_initcall (7)

6.2 同步initcall

对于需要确保在某个阶段完成所有初始化的场景,可以使用 *_sync 版本的宏,如 core_initcall_sync 。这些宏定义的初始化函数会在该等级所有普通初始化函数完成后执行。

6.3 调试技巧

内核提供了多种调试 initcall 的机制:

  1. 通过 initcall_debug 内核参数打印所有initcall的执行信息
  2. 使用 ftrace 跟踪initcall执行时间和顺序
  3. 通过 /sys/kernel/debug/tracing/events/initcall 查看initcall事件

7. 性能考量

7.1 启动时间优化

过多的initcall会延长内核启动时间。优化策略包括:

  1. 将非关键初始化延迟到 late_initcall 或用户空间
  2. 使用异步initcall机制
  3. 合并多个相关初始化函数

7.2 内存占用

所有标记为 __init 的函数和数据在内核初始化完成后会被释放,因此合理使用 __init 宏可以降低运行时的内存占用。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐