module_init
module_init解析及内核initcall的初始化顺序
module_init这个函数对做驱动的人来说肯定很熟悉,这篇文章用来跟一下这个函数的实现。
在include/linux/init.h里面有module_init的定义,自然,因为一个module可以在内核启动时自动加载进内核,也可以由我们手动在需要时加载进内核,基于这种场景,内核使用了MODULE这个宏,见代码:
复制代码
#ifndef MODULE
#ifndef __ASSEMBLY__
... #define __define_initcall(level,fn,id) \ static initcall_t __initcall_##fn##id __attribute_used__ \
__attribute__((__section__(".initcall" level ".init"))) = fn #define pure_initcall(fn) __define_initcall("0",fn,0) #define core_initcall(fn) __define_initcall("1",fn,1) #define core_initcall_sync(fn) __define_initcall("1s",fn,1s) #define postcore_initcall(fn) __define_initcall("2",fn,2) #define postcore_initcall_sync(fn) __define_initcall("2s",fn,2s) #define arch_initcall(fn) __define_initcall("3",fn,3) #define arch_initcall_sync(fn) __define_initcall("3s",fn,3s) #define subsys_initcall(fn) __define_initcall("4",fn,4) #define subsys_initcall_sync(fn) __define_initcall("4s",fn,4s) #define fs_initcall(fn) __define_initcall("5",fn,5) #define fs_initcall_sync(fn) __define_initcall("5s",fn,5s) #define rootfs_initcall(fn) __define_initcall("rootfs",fn,rootfs) #define device_initcall(fn) __define_initcall("6",fn,6) #define device_initcall_sync(fn) __define_initcall("6s",fn,6s) #define late_initcall(fn) __define_initcall("7",fn,7) #define late_initcall_sync(fn) __define_initcall("7s",fn,7s) #define __initcall(fn) device_initcall(fn) #define module_init(x) __initcall(x); #else /* MODULE */ ... #define module_init(initfn) \ static inline initcall_t __inittest(void) \
{ return initfn; } \ int init_module(void) __attribute__((alias(#initfn)));
...
复制代码
当我们使用make menuconfig来配置内核时,将某个module配置为m时,MODULE这个宏就被定义了,而当配置为y时,则没有定义,具体的实现在kernel的根Makefile(-DMODULE)里。
现在我们先看下第一种情况,即把module配置为m的情况,即else分支的代码。
先看下initcall_t的定义:
typedef int (*initcall_t)(void);
它是一个接收参数为void, 返回值为int类型的函数指针。这样就明白了,其实前两句话只是做了一个检测,当你传进来的函数指针的参数和返回值与initcall_t不一致时,就会有告警。
重点在第三句,是使用alias将initfn变名为init_module,我们知道,kernel 2.4版本之前都是用init_module来加载模块的。这样做应该是为了不用修改load module的那块代码吧。
当我们调用insmod将module加载进内核时,会去找init_module作为入口地址,即是我们的initfn, 这样module就被加载了。
取nvme.ko为例,我们可以通过objdump -t nvme.ko 查看该模块的符号表,发现init_module和nvme_init指向同一个偏移量。如下:
现在看第二种情况,即我们选择将模块编进内核,让它随内核启动而加载。
这种情况下module_init最终会调用__define_initcall宏,这个宏的作用就是将我们的初始化函数放在".initcall" level ".init"中。
在这里是.initcall6.init, 它的位置可以在Vmlinux.lds.h里面找到:
复制代码
#define INITCALLS \
*(.initcall0.init) \ *(.initcall0s.init) \ *(.initcall1.init) \ *(.initcall1s.init) \ *(.initcall2.init) \ *(.initcall2s.init) \ *(.initcall3.init) \ *(.initcall3s.init) \ *(.initcall4.init) \ *(.initcall4s.init) \ *(.initcall5.init) \ *(.initcall5s.init) \ *(.initcallrootfs.init) \ *(.initcall6.init) \ *(.initcall6s.init) \ *(.initcall7.init) \ *(.initcall7s.init)
复制代码
而INITCALL可以在vmlinux.lds.S里面找到:
复制代码
.init.text : AT(ADDR(.init.text) - LOAD_OFFSET) {
__init_begin = .;
_sinittext = .; *(.init.text)
_einittext = .;
}
.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) { *(.init.data) }
. = ALIGN(16);
.init.setup : AT(ADDR(.init.setup) - LOAD_OFFSET) {
__setup_start = .; *(.init.setup)
__setup_end = .;
}
.initcall.init : AT(ADDR(.initcall.init) - LOAD_OFFSET) {
__initcall_start = .; INITCALLS __initcall_end = .;
}
.con_initcall.init : AT(ADDR(.con_initcall.init) - LOAD_OFFSET) {
__con_initcall_start = .; *(.con_initcall.init)
__con_initcall_end = .;
}
复制代码
上面贴出来的代码是系统启动时存放初始化数据的地方,执行完成后不再需要,会被释放掉。根据上面的内存布局,可以列出初始化宏和内存的对应关系:
复制代码
_init_begin -------------------
| .init.text | ---- __init |-------------------|
| .init.data | ---- __initdata
_setup_start |-------------------|
| .init.setup | ---- __setup_param
__initcall_start |-------------------|
| .initcall1.init | ---- core_initcall |-------------------|
| .initcall2.init | ---- postcore_initcall |-------------------|
| .initcall3.init | ---- arch_initcall |-------------------|
| .initcall4.init | ---- subsys_initcall |-------------------|
| .initcall5.init | ---- fs_initcall |-------------------|
| .initcall6.init | ---- device_initcall |-------------------|
| .initcall7.init | ---- late_initcall
__initcall_end |-------------------|
| |
| ... ... ... |
| | __init_end -------------------
复制代码
而各个initcall被调用的地方在kernel_init-》do_basic_setup-》do_initcalls里面:
复制代码
static void __init do_initcalls(void)
{
initcall_t *call; int count = preempt_count(); for (call = __initcall_start; call < __initcall_end; call++) {
ktime_t t0, t1, delta; char *msg = NULL; char msgbuf[40]; int result; if (initcall_debug) {
printk("Calling initcall 0x%p", *call);
print_fn_descriptor_symbol(": %s()",
(unsigned long) *call);
printk("\n");
t0 = ktime_get();
}
result = (*call)();
...
}