35

Linux 各种 initcall 的调用原理

 5 years ago
source link: https://www.byteisland.com/linux-各种-initcall-的调用原理/?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

很早以前就想写一下关于 initcall 的相关知识点,但是一直苦于没有特别强烈的需求以及自己的懒惰才拖到现在,其实一直在想要不要写一篇这样的文章,毕竟目前网上能够查阅到几乎所有的资料,不过也没有什么关系,就当是为了记忆把之前的一些理解写下来。

module_init 与其他的 __initcall

很早以前,不理解模块是如何插入到内核中的,特地的分析了一次 module_init 是如何工作的,在头文件 include/linux/module.h 中定义了:

#define module_init(x)  __initcall(x);

这个 __initcall 又是一个什么鬼?一起来继续深入分析,在 include/linux/init.h 定义如下:

#define __initcall(fn) device_initcall(fn)

原来所谓的 module_init 就是一个设备的初始化注册,这其实也很符合模块的设计初衷,那本来就是为了适应新的设备而设计的驱动加载框架,当然在观察到 module_init 属于 device_initcall 的过程中就肯定也关注到了更多的 __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)

/*
 * Early initcalls run before initializing SMP.
 *
 * Only for built-in code, not modules.
 */
#define early_initcall(fn)              __define_initcall(fn, early)

/*
 * A "pure" initcall has no dependencies on anything else, and purely
 * initializes variables that couldn't be statically initialized.
 *
 * This only exists for built-in code, not for modules.
 * Keep main.c:initcall_level_names[] in sync.
 */
#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)

可以看到有各种类型的 initcall,从名字大概可以猜测得出每一个类型的 initcall 所起的作用,每一种类型都是通过 __define_initcall 做的定义,唯一的区别就是第二个参数不一样,一起猜测一下这个参数一定代表着等级,他们决定着内核在启动过程中启动顺序,这就给我们各个功能部件提供了一个启动的关系表,让被依赖的子系统首先启动,这样可以确保 Linux 的所有子系统正确完成初始化。

谁在挥舞 __initcall_xx.init

尽管了解了他们都是使用 gcc 的属性 section 通过名字 __initcall_xx.init 来存放一系列的函数地址,那么这些个段又是在哪儿定义的呢?通过 grep 不难搜索得到,位于 include/asm-generic/vmlinux.lds.h

#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) = .;

#define INIT_DATA_SECTION(initsetup_align)                              \
        .init.data : AT(ADDR(.init.data) - LOAD_OFFSET) {               \
                INIT_DATA                                               \
                INIT_SETUP(initsetup_align)                             \
                INIT_CALLS                                              \
                CON_INITCALL                                            \
                SECURITY_INITCALL                                       \
                INIT_RAM_FS                                             \
        }

所以只需要定义好 INIT_DATA_SECTION 即可在内核 vmlinux 中定义好所有的 initcall 所需要的段,每一种不同的架构所定义的 vmlinux 二进制段都不太一样,例如在 arm64 平台下就是直接使用的 INIT_CALL 直接定义的 initcall 数据段,而 arm32 下使用的是 INIT_DATA_SECTION 这样的结构,arm64 定义位于 arch/arm64/kernel/vmlinux.lds.S

.init.data : {
         INIT_DATA
         INIT_SETUP(16)
         INIT_CALLS
         CON_INITCALL
         INIT_RAM_FS
         *(.init.rodata.* .init.bss)     /* from the EFI stub */
 }

这些段在内核编译阶段就已经确立好了,在不同的驱动/子系统的源码结构下通过不同的 __initcall 定义的函数入口都会被 gcc 插入到不同名称的数据段之下。

如何调用

以上已经将各种类型的数据通过 gcc 的数据段属性功能将不同的初始化函数按照一定的顺序插入到 vmlinux 的二进制文件中,接下来就需要通过一定的手段解析这些段并让他们然后定义的顺序逐个执行。

start_kernel -->
    arch_call_rest_init -->
        rest_init -->
            kthread_create(kernel_init) -->
                kernel_init_freeable -->
                    do_basic_setup -->
                        do_initcalls

通过以上的调用栈可以找到最终的实际的 initcall 数据段处理函数 do_initcalls ,只需要解析这些数据段即可按顺序逐句调入到个注册的函数内。

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

static const char *initcall_level_names[] __initdata = {
        "pure",
        "core",
        "postcore",
        "arch",
        "subsys",
        "fs",
        "device",
        "late",
};

static void __init do_initcalls(void)
{
        int level;

        for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
                do_initcall_level(level);
}

static void __init do_initcall_level(int level)
{
        initcall_entry_t *fn;

        strcpy(initcall_command_line, saved_command_line);
        parse_args(initcall_level_names[level],
                ┊  initcall_command_line, __start___param,
                ┊  __stop___param - __start___param,
                ┊  level, level,
                ┊  NULL, &repair_env_string);

        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));
}

int __init_or_module do_one_initcall(initcall_t fn)
{
        int count = preempt_count();
        char msgbuf[64];
        int ret;

        if (initcall_blacklisted(fn))
                return -EPERM;

        do_trace_initcall_start(fn);
        ret = fn();
        do_trace_initcall_finish(fn, ret);

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

        add_latent_entropy();
        return ret;
}

通过对每一级 level 内的所有段内函数进行遍历执行,完成对等级的划分以及函数的执行。

参考链接

利用 attribute ((section()))构建初始化函数表与Linux内核init的实现


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK