天天看點

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段。

繼續閱讀