本文Linux内核代码版本为:ARM Linux 4.1.15,源码阅读工具:Vscode
在分析struct device_node 如何转换成struct platform_device时,遇到个问题:customize_machine函数是怎么被调用的?搜索了内核代码,都没有发现调用的地方,后面才发现customize_machine函数下方有个arch_initcall(customize_machine);那接下来的就分析相关的*inticall。
一.initcall机制理解
linux对驱动程序提供静态编译进内核和动态加载两种方式,当我们想把一个驱动程序编译进内核,通常通常在一个*Init函数里加载相关的驱动程序,那问题来了,这个init函数怎么被调用?在哪里调用?内核维护一个类似table的东西,然后所有的驱动都放到这个table里,有新的驱动了,就新增一个table item,然后内核就使用一个for循环,遍历这个table,就可以实现自动调用相应的init函数。这样的方法很直观,但存在的问题是,现Linux支持的驱动已经超级超级多了,在某个地方维护一个这么大的table,随着驱动程序数量规模进一步增大,这个table就是一个灾难。因此,Linux内核的做法是在kernel image里维护一个section,这个section里存放很多init函数的地址,内核启动时,只需要在这个段地址处取出函数指针,一个个执行即可。但这样还存在一个问题就是:有些驱动模块存在依赖关系,驱动加载的先后顺序很重要,Linux当然考虑到了这点,在__define_initcall里就可以自定义优先级。
二.源码分析
arch_initcall:
include\linux\init.h1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19可以看到init.h里定义了很多的的**__define_initcall**,并且这些____define_initcall的第二个参数有的是纯数字,有的还带了s后缀,这个数字代表这个fn执行的优先级,数字越小,优先级越高,带s的fn优先级低于不带s的fn优先级。继续看__define_initcall
1
2
3
4我们一点点剖析____define_initcall,首先在____define_initcall函数体内,定义了一个static类型的变量,该变量类型是inticall_t,在
include\linux\init.h里可以看到initcall_t就是一个回调函数,定义的变量名字以____initcall为前缀,##在宏定义中的作用是符号连接,将多个符号(fn,id)连接成一个符号,并且不会把其字符串化。_used使用前提是在编译器编译过程中,如果定义的符号没有被引用,编译器就会对其进行优化,不保留这个符号,而____attribute((used))的作用是告诉编译器这个静态符号在编译的时候即使没有使用到也要保留这个符号。我们以arch_initcall(customize_machine)举例。1
2
3
4/*
* Used for initialization calls..
*/
typedef int (*initcall_t)(void);1
arch_initcall(customize_machine)展开后变成__define_initcall(fn, 3),最终展开成:
1
2static initcall_t __initcall_customize_machine3 = customize_machine
只不过特殊的是把customize_machine这个函数放到.initcall3.init ELF.section里,inticalls section具体放到哪里由链接器决定:
include/asm-generic/vmlinux.lds.h。每个level定义了INIT_CALLS_LEVEL(level),将INIT_CALLS_LEVEL(level)展开之后的结果我们在后续的分析可以用上。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18#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) = .;我们可以在System.map找到__initcall_customize_machine3:

因此我们可以得到这样一个结论:所有的*_initcall都会在linux kernel编译的时候展开,所有的initcalls都会被放在他们相应的sections,并且由于inincall是static修饰的同时已经初始化,那么从
.datasection可以获得所有的这些sections,同时Linux kernel在初始化的时候也知道去哪里可以找到特定的initcall来调用。接下来又有新的问题出现了,Linux kernel在哪个地方来调用这些initcall呢?1
2
3
4
5
6
7
8
9start_kernel // init/main.c
rest_init();
pid = kernel_thread(kernel_init, NULL, CLONE_FS);
kernel_init
kernel_init_freeable();
do_basic_setup();
do_initcalls();
for (level = 0; level < ARRAY_SIZE(initcall_levels) - 1; level++)
do_initcall_level(level); // 比如 do_initcall_level(3)1
2for (fn = initcall_levels[level]; fn < initcall_levels[level+1]; fn++)
do_one_initcall(*fn);do_initcall_level(level)还会调用do_one_initcall(*fn);最终调用的就是fn,而fn就是initcall_levels数组的一员,其实也就是个函数指针。路径:
init\main.c
1 | static initcall_t *initcall_levels[] __initdata = { |
这里就又产生了一个疑问,在添加initcall时,实际上是把fn放到了.initcall” #id “.init section,但在init/main.c里我们却是取出initcall_levels每一个成员,然后调用,那initcall_levels是怎么和initcall” #id “.init section关联起来的?之前我们已经说过会把各个initcall放到链接器决定的位置,由之前分析lds.h可知,对于我们要添加的函数customize_machine,它对应的Symbol是__initcall_customize_machine3,这个Symbol可以在.initcall.3.init找到。对于__initcall3_start,由lds.h可知链接器会把其关联到.initcall##level##.init,也即.initcall3.init,这不正好是我们的函数指针保存的位置嘛,存放的就是customize_machine3的地址。
总结
开发者想要实现自己的驱动在系统启动的时候启动一些服务的话,需要调用xxx_init来把一些函数添加到系统,然后内核启动的时候调用do_initcall,根据函数指针数组找到相应函数指针(从相应的section里可以找到函数指针),然后调用即可。