天天看点

Linux中的initcall以及module_init

背景

Linux内核是如何确保各子系统按序加载的?

initcall调用流程

start_kernel 
  -->arch_call_rest_init 
    -->rest_init 
      -->kthread_create(kernel_init) 
        -->kernel_init_freeable 
          -->do_basic_setup 
            -->driver_init //init driver model
                -->devices_init
                -->of_core_init
                -->hypervisor_init
                -->platform_bus_init
                -->container_dev_init
            -->do_initcalls //initcall      

通过以上的调用栈可以找到最终的实际的 initcall 数据段处理函数 ​

​do_initcalls​

​,只需要解析这些数据段即可按顺序逐句调入到个注册的函数内。

在生成vmlinux的链接阶段,编译器为initcall创建了特定的section,每一类initcall对应一组section,然后遍历执行initcall section中的initcalls。

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

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

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

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

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

各个函数如下,于是实际的数据段映射关系是:

pure_initcall(fn)->.initcall0.init ;

core_initcall(fn)->.initcall1.init 。

我们可以看到有各种类型的initcall,从名字大概可以猜测得出每一个类型的 initcall 所起的作用,每一种类型都是通过 ​

​__define_initcall​

​ 做的定义,唯一的区别就是第二个参数不一样,这个参数一定代表着等级,他们决定着内核在启动过程中启动顺序,这就给我们各个功能部件提供了一个启动的关系表,让被依赖的子系统首先启动,这样可以确保 Linux 的所有子系统正确完成初始化。

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

内核驱动中的module_init函数

我们在驱动中经常会使用到module_init(xxx_init)来设置这个驱动的入口函数,module_init函数的定义如下:

#define module_init(x)  __initcall(x);

#define __initcall(fn) device_initcall(fn)      

这里我们说的module_init是指驱动已经打包进了内核。device_initcall是众多宏定义中的一个,因此所有的设备驱动排在第6的加载顺序。

insmod操作

对于insmod加载驱动操作,它的调用栈如下。

Linux中的initcall以及module_init

do_init_module

对于最后的do_init_module函数,是调用do_one_initcall(mod->init)函数来实现的。

do_one_initcall用__init_or_module(___init)进行了修饰。__init的定义如下:

//include/linux/init.h
#define __init    __section(".init.text") __cold  __latent_entropy __noinitretpoline      

也就是说对于do_one_initcall函数,在预编译的时候也为其空出了一块数据段".init.text"。而我们在定义驱动init函数的时候也得使用__init定义这个入口函数对吧。对于exit函数也是一样的,预留出了.exit.data段。

继续阅读