一、导读
在linux内核启动过程中,会向终端打印出很多的日志信息,从这些信息中可以得到许多内核的行为。如果在启动阶段出现了问题,那么很多的提示信息也会从终端打印出。
这些信息的输出与具体模块功能的执行都归功于一个函数:do_initcalls,本文将主要分析这个函数的执行逻辑,且从这个函数延伸到linux各个子系统初始化背后的机制。
本文所有源码分析基于linux内核版本:4.1.15
二、do_initcalls
do_initcalls由do_basic_setup()调用:
do_basic_setup()由kernel_init()代表的内核init线程函数间接调用(在kernel_init_freeable()被调用)。
在调用do_basic_setup之前,处理器已经被初始化了,CPU子系统已经启动并且运行,内存和进程管理也工作正常,但是系统中的设备还没有被初始化,故而do_basic_setup正作用于此,本文主要描述do_initcalls,所以不再进而分析其他的函数。
do_initcalls在/init/main.c文件中实现:
static void __init do_initcalls(void) { int level; for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++) do_initcall_level(level); }
函数中内容比较少,是一个for循环结构,循环的对象是initcall_levels数组,该数组用于描述初始化调用的级别,定义如下:
extern initcall_t __initcall_start[]; extern initcall_t __initcall0_start[]; extern initcall_t __initcall1_start[]; extern initcall_t __initcall2_start[]; extern initcall_t __initcall3_start[]; extern initcall_t __initcall4_start[]; extern initcall_t __initcall5_start[]; extern initcall_t __initcall6_start[]; extern initcall_t __initcall7_start[]; extern initcall_t __initcall_end[]; static initcall_t *initcall_levels[] __initdata = { __initcall0_start, __initcall1_start, __initcall2_start, __initcall3_start, __initcall4_start, __initcall5_start, __initcall6_start, __initcall7_start, __initcall_end, };
从上述代码可见,initcall_levels数组中的元素为initcall_t类型的指针,回到do_initcalls()函数中,该函数的核心操作是:按顺序从__initcall0_start开始,到__initcall_end结束的节段(称为初始化调用段)中取出不同段之间的函数,并执行。
存在这几个初始化调用段之间的函数都是内核中各个模块的初始化函数,而这些函数是如何加入到初始化调用段中的呢?又是如何设置调用级别的,会在后文中描述到。
在do_initcalls()函数中,会根据initcall_levels初始化调用级别的数量调用do_initcall_level(),该函数实现如下:
static void __init do_initcall_level(int level) { initcall_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, &repair_env_string); for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++) do_one_initcall(*fn); }
从上述代码可见,在函数的最后也是一个for循环结构,该循环的操作对象为函数指针,且会将对应的函数指针传递到do_one_initcall中,在该函数执行函数指针所指向的函数:
三、构造section并添加函数
(3-1)构造初始化调用section
在linux内核中,不同架构(ARCH)下的kernel目录中,都会有一个名为vmlinux.lds.S的链接脚本,初始化调用section的构造则在这个链接脚本中完成。
本文以ARM32架构为例
在/arch/arm/kernel/vmlinux.lds.S中的链接脚本中,.init.data输出节段则需要INIT_CALLS作为输入节段:
INIT_CALLS定义在/include/asm-generic/vmlinux.lds.h文件中:
而在内核的makefile中有以下语句:
LDFLAGS_vmlinux += -T arch/$(ARCH)/kernel/vmlinux.lds.s
用于指定构建linux内核镜像时所使用的链接脚本,基于此,则会构造好初始化调用section。
当初始化调用section构造完成后,是如何向该section中添加函数的呢?继续往下看。
(3-2)向section中添加函数
向section中添加函数的本质操作则是__define_initcall(),定义如下:
并且linux内核基于__define_initcall()封装出了多个宏定义接口,供内核中各个模块使用,接口如下:
__define_initcall()宏定义的本质则是定义一个initcall_t函数指针类型的变量并命名为__initcall_##fn##id,其中fn为赋值给该变量的函数名称,id为初始化调用级别,然后将fn赋值给该变量。接着就是最为重要的技术点:使用__attribute__将该变量加入到命名为"initcall##id.init"的section中,其中id为初始化调用级别,所以将fn添加到初始化调用section中则是通过这一点实现。例如:如果有以下类似的代码:
static void __init show_info(void) { printk("I'm iriczhao ") } core_initcall(show_info);
经过层层宏替换后,本质上则变成:
static initcall_t __initcall_core_initcall1 __used __attribute__((__section__(".initcall1.init"))) = show_info;
四、总结
综上,linux内核中使用基于__define_initcall封装出的多个接口API初始化内核的各个模块,使用这些API接口会将指定的函数放到名称为.initcall##id.init的section中,id为初始化调用级别,内核中定义了14种调用级别:分别为1~7和1s~7s(linux 3.0后增加的扩展)。这些调用级别是按照先后顺序依次排列的。
(4-1)linux内核中,对于内核的各个模块的初始化,正是通过使用__define_initcall()的衍生宏定义API将初始化函数放置到__initcall##id.initsection中,不同模块的初始化函数按照调用级别顺序排列。在内核启动阶段,这些放置到这个section中的函数指针将被do_initcalls()按顺序依次调用,进而完成各个模块的初始化操作。
linux内核系统非常庞大,各个子系统也非常多,他们的初始化函数从源码上是不需要在内核启动过程中去主动调用的,从设计上这一点也不现实,随着内核功能的增加,越来越复杂的驱动程序,从而linux内核基于编译器section技术,设计了初始化调用机制,将各个模块的初始化与linux内核启动主线分离。
(4-2)当使用基于__define_initcall封装出的多个API接口时,函数指针放置到哪个子section由具体的宏定义API接口的level参数确定,较小的level参数对应的函数指针则被放置在前面。而位于同一个子section内的函数指针顺序不定,由编译器按照编译的顺序随机指定。所以,如果一个模块的初始化函数想要越早被调用执行,则需要有较小的调用级别。
审核编辑:刘清
全部0条评论
快来发表一下你的评论吧 !