请稍侯

内核启动的initcall

1. initcall 定义

include/linux/init.h 中定义:

#define early_initcall(fn)      __define_initcall(fn, early)
#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)

最终都是通过 __define_initcall 实现的 :

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

也就是说经过 *_initcall 修饰的函数最终都会按照顺序放到 section .initcall#id#.init 中,然后kernel 会按照顺序执行这些函数。

而在 include/asm-generic/vmlinux.lds.h 里面已经定义好了对应的 section :

#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                     \
    }
#define VMLINUX_SYMBOL(x) __VMLINUX_SYMBOL(x)

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

最终编译内核使用 lds 链接脚本 $(arch)/kernel/vmlinux.lds 中就有:

INIT_DATA_SECTION(...)

2. initcall 的执行

kernel 启动过程中会执行 init/main.cstart_kernel(),然后按照下面的调用链最终会调用 do_initcalls() 执行这些初始化函数:

start_kernel()->rest_init()->kernel_thread(kernel_init)->kernel_init_freeable()->do_basic_setup()->do_initcalls()->do_initcall_level()

而执行的顺序就跟各个宏的定义有关,比如 pure_initcall 实际上是 __define_initcall(fn, 0) 所以它会被首先执行,而 late_initcall__define_initcall(fn, 7) 所以会最后执行。具体的执行如下:

static void __init do_initcalls(void)
{
    int level;

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

do_initcall_level() 会执行每个 section 内部的所有初始化函数 :

static void __init do_initcall_level(int level)
{
...
    for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
        do_one_initcall(*fn);
}

do_initcall_level() 按顺序遍历每个 section 执行所有函数。

两个相关的数组,initcall_levels 指向了每个 section 的起始地址,也就是第一个函数地址,initcall_level_names 则将 section 的顺序号(就是 0,1,2,3 这些)和初始化函数的含义关联起来(比如 0 对应 early ,1 对应 core):

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

/* Keep these in sync with initcalls in include/linux/init.h */
static char *initcall_level_names[] __initdata = {
    "early",
    "core",
    "postcore",
    "arch",
    "subsys",
    "fs",
    "device",
    "late",
};

3. initdata

__init__initdata 都定义在 include/linux/init.h :

#define __init      __section(.init.text) __cold notrace
#define __initdata  __section(.init.data)               

相对的也定义了:

#define __exitdata  __section(.exit.data)

同时各个子系统根据自身需求有对这几个宏进行了封装,但是本质是一样的,比如 include/net/net_namespace.h 中:

#define __net_init  __init         
#define __net_exit  __exit_refok   
#define __net_initdata  __initdata 
#define __net_initconst __initconst

使用这几个宏修饰的变量都保存到了对应的 section ,而内核启动时会按照既定格式去处理该 section 内的结构体变量。

[After boot, the kernel frees up a special section;

functions marked with __init and data structures marked with __initdata are dropped after boot is complete (within modules this directive is currently ignored).

__exit is used to declare a function which is only required on exit: the function will be dropped if this file is not compiled as a module.]1

Static data structures marked as __initdata must be initialised (as opposed to ordinary static data which is zeroed BSS) and cannot be const.

以 arm 为例,在 arch/arm/mm/init.cfree_initmem() 会在 boot 结束后执行:

static int __ref kernel_init(void *unused)
{
    kernel_init_freeable();
    /* need to finish all async __init code before freeing the memory */
    async_synchronize_full();
    free_initmem();
    ...
}

执行后会清理掉段 .init.*

void free_initmem(void)
{
#ifdef CONFIG_HAVE_TCM
    extern char __tcm_start, __tcm_end;

    poison_init_mem(&__tcm_start, &__tcm_end - &__tcm_start);
    free_reserved_area(&__tcm_start, &__tcm_end, -1, "TCM link");
#endif

    poison_init_mem(__init_begin, __init_end - __init_begin);
    if (!machine_is_integrator() && !machine_is_cintegrator())
        free_initmem_default(-1);
}
static inline unsigned long free_initmem_default(int poison)
{
    extern char __init_begin[], __init_end[];

    return free_reserved_area(&__init_begin, &__init_end,
                  poison, "unused kernel");
}

其中 __init_begin__init_end 就是 .init.* 的地址范围

4. why

为什么要这样做?我的理解原因有二:

  1. 简化操作,不在定义每个函数、变量时写一堆 gcc 的编译指令,以简单的 __initdata__initcore_initcall 等宏定义代替复杂的命令,既简单又有清晰的含义;
  2. 把这么多的函数、变量根据其功能放到不同的段,然后由系统统一调用、清理,这样每个人实现自己功能时就不需要考虑在何处调用自己的函数,何时清理自己不用的数据。

但是这样有个不好的地方,不熟悉代码的人不能清楚的知道初始化阶段要执行哪些函数,他只能以 __init 为关键字搜索全部代码来知道那些函数是初始化函数。