从根文件系统的挂载说起吧,使能了initd小文件系统的OS,最终会调用到populate_rootfs执行解压挂载的操作,搜索populate最终看到是rootfs_initcall被执行到的,那就从此处开始说起吧,看看内核是如何执行到这个函数的
一、initcall函数调用流程
调用链如下:
1 | |
1 | |
for 循环针对一个指针数组轮询,该数组是一个静态的 initcall_entry_t 类型,这些数据都存放在 __initdata 段。
指针数组的类型为 initcall_entry_t,是 initcall_t 的另一种叫法
继续来看下这个指针数组中的元素:__initcall0_start ~ __initcall_end,而这些元素的值在本c文件中已经声明:
1 | |
不难看出,数组 initcall_levels 中元素存放的是这些函数指针数组的首地址。
那么这些实际的指针数组是在哪里呢?从上文得知,initcall 函数都会被定义成 static initcall_t 类型,并且保存在 .initcall##level##.init 段中,那么 initcall_levels 与其是怎么关联的呢?
答案在 vmlinux.lds.h 中。
二、vmlinux.lds.h
1 | |
在这里首先定义了__initcall_start,将其关联到”.initcallearly.init”段。
然后对每个level定义了INIT_CALLS_LEVEL(level),将INIT_CALLS_LEVEL(level)展开之后的结果是定义__initcall##level##_start,并将__initcall##level##_start关联到”.initcall##level##.init”段和”.initcall##level##s.init”段。
1 | |
上面这些代码段最终在kernel.img中按先后顺序组织,也就决定了位于其中的一些函数的执行先后顺序。
在 kernel_init() 初始化完成后,会调用 free_initmem() 对内核 init 段的内存进行释放处理。即 .init 或者 .initcalls 段在内核启动完毕后,这个段中的内存会被释放掉。
三、do_initcall_level
do_initcall_level函数定义如下:
1 | |
do_initcall_level() 函数的参数 level 是之前是initcall_levels 数组的索引,从第0个开始。
这里再用一个 for 循环,跳到 initcall_levels 内部元素 (函数指针数组)进行轮询,fn 初始值为函数指针数组的起始地址,后面 fn++ 相当于函数指针 +1,即跳到下一个函数指针。
即for 循环中,根据传入的 level,确定需要轮询的 .initcall##level##.init 段的所有函数指针,一直到下一个 .intcall##(level+1)##.init 段。
另外,需要注意 do_one_initcall() 的参数就是获取函数指针的内容,而这个内容就是注册进来的 initcall 的实际初始化函数。
如上面的举例:
1 | |
这里最终就看成调用 do_one_initcall(populate_rootfs);
1 | |
四、initcall源码
上文提到过 Linux 对驱动程序提供静态编译进内核和动态加载两种方式,Linux 的 initcall 机制也是根据静态编译和动态加载的两种方式选择不同的编译、运行流程。
1 | |
MODULE 是在编译的时候,通过编译器参数来传入。例如,在编译 ko 时会使用如下两个编译选项,如果是链接到内核,则不会使用:
1 | |
通过 MODULE 的配置,选择静态编译还是动态加载。
本文将只分析静态编译流程
内核中定义的initcall如下:
1 | |
对于静态编译 initcall 接口如上,其中 pure_initcall() 只能在静态编译中存在。
当然,对于静态编译的驱动也可以调佣 module_init() 接口:
1 | |
module_init在内核中是有两种定义的,module_init定义在<linux/module.h>中,当包含此头文件的代码中,没有定义MODULE宏时,module_init定义为initcall形式
五、initcall级别
1 | |
__define_initcall实现:
1 | |
下文会继续细化分析,这里提前提示:
__define_initcall() 其实就是定义了一个 static initcall_t 的函数指针
1 | |
六、attribute详解
(1)、__used
1 | |
这是一种 attribute 修饰属性的一种,意思是告诉编译器:这个静态符号在编译的时候,即使没有使用也要保留,不能优化掉。
(2)、__attribute__ ((section(…)))
attribute 是 GNU C 的一大特色,可以用来修饰对象、函数、结构体类型等等。
这里用来修改 section,意思是将作用的函数放入指定的 section name 对应的段中。
(3)、 __stringify()
1 | |
将 __stringify() 中内容字符串化。
七、例子
以静态的module_init定义为例:
假如,我们在驱动使用如下接口:
1 | |
那么,在编译的时候编译器会通过 initcall 接口产生:
1 | |
linux 编译后的initcall 函数
查看编译好的 System.map:
1 | |
从 System.map 得知:
- 从 __initcall_start ~ __initcall_end 所有函数指针都是连续的,相差8 个字节;
- __initcall_start 就是第一个 early 级别的 initcall 函数指针,同理 __initcall0_start 就是第一个 level 0 级别的initcall 函数指针,以此类推;
- rootfs 级别的 initcall 函数是插在 level 5s 之后,level 6 级别之前;
- console 级别的函数在 level 7s 之后,__initcall_end 之前;
当然通过命令 readelf 或者 objdump (objdump -h vmlinux.o)都能看到各个字段
1 | |