本文主要分析linux内核如何顺序调用init_call初始化函数,所使用的内核版本为linux5.0.5。

一.内核按级别调用初始化函数过程

linux内核initcall调用的入口函数为do_initcalls(),该函数位于init/main.c中。

该函数根据级别通过do_initcall_level函数依次进行所有级别的初始化工作,在linux内核该级别定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
extern initcall_entry_t __initcall_start[];
extern initcall_entry_t __initcall0_start[];
extern initcall_entry_t __initcall1_start[];
extern initcall_entry_t __initcall2_start[];
extern initcall_entry_t __initcall3_start[];
extern initcall_entry_t __initcall4_start[];
extern initcall_entry_t __initcall5_start[];
extern initcall_entry_t __initcall6_start[];
extern initcall_entry_t __initcall7_start[];
extern initcall_entry_t __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,
};

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

initcall_levels数组中总共0-7这八个级别,通过对应的initcall_level_names数组可知其类别。数组中每一项为该级别中第一个初始化函数的地址,在do_initcall_level函数中通过for循环调用do_one_initcall函数调用一个级别中的所有初始化函数。

1
2
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(initcall_from_entry(fn));

do_one_initcall函数传入参数为initcall_t类型,该类型在内核中的定义如下:

1
2
3
4
5
/*
* Used for initialization calls..
*/
typedef int (*initcall_t)(void);
typedef void (*exitcall_t)(void);

由以上定义可知,内核模块和子系统入口和出口函数的格式。

函数do_one_initcall所做的工作:

do_one_initcall函数首先判断要调用的函数是否在“黑名单”中,如果在则不调用这个初始化函数。

1
2
if (initcall_blacklisted(fn))
return -EPERM;

紧接着调用该初始化函数指针。

1
2
3
do_trace_initcall_start(fn);
ret = fn();
do_trace_initcall_finish(fn, ret);

然后判断优先级是否被改变,中断是否被屏蔽,并根据判断结果做相应的恢复工作。

1
2
3
4
5
6
7
8
if (preempt_count() != count) {
sprintf(msgbuf, "preemption imbalance ");
preempt_count_set(count);
}
if (irqs_disabled()) {
strlcat(msgbuf, "disabled interrupts ", sizeof(msgbuf));
local_irq_enable();
}

上面代码片段中的count是在do_one_initcall开始的时候进行保存的。

1
int count = preempt_count();

二.初始化入口函数设置

以内核USB子系统为例,usb子系统的初始化设置如下:

1
subsys_initcall(usb_init);

由以上代码可知,设置子系统初始化函数的宏为subsys_initcall,该宏定义在内核init.h中。

1
#define subsys_initcall(fn)		__define_initcall(fn, 4)

__define_initcall宏的定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/*
* initcalls are now grouped by functionality into separate
* subsections. Ordering inside the subsections is determined
* by link order.
* For backwards compatibility, initcall() puts the call in
* the device init subsection.
*
* The `id' arg to __define_initcall() is needed so that multiple initcalls
* can point at the same handler without causing duplicate-symbol build errors.
*
* Initcalls are run by placing pointers in initcall sections that the
* kernel iterates at runtime. The linker can do dead code / data elimination
* and remove that completely, so the initcall sections have to be marked
* as KEEP() in the linker script.
*/

#ifdef CONFIG_HAVE_ARCH_PREL32_RELOCATIONS
#define ___define_initcall(fn, id, __sec) \
__ADDRESSABLE(fn) \
asm(".section \"" #__sec ".init\", \"a\" \n" \
"__initcall_" #fn #id ": \n" \
".long " #fn " - . \n" \
".previous \n");
#else
#define ___define_initcall(fn, id, __sec) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(#__sec ".init"))) = fn;
#endif

#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)

完全展开该宏后可知将usb_init函数放到了.initcall4.init段。该工作由架构目录下内核链接脚本完成。根据以上步骤分析module_init宏得知模块初始化放到.initcall6.init段,说明模块初始化优先级低于子系统初始化,而内核正是按照这种优先级别完成顺序初始化工作的。