本文采用Linux 内核 v3.10 版本 x86-64 架构
一、概述
在 Linux 内核中,我们经常会看到类似下面的代码:
c
// file: kernel/sched/core.c
early_initcall(migration_init);
或者
c
// file: fs/devpts/inode.c
module_init(init_devpts_fs)
在上面两段代码中的 early_initcall()
和 module_init()
函数,它们使用了内核的 initcall
机制。
总体来说,initcall
机制实际是向 Linux 内核中注册了多组回调函数,这些函数会在系统初始化的时候被调用。而且,不同组内的函数,其调用优先级是不同的,在执行的时候会有先后顺序。
在 Linux 内核中,通过 initcall
机制注册的函数,主要分为 0 ~ 7 共 8 个等级:
c
// file: init/main.c
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_end
仅用来表示数组元素数量,没有对应的函数。
不同等级的名称保存在 initcall_level_names
变量里,等级越小的优先级越高。比如等级为 1 的函数会比等级为 7 的先执行,也就是说通过 core_initcall()
注册的函数比通过 late_initcall()
注册的先执行。
c
// file: init/main.c
/* 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",
};
除了 0 ~ 7 这 8 个等级外,还有几个特殊的等级,如early
、rootfs
等,我们在下文中都可以看到。
二、initcall 机制的实现
2.1 一组宏 / *_initcall()
Linux 内核提供了一组宏用来注册不同等级的 initcall
函数。这些宏位于 include/linux/init.h
文件中:
c
// file: include/linux/init.h
/*
* Early initcalls run before initializing SMP.
*
* Only for built-in code, not modules.
*/
#define early_initcall(fn) __define_initcall(fn, early)
/*
* A "pure" initcall has no dependencies on anything else, and purely
* initializes variables that couldn't be statically initialized.
*
* This only exists for built-in code, not for modules.
* Keep main.c:initcall_level_names[] in sync.
*/
#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)
可以看到,这些 *_initcall()
宏又扩展为 __define_initcall()
宏。
2.1.1 宏 __define_initcall()
宏 __define_initcall()
的作用是注册一个 initcall
函数,该宏定义如下:
c
// file: include/linux/init.h
/* 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.
*/
#define __define_initcall(fn, id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" #id ".init"))) = fn
该宏接收 2 个参数:
fn
-- 回调函数,该函数在系统初始化时被执行id
--initcall
标识,防止多个同样的initcalls
指向同一个处理程序时导致构建时出现重复符号的错误
首先,我们来看下函数类型 initcall_t
,其定义如下:
c
// file: include/linux/init.h
/*
* Used for initialization calls..
*/
typedef int (*initcall_t)(void);
initcall_t
类型的函数,返回值为 int
类型且没有参数。
接下来,__initcall_##fn##id
将入参 fn
和 id
以及 字符串__initcall_
连接成一个函数名。比如,early_initcall(migration_init)
扩展后的函数名为 __initcall_migration_initearly
。其中,## 是符号连接符。
宏 __used
扩展如下:
c
// file: include/linux/compiler-gcc4.h
#define __used __attribute__((__used__))
使用 used 属性修饰的函数,当该函数没有被引用时,GCC 不会发出 defined but not used
的警告,并强制编译器将它们编译到汇编文件。
__attribute__((__section__(".initcall" #id ".init")))
会指示编译器将该函数放置到 .initcall<id>.init
节。比如,migration_init()
函数会被放置到 .initcallearly.init
节。
接下来,我们看到,在连接脚本 include/asm-generic/vmlinux.lds.h
里,将这些 initcall
相关的节,放置到符号 __initcall_start
和 __initcall_end
之间,其中使用 early_initcall()
定义的函数位于最靠前的位置,即 .initcallearly.init
节。
assembly
// file: include/asm-generic/vmlinux.lds.h
#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) = .;
另外,宏 INIT_CALLS
内部又引用了 INIT_CALLS_LEVEL()
宏,该宏定义如下:
c
// file: include/asm-generic/vmlinux.lds.h
#define INIT_CALLS_LEVEL(level) \
VMLINUX_SYMBOL(__initcall##level##_start) = .; \
*(.initcall##level##.init) \
*(.initcall##level##s.init) \
INIT_CALLS_LEVEL()
宏只有 1 个参数,即 initcall
函数的级别 -- level
。在宏内部,首先定义了一个该 level
所在节的起始地址,即__initcall##level##_start
。根据参数 level
的不同,可能是 __initcall0_start
、 __initcallrootfs_start
、__initcall7_start
等等。
接下来,是该 level
相关的 2 个节,.initcall##level##.init
和 .initcall##level##s.init
。根据 level
值的不同,这 2 个节可以扩展为 .initcall0.init
、 .initcall0s.init
或者 .initcall7.init
、 .initcall7s.init
等等。
宏 INIT_CALLS
扩展后如下所示:
assembly
// file: include/asm-generic/vmlinux.lds.h
#define INIT_CALLS \
VMLINUX_SYMBOL(__initcall_start) = .; \
*(.initcallearly.init) \
VMLINUX_SYMBOL(__initcall0_start) = .; \
*(.initcall0.init) \
*(.initcall0s.init) \
VMLINUX_SYMBOL(__initcall1_start) = .; \
*(.initcall1.init) \
*(.initcall1s.init) \
VMLINUX_SYMBOL(__initcall2_start) = .; \
*(.initcall2.init) \
*(.initcall2s.init) \
...
...
VMLINUX_SYMBOL(__initcall5_start) = .; \
*(.initcall5.init) \
*(.initcall5s.init) \
VMLINUX_SYMBOL(__initcallrootfs_start) = .; \
*(.initcallrootfs.init) \
*(.initcallrootfss.init) \
VMLINUX_SYMBOL(__initcall6_start) = .; \
*(.initcall6.init) \
*(.initcall6s.init) \
...
VMLINUX_SYMBOL(__initcall_end) = .;
这些节中的数据都会被链接脚本输出到 .init.data
节中:
assembly
// file: include/asm-generic/vmlinux.lds.h
#define INIT_DATA_SECTION(initsetup_align) \
.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) { \
...
INIT_CALLS \
...
}
最后,.init.data
节,会被放置到 __init_begin
和 __init_end
这两个符号之间:
assembly
SECTIONS
{
...
* Init code and data - will be freed after init */
. = ALIGN(PAGE_SIZE);
.init.begin : AT(ADDR(.init.begin) - LOAD_OFFSET) {
__init_begin = .; /* paired with __init_end */
}
...
INIT_DATA_SECTION(16)
...
/* freed after init ends here */
.init.end : AT(ADDR(.init.end) - LOAD_OFFSET) {
__init_end = .;
}
...
}
这 2 个符号之间的数据,在系统初始化完成之后,会被释放掉。
这些符号声明如下:
c
// init/main.c
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[];
可以看到,这些符号都被声明为 initcall_t
数组。其中,__initcall_start
和 __initcall_end
分别表示 initcall
相关节的起始和结束地址;其它符号,分别表示对应 level
的起始地址。
接下来,我们看下这些通过 initcall
机制注册的函数,是什么时候执行的。
2.2 early_initcall()
使用 early_initcall()
注册的函数,在链接后,位于符号 __initcall_start
和 __initcall0_start
之间。这些回调函数在 do_pre_smp_initcalls()
函数中被调用。
c
// file: init/main.c
static void __init do_pre_smp_initcalls(void)
{
initcall_t *fn;
for (fn = __initcall_start; fn < __initcall0_start; fn++)
do_one_initcall(*fn);
}
在 do_pre_smp_initcalls()
中,会遍历 __initcall_start
和 __initcall0_start
之间的函数,并通过 do_one_initcall()
函数来调用它们。
2.2.1 do_one_initcall()
do_one_initcall()
函数定义如下:
c
// file: init/main.c
int __init_or_module do_one_initcall(initcall_t fn)
{
...
int ret;
if (initcall_debug)
ret = do_one_initcall_debug(fn);
else
ret = fn();
...
return ret;
}
函数内部,根据变量 initcall_debug
来决定是直接调用 fn()
函数还是调用 do_one_initcall_debug()
函数。initcall_debug
是一个 bool
类型变量,用来决定是否需要追踪 initcall
函数的执行。
c
// file: init/main.c
bool initcall_debug;
assembly
// file: Documentation/kernel-parameters.txt
initcall_debug [KNL] Trace initcalls as they are executed. Useful
for working out where the kernel is dying during
startup.
从 do_one_initcall_debug()
函数的名称也能推断出,该函数包含一些调试信息。
c
// file: init/main.c
static int __init_or_module do_one_initcall_debug(initcall_t fn)
{
ktime_t calltime, delta, rettime;
unsigned long long duration;
int ret;
pr_debug("calling %pF @ %i\n", fn, task_pid_nr(current));
calltime = ktime_get();
ret = fn();
rettime = ktime_get();
delta = ktime_sub(rettime, calltime);
duration = (unsigned long long) ktime_to_ns(delta) >> 10;
pr_debug("initcall %pF returned %d after %lld usecs\n",
fn, ret, duration);
return ret;
}
可以看到, do_one_initcall_debug()
函数中,在 fn()
函数调用前后,通过 ktime_get()
函数分别获取了开始时间和结束时间,然后计算出时间差并打印出来。最后,返回 fn()
函数的执行结果。
2.3 主要 initcalls
宏 early_initcall()
注册的是 early
等级的函数,注册其它等级函数的宏如下所示。
注册 0 ~ 7 等级函数的宏:
core_initcall()
postcore_initcall()
arch_initcall()
subsys_initcall()
fs_initcall()
device_initcall()
late_initcall()
注册 rootfs
等级函数的宏:
rootfs_initcall()
注册 0s
~ 7s
等级函数的宏:
core_initcall_sync()
postcore_initcall_sync()
arch_initcall_sync()
subsys_initcall_sync()
fs_initcall_sync()
device_initcall_sync()
late_initcall_sync()
通过这些宏注册的函数,在链接后,位于 __initcall0_start
和 __initcall_end
之间。
2.3.1 do_initcalls()
上述 initcall
函数在 do_initcalls()
中被调用,该函数位于 do_basic_setup()
函数中。
c
// file: init/main.c
/*
* Ok, the machine is now initialized. None of the devices
* have been touched yet, but the CPU subsystem is up and
* running, and memory and process management works.
*
* Now we can finally start doing some real work..
*/
static void __init do_basic_setup(void)
{
...
do_initcalls();
}
do_initcalls()
函数定义如下:
c
// file: 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);
}
initcall_levels
是一个数组,该数组一共 9 个成员,其中每个成员又是 initcall_t
类型的数组。除了最后一个成员 __initcall_end
仅用来标识结束地址外,前 8 个成员分别对应着不同的 level
的函数集合。
c
// file: init/main.c
static initcall_t *initcall_levels[] __initdata = {
__initcall0_start,
__initcall1_start,
__initcall2_start,
__initcall3_start,
__initcall4_start,
__initcall5_start,
__initcall6_start,
__initcall7_start,
__initcall_end,
};
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[];
在 do_initcalls()
函数中,会遍历所有 level
,然后调用 do_initcall_level()
函数来执行该 level
下的所有回调函数。
do_initcall_level()
函数定义如下:
c
// file: init/main.c
static void __init do_initcall_level(int level)
{
...
initcall_t *fn;
...
for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(*fn);
}
在 do_initcall_level()
函数内部,会遍历该 level
下所有的回调函数,并调用 do_one_initcall()
函数来执行回调。
值得注意的是,通过宏 rootfs_initcall()
注册的函数位于 __initcall5_start[]
数组中。
2.4 module_init()
宏 module_init()
扩展为 __initcall()
:
c
// file: include/linux/init.h
/**
* module_init() - driver initialization entry point
* @x: function to be run at kernel boot time or module insertion
*
* module_init() will either be called during do_initcalls() (if
* builtin) or at module insertion time (if a module). There can only
* be one per module.
*/
#define module_init(x) __initcall(x);
而 __initcall()
又扩展为 device_initcall()
:
c
// file: include/linux/init.h
#define __initcall(fn) device_initcall(fn)
device_initcall()
宏我们在上文中已经介绍过了,通过该宏注册的函数其 level
为 6:
c
#define device_initcall(fn) __define_initcall(fn, 6)
2.5 console_initcall()
通过 console_initcall()
注册的函数,会被放置在 .con_initcall.init
节。
c
// file: include/linux/init.h
#define console_initcall(fn) \
static initcall_t __initcall_##fn \
__used __section(.con_initcall.init) = fn
该节的起始地址是 __con_initcall_start
,结束地址是 __con_initcall_end
。
c
// file: include/asm-generic/vmlinux.lds.h
#define CON_INITCALL \
VMLINUX_SYMBOL(__con_initcall_start) = .; \
*(.con_initcall.init) \
VMLINUX_SYMBOL(__con_initcall_end) = .;
__con_initcall_start
和 __con_initcall_end
均被声明为 initcall_t
类型的数组。
c
// file: include/linux/init.h
extern initcall_t __con_initcall_start[], __con_initcall_end[];
.con_initcall.init
节的位置紧挨着宏 INIT_CALLS
创建的各节, 位于 __initcall_end
之后。
assembly
// file: include/asm-generic/vmlinux.lds.h
#define INIT_DATA_SECTION(initsetup_align) \
.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) { \
...
INIT_CALLS \
CON_INITCALL \
...
}
通过宏console_init()
注册的函数,在 console_init()
函数中被调用。
c
// file: drivers/tty/tty_io.c
/*
* Initialize the console device. This is called *early*, so
* we can't necessarily depend on lots of kernel help here.
* Just do some early initializations, and do the complex setup
* later.
*/
void __init console_init(void)
{
initcall_t *call;
...
/*
* set up the console device so that later boot sequences can
* inform about problems etc..
*/
call = __con_initcall_start;
while (call < __con_initcall_end) {
(*call)();
call++;
}
}
在 console_init()
函数内部,会遍历 __con_initcall_start
和 __con_initcall_end
之间的所有函数,并调用它们。
2.6 security_initcall()
通过 security_initcall()
注册的函数,会被放置在 .security_initcall.init
节。
c
// file: include/linux/init.h
#define security_initcall(fn) \
static initcall_t __initcall_##fn \
__used __section(.security_initcall.init) = fn
该节的起始地址是 __security_initcall_start
,结束地址是 __security_initcall_end
。
c
// file: include/asm-generic/vmlinux.lds.h
#define SECURITY_INITCALL \
VMLINUX_SYMBOL(__security_initcall_start) = .; \
*(.security_initcall.init) \
VMLINUX_SYMBOL(__security_initcall_end) = .;
__security_initcall_start
和 __security_initcall_end
均被声明为 initcall_t
类型的数组。
c
// file: include/linux/init.h
extern initcall_t __security_initcall_start[], __security_initcall_end[];
.security_initcall.init
节的位置紧挨着宏 CON_INITCALL
创建的节,即 .con_initcall.init
。
assembly
// file: include/asm-generic/vmlinux.lds.h
#define INIT_DATA_SECTION(initsetup_align) \
.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) { \
...
INIT_CALLS \
CON_INITCALL \
SECURITY_INITCALL \
...
}
通过宏security_init()
注册的函数,在 security_init()
函数中被调用。
c
// file: security/security.c
/**
* security_init - initializes the security framework
*
* This should be called early in the kernel initialization sequence.
*/
int __init security_init(void)
{
...
do_security_initcalls();
return 0;
}
security_init()
函数内部,通过 do_security_initcalls()
函数,执行所有的回调函数。
c
// file: security/security.c
static void __init do_security_initcalls(void)
{
initcall_t *call;
call = __security_initcall_start;
while (call < __security_initcall_end) {
(*call) ();
call++;
}
}
2.7 各组 initcall 函数的执行顺序
三、参考资料
1、gcc - "##" 连接符
2、gcc - "used" 属性