initcall简介
linux kernel中包含了许多独立的features,这些features可以在配置内核时enable或者disable。而这些features一般都会有自己的初始化函数,按照传统的写法,对这些的features的初始化会这样做:
- features_init()
- {
- #ifdef FEATURE_A
- feature_a_init();
- #endif
- #ifdef FEATURE_B
- feature_b_init();
- #endif
- #ifdef FEATURE_C
- feature_c_init();
- #endif
- ......
- }
这种做法的问题在于,linux kernel拥有无数features,这就意味着这里要有无数的”#ifdef … #endif”。这一方面使得代码看起来很dirty,另一方面,每个features的作者都需要在这里额外的维护一小段代码,这降低了整个代码的可维护性和正交性。
为了解决这个问题,Linux Kernel 2.3.13引入了initcall的概念。
initcall在概念上很简单。
- kernel image使用ELF格式
- 每个feature维护一个指向自己初始化函数的函数指针,使用gcc的扩展关键字__attribute__可以把这个指针放到一个特定的ELF Section中,比方说.initcall。在编译时,只有enable了的feature才会被编译,它维护的函数指针才会放到最后生成的ELF文件的.initcall中。
- 使用ld script,可以“手工”向ELF文件中插入symbol。可以通过适当插入symbol的来让我们的C code知道.initcall这个section的起始地址和结束地址,这样我们的C code就可以访问这个section中的数据。
- 现在我们可以访问到.initcall中的函数指针,遍历调用他们即可。
下面我们用一个简单的例子来演示一下initcall的实现。首先为了能在C code中定位.initcall的起始、终止地址,我们不能使用默认的ld script,要对他做一点点修改。可以用”ld -verbose”命令获得ld默认使用的ld script,对它做简单修改后我们得到:
文件比较长,但是这blog的文件上传功能貌似有点问题,所以还是直接贴上来了。不过这个文件中只有62-64这3行是我添加的,具体的说是以下三行:
__initcall_begin = .;
.initcall : { *(.initcall) }
__initcall_end = .;
其他全部是ld默认script的内容。ld script的内容这里不说太多,不会的可以查阅ld的info page。
然后是一个简单的c程序initcall.c:
- #include <stdio.h>
- #define __init __attribute__ ((section (".initcall")))
- typedef int (*initcall_t)(void);
- int ic1(void)
- {
- return 1;
- }
- int ic2(void)
- {
- return 2;
- }
- int ic3(void)
- {
- return 3;
- }
- initcall_t __init ic_1 = ic1;
- initcall_t __init ic_2 = ic2;
- initcall_t __init ic_3 = ic3;
- extern initcall_t __initcall_begin, __initcall_end;
- int main(void)
- {
- initcall_t *initcall;
- for(initcall=&__initcall_begin; initcall<&__initcall_end; initcall++) {
- printf("%d\n", (*initcall)());
- }
- return 0;
- }
编译:
gcc -Tinitcall.lds initcall.c
执行:
$ ./a.out
1
2
3
参考资料:
- info gcc “C Extensions” “Variable Attributes”
- info ld Scripts Assignments
//======================================================
分析kernel的initcall函数
Author: Dongas
Data: 08-07-15
先来看看这些initcall函数的声明:
/* 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.
*/
#define __define_initcall(level,fn) \
static initcall_t __initcall_##fn __attribute_used__ \
__attribute__((__section__(".initcall" level ".init"))) = fn
#define core_initcall(fn) __define_initcall("1",fn)
#define postcore_initcall(fn) __define_initcall("2",fn)
#define arch_initcall(fn) __define_initcall("3",fn)
#define subsys_initcall(fn) __define_initcall("4",fn)
#define fs_initcall(fn) __define_initcall("5",fn)
#define device_initcall(fn) __define_initcall("6",fn)
#define late_initcall(fn) __define_initcall("7",fn)
#define __initcall(fn) device_initcall(fn)
#define __exitcall(fn) \
static exitcall_t __exitcall_##fn __exit_call = fn
#define console_initcall(fn) \
static initcall_t __initcall_##fn \
__attribute_used__ __attribute__((__section__(".con_initcall.init")))=fn
#define security_initcall(fn) \
static initcall_t __initcall_##fn \
__attribute_used__ __attribute__((__section__(".security_initcall.init"))) = fn
#define module_init(x) __initcall(x); ß从这里知道module_init的等级为6,相对靠后
#define module_exit(x) __exitcall(x);
可以发现这些*_initcall(fn)最终都是通过__define_initcall(level,fn)宏定义生成的。
__define_initcall宏定义如下:
#define __define_initcall(level,fn) \
static initcall_t __initcall_##fn __attribute_used__ \
__attribute__((__section__(".initcall" level ".init"))) = fn
这句话的意思为定义一个initcall_t型的初始化函数,函数存放在.initcall”level”.init section内。.initcall”level”.init section定义在vmlinux.lds内。
/* arch/arm/kernel/vmlinux.lds */
……
__initcall_start = .;
*(.initcall1.init)
*(.initcall2.init)
*(.initcall3.init)
*(.initcall4.init)
*(.initcall5.init)
*(.initcall6.init)
*(.initcall7.init)
__initcall_end = .;
……
正好包括了上面init.h里定义的从core_initcall到late_initcall等7个level等级的.initcall”level”.init section. 因此通过不同的*_initcall声明的函数指针最终都会存放不同level等级的.initcall”level”.init section内。这些不同level的section按level等级高低依次存放。
下面我们再来看看,内核是什么时候调用存储在.initcall”level”.init section内的函数的。
内核是通过do_initcalls函数循环调用执行initcall.init section内的函数的,流程如下:
start_kernel -> rest_init -> kernel_thread -> init -> do_basic_setup -> do_initcalls
kernel将initcall 以__early_initcall_end为分界线分成两部分: 用于SMP的initcall和用于(SMP/非SMP)的initcall。
代码如下:
//do_pre_smp_initcalls
static void __init do_pre_smp_initcalls(void)
{
initcall_t *fn;
for (fn = __initcall_start; fn < __early_initcall_end; fn++)
do_one_initcall(*fn);
}
//do_basic_setup() -> do_initcalls()
static void __init do_initcalls(void)
{
initcall_t *fn;
for (fn = __early_initcall_end; fn < __initcall_end; fn++)
do_one_initcall(*fn);
/* Make sure there is no pending stuff from the initcall sequence */
flush_scheduled_work();
}
|
这里要分析两个函数: kernel_thread和do_initcalls,这两个函数都定义在init/main.c内
1) kernel_thread
1.static void noinline rest_init(void)
2. __releases(kernel_lock)
3.{
4. system_state = SYSTEM_BOOTING_SCHEDULER_OK;
5.
6. kernel_thread(init, NULL, CLONE_FS | CLONE_SIGHAND);
7. numa_default_policy();
8. unlock_kernel();
9.
10. /*
11. * The boot idle thread must execute schedule()
12. * at least one to get things moving:
13. */
14. __preempt_enable_no_resched();
15. schedule();
16. preempt_disable();
17.
18. /* Call into cpu_idle with preempt disabled */
19. cpu_idle();
20.}
第6行通过kernel_thread创建一个内核线程执行init函数。(其实这里创建的即Linux的1号进程(init进程), 为linux中所有其他进程的父进程,有兴趣的可以自己查资料)
2) do_initcalls
1.static void __init do_initcalls(void)
2.{
3. initcall_t *call;
4. int count = preempt_count();
5.
6. for (call = __initcall_start; call
7. ……
8. result = (*call)();
9. ……
10. }
11.}
其中, initcall_t类型如下:
typedef int (*initcall_t)(void);
__initcall_start和__initcall_end定义在vmlinux.lds内,表示initcall section的起始和结束地址。
/* arch/arm/kernel/vmlinux.lds */
……
__initcall_start = .;
*(.initcall1.init)
*(.initcall2.init)
*(.initcall3.init)
*(.initcall4.init)
*(.initcall5.init)
*(.initcall6.init)
*(.initcall7.init)
__initcall_end = .;
……
因此,上面6-10行代码的作用为按initcall level等级的顺序,依次循环调用预先存储在initcall section内的所有各个级别的初始化函数。这样,kernel的initcall函数的原理我们就搞清楚了。
最后要注意的是rest_init是在start_kernel函数内最后部分才被调用执行的,rest_init前包含了kernel一系列的初始化工作。另外,这些不同level等级的initcall.init section本身有一定的执行顺序,因此如果你的驱动依赖于特定的执行顺序的话需要考虑到这一点。
阅读(1520) | 评论(0) | 转发(0) |