Linux内核initcall机制:驱动初始化的幕后英雄

开发 前端
当系统启动的号角吹响,众多硬件驱动就像等待检阅的士兵,急切地需要按恰当顺序、在精准时机完成初始化,才能确保整个系统顺利启航。要是没有 initcall 机制,这乱糟糟的局面简直不敢想象,可能系统还没 “起跑” 就陷入崩溃泥潭。

你想想,咱们每天使用的电脑、手机等各类智能设备,之所以能流畅运行形形色色的功能,背后离不开 Linux 内核驱动着海量的硬件。而在这繁杂的硬件驱动初始化过程中,initcall 机制宛如一位运筹帷幄的指挥官,不动声色地将一切安排得井井有条。

当系统启动的号角吹响,众多硬件驱动就像等待检阅的士兵,急切地需要按恰当顺序、在精准时机完成初始化,才能确保整个系统顺利启航。要是没有 initcall 机制,这乱糟糟的局面简直不敢想象,可能系统还没 “起跑” 就陷入崩溃泥潭。现在,就跟着我一同揭开 initcall 机制那神秘的面纱,看看它究竟是如何施展魔力,让 Linux 内核世界有条不紊运转的吧!

一、Linux驱动初始化的困境

写过 Linux 驱动的朋友,想必对 module_init 宏都不陌生,它可是驱动初始化的关键入口。在 Linux 系统里,驱动程序的加载方式有静态编译进内核和动态加载两种。要是采用静态编译,开发者通常得提供诸如 xxx_init() 这样的函数接口,来启动驱动并提供相关服务。按照常理,这个 xxx_init() 函数必须在系统启动的某个节点被调用,驱动才能正常运作。

最容易想到的办法,就是开发者手动在内核启动 init 程序的某个地方,添加对自己驱动程序 xxx_init() 函数的调用。就像下面这样:

void init(void) {
    a_init();
    b_init();
    //...
    z_init();
}

不过,这种做法要是放在单人开发的小系统里,或许还能应付得来。但 Linux 系统如此庞大复杂,驱动数量众多,要是每添加一个驱动,都得去改动 kernel_init() 代码,那简直就是一场 “灾难”。一方面,这极易引入人为错误,稍有不慎就可能导致系统启动故障;另一方面,代码的可维护性会变得极差,后续排查问题、升级驱动都会让人头疼不已。

既然直接手动添加不靠谱,那换种思路,集中提供一个地方来管理驱动初始化程序怎么样?比如,开发者把自己的初始化函数添加到这个统一的地方,内核启动时,就去扫描并执行所有添加进来的驱动程序。像下面这样简单用 C 文件做个列表:

#include <stdio.h>
void a_init(void) {
    printf("%s\n", __func__);
}
void b_init(void) {
    printf("%s\n", __func__);
}
void (*fun_list[])(void) = {a_init, b_init};
void init(void) {
    int i;
    void(*pfun)(void);
    for (i = 0; i < sizeof(fun_list) / sizeof(fun_list[0]); ++i) {
        printf("%d\n", i);
        fun_list[i]();
    }
}

但这个方法也并非尽善尽美,它需要开发者手动维护这个列表,一旦驱动数量增多或者有更新、删除操作,管理成本就会直线上升,还容易出现遗漏、重复添加等问题。那么,Linux 内核究竟是如何巧妙化解这个难题的呢?

二、Initcall机制登场

2.1核心概念

Linux 内核里,为了解决驱动初始化的难题,引入了 initcall 机制。简单来说,initcall 机制就是一套规范化、自动化的驱动初始化函数管理方案。它在内核编译阶段 “大显身手”,通过一系列精心设计的宏定义,巧妙地将不同驱动的初始化函数按照预设的优先级顺序,依次存放到特定的内存段中。当内核启动时,就如同一位训练有素的指挥官,有条不紊地遍历这些内存段,精准地调用各个初始化函数,确保每个驱动都能在恰当的时机完成初始化,顺利 “上岗”,为系统的稳定运行保驾护航。这一机制不仅让驱动初始化变得井井有条,还极大地减轻了开发者的负担,提升了内核的可维护性,可谓是 Linux 内核中的一大 “得力助手”。

2.2源码剖析

深入到 Linux 内核源码中,initcall 机制的实现可谓精妙绝伦。在 include/linux/init.h 文件里,藏着一系列让人眼花缭乱却又逻辑严密的宏定义,它们是 initcall 机制的 “幕后操控者”。

对于静态加载的驱动,内核定义了诸如 early_initcall、pure_initcall、core_initcall 等众多宏。就拿 core_initcall 来说,它背后其实是 __define_initcall 宏在发挥关键作用。展开来看,__define_initcall(fn, 1)(这里以 core_initcall 的参数 1 为例),经过层层解析,就像是一场奇妙的 “魔术表演”:先是定义了一个静态的函数指针 initcall_t __initcall_##fn##1,这里的 ## 是连接符号的 “胶水”,把函数名 fn 和等级标识 1 紧紧粘在一起,变成一个独一无二的函数指针名称。而 __attribute__((__section__(".initcall1.init"))) 则像是一个精准的 “导航仪”,告诉编译器把这个函数指针变量放到名为 .initcall1.init 的特定代码段中,这个代码段就像是一个为初始化函数精心准备的 “候车室”,等待内核启动时的 “召唤”。并且,__used 这个属性也很关键,它像是给函数指针穿上了一层 “保护衣”,防止编译器在优化过程中,把这个看似暂时没被用到的符号给无情 “抛弃”,确保了机制的完整性。

再看动态加载的情况,以常用的 module_init 宏为例,当我们在驱动代码里写下 module_init(xxx_init) 时,这背后的故事同样精彩。module_init 宏在 include/linux/module.h 中被定义为 __initcall(x),而进一步追溯,它其实就是 device_initcall(x),最终也会导向 __define_initcall(x, 6)。这意味着,通过 module_init 修饰的驱动初始化函数,会被安排到优先级为 6 的 .initcall6.init 这个 “候车室” 里,等待内核按部就班地来 “检票上车”,完成初始化流程。

在内核启动流程的 init/main.c 文件中,有一个至关重要的函数 do_initcalls,它就是那位掌控全局的 “指挥官”。当内核启动进入这个环节,do_initcalls 函数开始施展它的 “魔法”。它会依据预先设定好的优先级顺序,如同一位严谨的列车调度员,依次 “调度” 各个等级的初始化函数。从早期初始化的 early_initcall 开始,逐步到后续各级别的 initcall,逐个检查每个优先级对应的代码段,一旦发现有初始化函数指针 “候车”,就立即调用执行,确保驱动们有序地完成初始化,为系统正常运行搭建好坚实的基础。

2.3实现原理

总体来说,initcall是基于以下思路设计出来的:

  • 在生成vmlinux的链接阶段为initcall创建特定的section
  • 开发者创建相关的initcall函数,并使用xxx_initcall声明为不同类型
  • 每一类initcall对应一组section
  • 遍历执行initcall section中的initcalls

xxx_initcall的定义位于include/linux/init.h中,从这个文件的名字也可以看出xxx_initcall是针对初始化操作的。

#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)

代码解读

从上面的宏定义可以发现,所有的xxx_initcall都是基于__define_initcall的,后者的定义位于同一个文件中,通过__define_initcall将各个xxx_initcall统一到一起,基于ID编号链接到不同的subsection,在同一个subsection中各个initcall的排序以链接的顺序为准。另外,__define_initcall中的ID编号还有另外一个作用,就是防止不同类型的xxx_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)

以rockchip_grf_init()为例拆解分析xxx_initcall的实现细节,如下图所示,注意,在倒数第二个框图内可以看出来initcall机制使用到了GNU编译工具链的属性。

图片图片

执行流程,根据前面的介绍,当xxx_initcall被链接到目标文件后,会生成不同类别的section,包含不同的initcall函数,如下所示:

.initcallearly.init    0000000000000008 __initcall_trace_init_flags_sys_exitearly
.initcall0.init        0000000000000008 __initcall_ipc_ns_init0
.initcall1.init        0000000000000008 __initcall_map_entry_trampoline1
.initcall2.init        0000000000000008 __initcall_bdi_class_init2
.initcall3.init        0000000000000008 __initcall_dma_bus_init3
.initcall4.init        0000000000000008 __initcall_fbmem_init4
.initcall5.init        0000000000000008 __initcall_chr_dev_init5
.initcall6.init        0000000000000008 __initcall_hwrng_modinit6
.initcall7.init        0000000000000008 __initcall_deferred_probe_initcall7
.initcallrootfs.init   0000000000000008 __initcall_populate_rootfsrootfs

同一类的initcall执行顺序由编译顺序决定,不同类的initcall执行顺序在init/main.c中定义,如下所示:

static initcall_t *initcall_levels[] __initdata = {
	__initcall0_start,
	__initcall1_start,
	__initcall2_start,
	__initcall3_start,
	__initcall4_start,
	__initcall5_start,
	__initcall6_start,
	__initcall7_start,
	__initcall_end,
};

在实际执行时,内核必须知道xxx_initcall section所在的位置,而在include/asm-generic/vmlinux.lds.h中将xxx_start和.initcall*.init链接到了一起,这样的话,do_initcalls()遍历不同ID的initcall时,基于xxx_start便可以找到想对应的.initcall entry,然后循环遍历里面的各个initcalls。

#define INIT_CALLS_LEVEL(level)                                         \
                VMLINUX_SYMBOL(__initcall##level##_start) = .;          \
                *(.initcall##level##.init)                              \
                *(.initcall##level##s.init)                             \

#define INIT_CALLS                                                      \
                VMLINUX_SYMBOL(__initcall_start) = .;                   \
                *(.initcallearly.init)                                  \
                INIT_CALLS_LEVEL(0)                                     \
                INIT_CALLS_LEVEL(1)                                     \
                INIT_CALLS_LEVEL(2)                                     \
                INIT_CALLS_LEVEL(3)                                     \
                INIT_CALLS_LEVEL(4)                                     \
                INIT_CALLS_LEVEL(5)                                     \
                INIT_CALLS_LEVEL(rootfs)                                \
                INIT_CALLS_LEVEL(6)                                     \
                INIT_CALLS_LEVEL(7)                                     \
                VMLINUX_SYMBOL(__initcall_end) = .;

在arch/arm64/kernel/vmlinux.lds中可以看到initcall的符号排布如下图所示,基于*_start可以定位到各个initcall函数所对应的符号。

图片图片

基于以上分析,整理出initcalls的完整执行流程如下:

图片图片

三、Initcall优先级的奥秘

3.1分级规则

initcall 机制里,函数的优先级可是 “暗藏玄机”。它一共划分为 8 个等级,从 0 到 7,数字越小,优先级越高,执行顺序也就越早。像 pure_initcall 对应的优先级是 0,意味着它会在内核启动的早期,抢在很多初始化任务之前被调用,适用于那些没有复杂依赖、纯粹进行变量初始化的函数,能快速完成一些基础准备工作;而 late_initcall 优先级为 7,属于 “慢性子”,要等到系统大部分关键初始化都完成,快接近稳定运行状态时才登场,通常用来处理一些对启动顺序不太敏感、可以稍后进行的辅助性初始化,避免过早执行影响系统前期关键流程。

其中,还有些特殊标记,比如 arch_initcall 里的 “arch”,表明和硬件架构紧密相关,这类初始化函数在系统启动初期,硬件初始化阶段就会被调用,确保硬件能快速进入可用状态,为后续驱动和软件运行搭好硬件 “舞台”;rootfs_initcall 涉及根文件系统相关初始化,它的优先级介于 5 和 6 之间,在文件系统相关的初始化流程里找准时机切入,保障文件系统布局、挂载等操作有序完成,让系统能顺利读写文件,为各种应用程序和服务提供数据存储 “根基”。

而且,带 “sync” 后缀的,像 core_initcall_sync 相较于 core_initcall,多了同步操作的意味。它会在执行完前一级初始化后,等待一些关键条件达成或资源准备好,才继续后续操作,保证系统状态的一致性,避免因异步执行可能带来的资源竞争、数据不一致等隐患,让初始化流程更加稳健。

3.2实战运用

假设我们现在有三个驱动:i2c_driver、video_driver 和 audio_driver。

i2c_driver 负责管理系统中的 I2C 总线设备,它需要在系统启动早期就完成初始化,以便后续挂载在 I2C 总线上的各类传感器、控制器等设备能及时被识别和配置,那我们就可以使用 arch_initcall(i2c_driver_init),把它的初始化函数优先级设高,确保硬件层面的通信基础尽早搭建好。

video_driver 用于驱动显卡,让显示器能正常输出图像,但它依赖一些内核子系统的基本框架搭建完成,比如内存管理子系统要先准备好显存分配的机制,此时使用 subsys_initcall(video_driver_init) 较为合适,在子系统初始化中期阶段介入,与依赖的子系统协同初始化,保障视频输出功能顺利启用。

audio_driver 相对来说,对启动及时性要求没那么高,只要在系统快要进入用户交互阶段,能正常播放声音即可,所以采用 late_initcall(audio_driver_init),放在较晚的优先级,避免过早初始化占用资源,还可能因其他关键系统组件未就绪而出现异常,确保音频服务在合适的时候 “低调登场”。

当内核启动,执行到 do_initcalls 函数时,就会按照 arch_initcall、subsys_initcall、late_initcall 的优先级顺序,依次检查对应的代码段。先找到存放 i2c_driver_init 函数指针的 .initcall3.init 段(假设 arch_initcall 对应 3,实际依内核版本和架构而定),执行 i2c_driver_init;接着在轮到 subsys_initcall 优先级时,从 .initcall4.init 段调用 video_driver_init;最后在其他大部分初始化都收尾时,从 .initcall7.init 段执行 audio_driver_init,有条不紊地让各个驱动在恰当的时机 “闪亮登场”,开启各自的使命,保障系统从启动到稳定运行的每一步都稳稳当当。

3.3优势尽显

initcall机制给 Linux 系统带来的好处那可真是数不胜数。从开发的便利性来讲,它就像是一位贴心的助手,大大简化了驱动开发者的工作。以往那种繁琐、易错的手动添加驱动初始化函数调用的方式一去不复返。现在,开发者只需轻松使用内核提供的对应宏,比如 module_init、arch_initcall 等,就能把驱动初始化函数妥妥地交给 initcall 机制 “托管”,编译器会自动完成后续复杂的整理、存放工作,开发者无需再为函数调用顺序、存放位置这些琐碎细节烦恼,得以将更多精力聚焦在驱动核心功能的实现上,开发效率直线飙升。

在内存管理方面,initcall 机制更是展现出了 “节约标兵” 的特质。要知道,内核启动时,那些用于初始化的代码在完成使命后,若还一直占用宝贵的内存空间,无疑是一种极大的浪费。而 initcall 机制巧妙地将初始化函数存放在特定的代码段,待系统启动,相关初始化工作顺利结束,内存管理器就能迅速回收这些代码段占用的内存,将其 “变废为宝”,重新分配给系统后续运行中更急需的任务,让内存资源得到高效利用,保障系统整体运行的流畅性。

对于系统稳定性,initcall 机制更是筑起了一道坚固的 “防线”。它精心设计的优先级体系,确保各个驱动、子系统严格按照合理的顺序初始化。这就有效避免了因初始化顺序混乱,可能导致的资源竞争冲突,比如两个驱动同时争抢同一硬件资源,造成系统死机;或者数据依赖关系出错,像某个驱动初始化时需要依赖另一个尚未初始化完成的子系统提供的数据,进而引发系统崩溃等严重问题,让 Linux 系统在启动和运行过程中稳如泰山。

四、Initcall机制在Linux内核中的实现

Linux 内核提供了一组来自头文件 include/linux/init.h 的宏,来标记给定的函数为 initcall。所有这些宏都相当简单:

#define early_initcall(fn)        __define_initcall(fn, early)
#define core_initcall(fn)        __define_initcall(fn, 1)
#define postcore_initcall(fn)        __define_initcall(fn, 2)
#define arch_initcall(fn)        __define_initcall(fn, 3)
#define subsys_initcall(fn)        __define_initcall(fn, 4)
#define fs_initcall(fn)            __define_initcall(fn, 5)
#define device_initcall(fn)        __define_initcall(fn, 6)
#define late_initcall(fn)        __define_initcall(fn, 7)

我们可以看到,这些宏只是从同一个头文件的 __define_initcall 宏的调用扩展而来。此外,__define_initcall 宏有两个参数:

  • fn - 在调用某个级别 initcalls 时调用的回调函数;
  • id - 识别 initcall 的标识符,用来防止两个相同的 initcalls 指向同一个处理函数时出现错误。

__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)

要了解 __define_initcall 宏,首先让我们来看下 initcall_t 类型。这个类型定义在同一个 头文件 中,它表示一个返回 整形指针的函数指针,这将是 initcall 的结果:

typedef int (*initcall_t)(void);

现在让我们回到 _-define_initcall 宏。## 提供了连接两个符号的能力。在我们的例子中,__define_initcall 宏的第一行产生了 .initcall id .init ELF 部分 给定函数的定义,并标记以下 gcc 属性:__initcall_function_name_id 和 __used。如果我们查看表示内核链接脚本数据的 include/asm-generic/vmlinux.lds.h 头文件,我们会看到所有的 initcalls 部分都将放在 .data 段:

#define INIT_CALLS                    \
        VMLINUX_SYMBOL(__initcall_start) = .;    \
        *(.initcallearly.init)                    \
        INIT_CALLS_LEVEL(0)                        \
        INIT_CALLS_LEVEL(1)                        \
        INIT_CALLS_LEVEL(2)                        \
        INIT_CALLS_LEVEL(3)                        \
        INIT_CALLS_LEVEL(4)                        \
        INIT_CALLS_LEVEL(5)                        \
        INIT_CALLS_LEVEL(rootfs)                \
        INIT_CALLS_LEVEL(6)                        \
        INIT_CALLS_LEVEL(7)                        \
        VMLINUX_SYMBOL(__initcall_end) = .;
#define INIT_DATA_SECTION(initsetup_align)    \
    .init.data : AT(ADDR(.init.data) - LOAD_OFFSET) {       \
        ...                                                \
        INIT_CALLS                                           \
        ...                                                \
    }

第二个属性 -__used,定义在include/linux/compiler-gcc.h头文件中,它扩展了以下gcc定义:

#define __used   __attribute__((__used__))

它防止 定义了变量但未使用 的告警。宏 __define_initcall 最后一行是:

LTO_REFERENCE_INITCALL(__initcall_##fn##id)

这取决于 CONFIG_LTO 内核配置选项,只为编译器提供链接时间优化存根:

#ifdef CONFIG_LTO
#define LTO_REFERENCE_INITCALL(x) \
        static __used __exit void *reference_##x(void)  \
        {                                               \
                return &x;                              \
        }
#else
#define LTO_REFERENCE_INITCALL(x)
#endif

为了防止当模块中的变量没有引用时而产生的任何问题,它被移到了程序末尾。这就是关于 __define_initcall 宏的全部了。所以,所有的 *_initcall 宏将会在Linux内核编译时扩展,所有的 initcalls 会放置在它们的段内,并可以通过 .data 段来获取,Linux 内核在初始化过程中就知道在哪儿去找到 initcall 并调用它。

既然 Linux 内核可以调用 initcalls,我们就来看下 Linux 内核是如何做的。这个过程从 init/main.c 头文件的 do_basic_setup 函数开始:

static void __init do_basic_setup(void)
{
    ...
    ...
    ...
       do_initcalls();
    ...
    ...
    ...
}

该函数在 Linux 内核初始化过程中调用,调用时机是主要的初始化步骤,比如内存管理器相关的初始化、CPU子系统等完成之后。do_initcalls函数只是遍历initcall级别数组,并调用每个级别的do_initcall_level函数:

static void __init do_initcalls(void)
{
    int level;
    for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
        do_initcall_level(level);
}

initcall_levels数组在同一个源码文件中定义,包含了定义在__define_initcall宏中的那些段的指针:

static initcall_t *initcall_levels[] __initdata = {
    __initcall0_start,
    __initcall1_start,
    __initcall2_start,
    __initcall3_start,
    __initcall4_start,
    __initcall5_start,
    __initcall6_start,
    __initcall7_start,
    __initcall_end,
};

如果你有兴趣,你可以在 Linux 内核编译后生成的链接器脚本arch/x86/kernel/vmlinux.lds中找到这些段:

.init.data : AT(ADDR(.init.data) - 0xffffffff80000000) {
    ...
    ...
    ...
    ...
    __initcall_start = .;
    *(.initcallearly.init)
    __initcall0_start = .;
    *(.initcall0.init)
    *(.initcall0s.init)
    __initcall1_start = .;
    ...
    ...
}

如果你对这些不熟,可以在本书的某些部分了解更多关于链接器的信息。

正如我们刚看到的,do_initcall_level 函数有一个参数 - initcall 的级别,做了以下两件事:首先这个函数拷贝了 initcall_command_line,这是通常内核包含了各个模块参数的命令行的副本,并用 kernel/params.c源码文件的 parse_args 函数解析它,然后调用各个级别的 do_on_initcall 函数:

for (fn = initcall_levels[level];
fn < initcall_levels[level+1]; fn++)
        do_one_initcall(*fn);

do_on_initcall为我们做了主要的工作。我们可以看到,这个函数有一个参数表示initcall回调函数,并调用给定的回调函数:

int __init_or_module do_one_initcall(initcall_t fn)
{
    int count = preempt_count();
    int ret;
    char msgbuf[64];
    if (initcall_blacklisted(fn))
        return -EPERM;
    if (initcall_debug)
        ret = do_one_initcall_debug(fn);
    else
        ret = fn();
    msgbuf[0] = 0;
    if (preempt_count() != count) {
        sprintf(msgbuf, "preemption imbalance ");
        preempt_count_set(count);
    }
    if (irqs_disabled()) {
        strlcat(msgbuf, "disabled interrupts ", sizeof(msgbuf));
        local_irq_enable();
    }
    WARN(msgbuf[0], "initcall %pF returned with %s\n", fn, msgbuf);
    return ret;
}

让我们来试着理解do_on_initcall函数做了什么。首先我们增加preemption计数,以便我们稍后进行检查,确保它不是不平衡的。这步以后,我们可以看到initcall_backlist函数的调用,这个函数遍历包含了initcalls黑名单的blacklisted_initcalls链表,如果initcall在黑名单里就释放它:

list_for_each_entry(entry, &blacklisted_initcalls, next) {
    if (!strcmp(fn_name, entry->buf)) {
        pr_debug("initcall %s blacklisted\n", fn_name);
        kfree(fn_name);
        return true;
    }
}

黑名单的 initcalls 保存在 blacklisted_initcalls 链表中,这个链表是在早期 Linux 内核初始化时由 Linux 内核命令行来填充的。处理完进入黑名单的 initcalls,接下来的代码直接调用 initcall:

if (initcall_debug)
    ret = do_one_initcall_debug(fn);
else
    ret = fn();

取决于 initcall_debug 变量的值,do_one_initcall_debug 函数将调用 initcall,或直接调用 fn()。initcall_debug 变量定义在同一个源码文件:

bool initcall_debug;

该变量提供了向内核日志缓冲区打印一些信息的能力。可以通过 initcall_debug 参数从内核命令行中设置这个变量的值。从Linux内核命令行文档可以看到:

initcall_debug    [KNL] Trace initcalls as they are executed. Useful
 for working out where the kernel is dying during
                      startup.

确实如此。如果我们看下 do_one_initcall_debug 函数的实现,我们会看到它与 do_one_initcall 函数做了一样的事,也就是说,do_one_initcall_debug 函数调用了给定的 initcall,并打印了一些和 initcall 相关的信息(比如当前任务的 pid、initcall 的持续时间等):

static int __init_or_module do_one_initcall_debug(initcall_t fn)
{
    ktime_t calltime, delta, rettime;
    unsigned long long duration;
    int ret;
    printk(KERN_DEBUG "calling  %pF @ %i\n", fn, task_pid_nr(current));
    calltime = ktime_get();
    ret = fn();
    rettime = ktime_get();
    delta = ktime_sub(rettime, calltime);
    duration = (unsigned long long) ktime_to_ns(delta) >> 10;
    printk(KERN_DEBUG "initcall %pF returned %d after %lld usecs\n",
         fn, ret, duration);
    return ret;
}

由于initcall被do_one_initcall或do_one_initcall_debug调用,我们可以看到在do_one_initcall函数末尾做了两次检查。第一个检查在initcall执行内部__preempt_count_add和__preempt_count_sub可能的执行次数,如果这个值和之前的可抢占计数不相等,我们就把preemption imbalance字符串添加到消息缓冲区,并设置正确的可抢占计数:

if (preempt_count() != count) {
    sprintf(msgbuf, "preemption imbalance ");
    preempt_count_set(count);
}

稍后这个错误字符串就会被打印出来。最后检查本地IRQs的状态,如果它们被禁用了,我们就将disabled interrupts字符串添加到我们的消息缓冲区,并为当前处理器使能IRQs,以防出现IRQs被initcall禁用了但不再使能的情况出现:

if (irqs_disabled()) {
    strlcat(msgbuf, "disabled interrupts ", sizeof(msgbuf));
    local_irq_enable();
}

这就是全部了。通过这种方式,Linux 内核以正确的顺序完成了很多子系统的初始化。现在我们知道 Linux 内核的 initcall 机制是怎么回事了。在这部分中,我们介绍了 initcall 机制的主要部分,但遗留了一些重要的概念。让我们来简单看下这些概念。

首先,我们错过了一个级别的 initcalls,就是 rootfs initcalls。和我们在本部分看到的很多宏类似,你可以在 include/linux/init.h 头文件中找到 rootfs_initcall 的定义:

#define rootfs_initcall(fn)       
 __define_initcall(fn, rootfs)

从这个宏的名字我们可以理解到,它的主要目的是保存和 rootfs 相关的回调。除此之外,只有在与设备相关的东西没被初始化时,在文件系统级别初始化以后再初始化一些其它东西时才有用。例如,发生在源码文件 init/initramfs.c 中 populate_rootfs 函数里的解压 initramfs:

rootfs_initcall(populate_rootfs);

在这里,我们可以看到熟悉的输出:

[ 0.199960] Unpacking initramfs...

除了 rootfs_initcall 级别,还有其它的 console_initcall、 security_initcall 和其他辅助的 initcall 级别。我们遗漏的最后一件事,是 *_initcall_sync 级别的集合。在这部分我们看到的几乎每个 *_initcall 宏,都有 _sync 前缀的宏伴随:

#define core_initcall_sync(fn)        __define_initcall(fn, 1s)
#define postcore_initcall_sync(fn)    __define_initcall(fn, 2s)
#define arch_initcall_sync(fn)        __define_initcall(fn, 3s)
#define subsys_initcall_sync(fn)    __define_initcall(fn, 4s)
#define fs_initcall_sync(fn)        __define_initcall(fn, 5s)
#define device_initcall_sync(fn)    __define_initcall(fn, 6s)
#define late_initcall_sync(fn)        __define_initcall(fn, 7s)

这些附加级别的主要目的是,等待所有某个级别的与模块相关的初始化例程完成。

责任编辑:武晓燕 来源: 深度Linux
相关推荐

2022-01-29 22:27:31

内核子线程应用

2015-06-09 17:14:11

容器LinuxLinux原子主机

2023-11-17 08:52:26

2013-11-11 16:15:15

云计算

2012-08-07 08:55:40

2012-05-22 10:10:41

2013-11-14 09:41:30

2018-09-13 10:37:54

思科幕后培训

2010-05-25 09:35:28

智能通信移动

2013-07-09 10:12:27

2013-09-18 15:20:49

用友U8

2014-03-19 15:05:14

数据中心苹果

2018-01-19 10:12:03

UCloudAI竞赛人工智能

2014-08-08 11:21:15

浪潮GSP+大数据

2015-09-09 09:10:58

项目英雄开源

2015-09-28 18:44:25

容联云通讯

2021-01-26 09:14:19

Linux内核模块

2012-05-30 15:15:13

2019-08-16 13:35:47

戴尔
点赞
收藏

51CTO技术栈公众号