在内核代码中经常可以看到类似fs_initcall\early_initcall\late_initcall这样的宏,这些宏有什么作用?如何实现的?下面来具体分析
在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)
可以看到它们利用了__define_initcall宏:
/* 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; \
LTO_REFERENCE_INITCALL(__initcall_##fn##id)
从上面的注释信息可以看出:initcalls这类函数会被分别放置到不同的subsections中,subsections中的先后顺序由linker来决定(但是subsections之间的顺序是能够确定的,由vmlinux.lds.S指定);此外id的作用是防止duplicate-symbol错误,如果没有这个参数,那么两类不同的initcall使用同一个fn的话会发生symbol重复定义的错误。__define_initcall宏
的作用是定义了一个initcall_t类型(函数指针)的变量,并将它放在名为.initcall#id.init
的subsection中。该变量指向函数fn。
这样一来的话,这些subsection中存储的都是些函数指针,可以通过这些值来调用对应的函数。那么何时调用这些函数?
首先,我们需要理解几个变量:
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开头的变量值实际上是在link阶段就被确定了。在内核的链接脚本中会记录这几个subsection的起始、结束地址,分别存在上述变量中。这样的话就可以在程序中通过这些变量来访问这些subsections。
在内核启动初始化过程中会有下列流程:
start_kernel --> rest_init
在rest_init函数中会创建一个内核线程,执行kernel_init
,
kernel_init --> kernel_init_freeable --> do_pre_smp_initcalls
|---> do_basic_setup --> do_initcalls
early_initcall
需要在SMP初始化之前被调用,其在do_pre_smp_initcalls
函数中被调用:
static void __init do_pre_smp_initcalls(void)
{
initcall_t *fn;
for (fn = __initcall_start; fn < __initcall0_start; fn++)
do_one_initcall(*fn);
}
这个for循环只对early_initcall定义的函数进行调用,do_one_initcall
函数中会调用fn指向的函数。
同理,在do_initcalls
函数中会对其他initcall函数进行调用。
我们现在知道了上面的initcall是在内核初始化过程中被调用的,因此使用initcall定义的函数一定是built-in的。
在模块编程的时候会使用到module_init
,它也有两种定义,在include/linux/module.h
中:
#ifndef MODULE
......
#define module_init(x) __initcall(x);
......
#else /* MODULE */
......
/* Each module must use one module_init(). */
#define module_init(initfn) \
static inline initcall_t __inittest(void) \
{ return initfn; } \
int init_module(void) __attribute__((alias(#initfn)));
......
#endif
对于built-in的模块,其module_init
对应的是__initcall
,最终对应的是device_initcall
(在include/linux/init.h
中):
#define __initcall(fn) device_initcall(fn)
这样,在内核初始化过程中就会调用到模块的初始化函数。
对于load的模块,我们暂时先不关心,以后分析模块子系统时再看。
上面提到过各个subsections之间的顺序在链接脚本中指定的,我们现在就来看看链接脚本中有关的内容。在arch/x86/kernel/vmlinux.lds.S
中有以下内容:
......
#include <asm-generic/vmlinux.lds.h>
......
SECTIONS
{
......
INIT_DATA_SECTION(16)
......
}
进入include/asm-generic/vmlinux.lds.h
文件,我们可以看到INIT_DATA_SECTION
的定义:
#define INIT_DATA_SECTION(initsetup_align) \
.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) { \
......
INIT_CALLS \
......
}
而INIT_CALLS
的定义如下:
#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) = .;
这个宏中的顺序就是在最终内核镜像中subsections之间的顺序,并且可以看到对于每个subsection都导出了xxx_start值,而整个initcall的范围是_initcall_start、_initcall_end,这些变量在上面处理initcall的时候会被使用到。
__init
也是在include/linux/init.h
中定义的:
#define __init __section(.init.text) __cold notrace
通过__init
定义的函数表示是在内核初始化期间被调用的,当初始化完成之后会将他们释放掉。它指明将该函数放到.init.text
section中。那么这个section放置在什么地方?
在arch/x86/kernel/vmlinux.lds.S
中有以下内容:
......
#include <asm-generic/vmlinux.lds.h>
......
SECTIONS
{
......
INIT_TEXT_SECTION(PAGE_SIZE)
......
INIT_DATA_SECTION(16)
......
}
INIT_DATA_SECTION
我们在上面已经分析过了,这里还有INIT_TEXT_SECTION
,这个宏在vmlinux.lds.h
中定义:
......
#define INIT_TEXT \
*(.init.text) \
MEM_DISCARD(init.text)
......
#define INIT_TEXT_SECTION(inittext_align) \
. = ALIGN(inittext_align); \
.init.text : AT(ADDR(.init.text) - LOAD_OFFSET) { \
VMLINUX_SYMBOL(_sinittext) = .; \
INIT_TEXT \
VMLINUX_SYMBOL(_einittext) = .; \
}
......
可以看到上述是对.init.text section如何放置的描述,并且导出了_sinittext、_einittext分别表示此section的起始、结束地址。
链接脚本展开后最终的效果如下:
* SECTIONS
* {
* . = START;
* __init_begin = .;
* HEAD_TEXT_SECTION
* INIT_TEXT_SECTION(PAGE_SIZE)
* INIT_DATA_SECTION(...)
* PERCPU_SECTION(CACHELINE_SIZE)
* __init_end = .;
*
* _stext = .;
* TEXT_SECTION = 0
* _etext = .;
*
* _sdata = .;
* RO_DATA_SECTION(PAGE_SIZE)
* RW_DATA_SECTION(...)
* _edata = .;
*
* EXCEPTION_TABLE(...)
* NOTES
*
* BSS_SECTION(0, 0, 0)
* _end = .;
*
* STABS_DEBUG
* DWARF_DEBUG
*
* DISCARDS // must be the last
* }
我们现在只关注和init section相关的,即被_init_begin和_init_end所包围的内容,这篇我们关注的initcalls和_init可以看到都包括在其中。
在上面我们提到了内核初始化尾声创建了一个kernel_init
内核线程,其中会执行free_initmem
函数:
void free_initmem(void)
{
free_init_pages("unused kernel",
(unsigned long)(&__init_begin),
(unsigned long)(&__init_end));
}
可以看到它会将_init_begin和_init_end之间的内存释放掉。