内核模块加载顺序的控制

前言

看i915显卡驱动时,发现i915模块代码假定自己会在intel-agp的模块加载之后执行,我就不理解,两个模块,怎么保证intel-agp模块执行一定在前呢?

百度后,没看对这个知识点说的比较透的,于是,有了本文。

注意,本文说的模块只是种通俗的说法,实际体现为自动被调用的功能,比如驱动的注册,子系统的注册等,不是编译成“模块”的模块。

本文中的模块都是被编译进内核的模块,当然也可以被编译成“模块”。编译成“模块”的模块加载顺序不在本文讨论范围。

理解本文需要一点内核Makefile基础,一点ELF文件格式基础,一点链接文件语法基础,不要咬文嚼字。

相关文档链接如下:

ELF文件格式:搜索“Executableand Linking Format Specification”链接

Makefile:搜索“GNUmake中文手册链接

Ldscript: 搜索”usingld”链接

模块的顺序声明
模块要被加载,必须先让外界知道它的存在。

实际使用中,模块通过一个声明告知外界自己的存在,没有这个声明,模块就不会被自动调用。一般设备驱动的声明函数是module_init,少有例外。非驱动的模块用的声明函数各不相同,看情况选择,能用的函数都在include/linux/init.h中。

这里简单分析下module_init这个宏,层层定义如下:

#definemodule_init(x) __initcall(x);

#define__initcall(fn) device_initcall(fn)

#definedevice_initcall(fn) __define_initcall(“6”,fn,6)

到这里,就等价与module_init(x)<==> __define_initcall(“6”,fn,6)。

继续看__define_initcall的定义:

#define__define_initcall(level,fn,id) \

staticinitcall_t _initcall##fn##id __used \

attribute((section(".initcall"level “.init”))) = fn

把宏展开后,modules_init(demo_init)就是:

static initcall_t__initcall_demo_init6 __used

attribute((setction(“.initcall6.init”)))= demo_init;

这看起来比较复杂,其中initcall_t是个函数指针类型:

typedef int(*initcall_t)(void);

如果去掉属性字段,这个定义看起来会清楚些,就是:

static initcall_t__initcall_demo_init6 = demo_init;

这就明白了,modules_init()是个函数指针变量的定义并赋值。

这条语句的属性字段(attribute)要求编译器在此c文件编译生成的.o文件中制造出一个叫initcall6.init的section,setion的内容就是这个demo_init函数的地址(在.o阶段应该是程序地址,链接后是虚拟地址,对于.ko文件这个段无用)。

这个section的名字是臆造的,后续链接内核的时候会用到。

section是ELF文件格式中的一个概念,不看ELF文件格式的同学可将就将section理解为平时说的代码段,BSS段中的段。ELF文件格式规范链接

现在知道了,module_init函数就是让编译器在本c文件生成的.o文件中生成一个叫做.initcall.6.init的section,仅此而已。

模块加载顺序控制的原理
归根到底,哪个模块先执行,内核说了算!

内核通过调用顺序控制哪个模块先执行。

内核有四处集中调用模块初始化函数,调用实际就是将内核二进制文件中的一部分区域解释为函数指针数组,依此调用。

这些函数指针数组是链接器根据链接脚本,按照不同的section名摆放的。初始化函数指针数组相关的section有18组(这18组链接后成为4组)。每个最终链接到内核的.o文件中如果有这18组之一的section名,则对应名称的section内容就会放入最终vmlinux中的相应函数指针函数中。

至于不同.o文件但同一名称的section在最终内核中的顺序,由链接顺序决定,先链接的先被调用。

链接的顺序由Makefile决定,可如下理解,Makefile在调用链接器的时候传给ld一串.o文件,这些.o文件的顺序就是链接的顺序。

核心的东西就是这些。

总的来说,内核模块加载顺序被五个因素控制,分别是:

内核模块调用框架,

内核链接脚本,

Makfile框架,

模块代码声明,

编译器。

后面对每个因素分别解释。

实际应用中,用于控制模块顺序的是其中两个因素:Makfile框架和模块代码声明。

内核模块调用框架控制的
内核在初始化过程中会调用四次初始化函数数组,按照顺序分别是:console_init、security_init、do_pre_smp_initcalls和do_basic_setup,都在init/main.c文件中。

以console_init函数为例看下这些函数的定义:

void __initconsole_init(void)

{

initcall_t *call;

call =__con_initcall_start;

while (call <__con_initcall_end) {

(*call)();

call++;

}

}

功能就是代码将__con_initcall_start到__con_initcall_end之间,也就是内存的两个地址间的内容解释为函数指针数组,依次调用每个指针对应的函数。由于是直接执行,可见这个地址是虚拟地址。内核编译是指定起始地址的,编译后的程序地址实际就是虚拟地址。__con_initcall_start和__con_initcall_end是链接脚本中定义的变量,链接的时候链接器会解析并替换成地址。

另三个的调用性质是一样的,分别调用的函数指针数组位于__security_initcall_start到__security_initcall_end,__initcall_start到__early_initcall_end,__early_initcall_end到__initcall_end之间,本文就称这四个段的内容为函数指针数组。

因此,不论物理上你放在哪里,谁先调用,内核说了算,他要先调用哪个函数指针数组哪个就被先调用。

所以可以说,在内核调用框架不变的前提下,con_initcall数组中的最先被调用,serurity_initcall数组中的随后被调用,initcall数组的最后被调用。

内核链接脚本控制的
上面说明了,在哪个函数数组中决定了函数被调用的先后,那么这些函数指针数组是怎么来的?

内核链接脚本控制的。

对于x86,就是arch/x86/kernel/vmlinux.lds文件,编译后链接前会生成这个文件。

链接脚本是主动控制链接行为的配置文件,由ld命令的-T选项使能生效。这个文件的有自己的一套语法,具体见Usingld中的LinkerScript一章。大致能看懂就行了,我们只关心和那四个函数指针数组相关的部分。

打开这个文件,搜索initcall。分离出我们需要的内容如下:

.init.data (此处省略);

__initcall_start= .; *(.initcallearly.init)

__early_initcall_end= .;

(.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_end =.;

__con_initcall_start= .; *(.con_initcall.init) __con_initcall_end = .;

__security_initcall_start= .; *(.security_initcall.init) __security_initcall_end = .;

我们关心的四段函数指针数组的来源用不同颜色标明了。

链接过程中有个阶段是不断拷贝被链接文件的section到最终的二进制文件中,这个过程就被vmlinux.lds文件控制,顺便说下,最终文件虚拟地址的起始值也是这文件指定的。

在开始生成.init.data这个section之前,前面已经放置了代码段等。

__initcall_start= .;

这句中的‘.’表示当前已经码放到的地址,同时将其赋值给__initcall_start这个变量。

__initcall_start= .; *(.initcallearly.init)

紧接的一句表示从这个地址开始,将待链接文件中所有以.initcallearly.init结尾的section的内容都拷贝到这个地址开始的空间上去。

__early_initcall_end= .;

(.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_end = .;

这句比较有内容,就是将待链接文件中所有以.initcall[0-7][s].init结尾的section都拷贝到这个地址开始的空间上去,当然严格注意顺序。用module_init声明的模块的编译出的.o文件都有”.initcall6.init”这个段,因此都会被放入__early_initcall_end开始的空间中,当然是按照顺序在所有”initcall[0-5][s].init”的section放完之后才放。

模块代码控制的
现在知道模块调用顺序大体上是由声明时定下的section名确定的,那么声明的时候有哪些选择呢?

能用的都在init.h中定义,可用如下命令查看:

catinclude/linux/init.h |grep initcall |grep define

实际不同的声明函数本质和module_init一样,只是生成的初始化调用section的名字不同,section名直接导致最后被链接器放置在不同的位置。

自己写代码的时候偶尔也会遇到本模块依赖另一个模块的情形,如果对方的声明级别已经确保先于自己,当然就没问题。如果和自己平级,那么看是否能降低自己的声明级别,比如对方是module_init的,实际是在initcall6.init的section,因此我们的可以使用这个之后的,比如6s(device_initcall_sync),7(device_initcall)。

当然,这样感觉好像让驱动受委屈了。

所以有的人拽啊,用Makefile来控制。

Makefile 框架控制的
内核的Makefile框架设计还是比较复杂的,复杂在你就是懂Makefile也不一定能准确地理解它。因为实际上内核的Makefile相当于封装了一层,非常强大。之前写过一个专门的分析文档,值得一看。

这里我们只关心一点,同样级别的section,比如都是modele_init调用的,最终在内核二进制文件中的先后顺序是怎么确定的。不可避免是要引入写内核Makefile的东西了。

最终vmlinux链接调用如下的命令完成:

$(LD)$(LDFLAGS) $(LDFLAGS_vmlinux) -o $@ \

-T$(vmlinux-lds)$(vmlinux-init) \

–start-group$(vmlinux-main)–end-group \

$(filter-out$(vmlinux-lds) $(vmlinux-init) ( v m l i n u x − m a i n ) v m l i n u x . o F O R C E , (vmlinux-main) vmlinux.o FORCE , (vmlinuxmain)vmlinux.oFORCE,^)

也就是最终出现在vmlinux-init中的待链接文件(.o文件)会比vmlinux-main的先链接到vmlinux中,vmlinux-init和vmlinux-main定义如下:

vmlinux-init :=$(head-y) $(init-y)

vmlinux-main :=$(core-y) $(libs-y) $(drivers-y) $(net-y)

也就是最终链接的顺序是:head-y,init-y,core-ylib-y,drivers-y,net-y。由于中间转换了一次,上面这些Makefile的全局变量最后都是少数xxx/built-in.o的组合。

下面以drviers-y为例,看下drivers-y中包含的.o文件时如何排序的。

drivers-y在成为一堆built-in.o前的内容为:

drivers/ sound/ firmware/ arch/x86/pci/ arch/x86/power/ arch/x86/video/

这是由主Makefile和其它部分目录下的Makefile的drivers-y赋值语句定义的。这些目录和子目录下的所有Makefile的从叶子Makefile起,依此或者乱需被编译成为.o,然后成为叶子子目录下的built-in.o。然后然后和上层的.o结合成为上层的built-in.o,.o和.o结合可以简单理解为同类型section的精确合体,两个文件的同名section最终会合并成一个section。

最终drivers-y成为多个built-in.o:

drivers/built-in.o sound/built-in.o firmware/built-in.o

arch/x86/pci/built-in.o arch/x86/power/built-in.o arch/x86/video/built-in.o

这个就是drivers-y最终给ld的链接文件列表的顺序。所以,如果是来自drivers-y的目录或子目录下的.o文件,在drivers下的先链接入最终的vmlinux,在arch/x86/video下的最后链接入vmlinux,也就是如果这两个目录下都有module_init声明的模块,在drivers下的最终会先被调用。

那么同样在drivers下的最终链接顺序是如何的呢?

由Makefile的行序确定。

以drivers/Makefile为例:开头几行如下:

obj-y += gpio/

obj-$(CONFIG_PCI) += pci/

obj-$(CONFIG_PARISC) += parisc/

obj-$(CONFIG_RAPIDIO)+= rapidio/

那么在gpio目录下的.o文件必定比pci目录下的.o文件先被链接到drivers/built-in.o中。有些人就利用这种特性,drivers/Makefile下就有例子:

obj-y += char/

# gpu/ comes afterchar for AGP vs DRM startup

obj-y += gpu/

看到注释了吧,gpu目录是特意放在char的下面一行是有目的的!

因为gpu目录下drm代码中会对到char目录下的agp代码有依赖。这样限定Makefile中两行的顺序后,可以确保都是module_init声明的模块,char下的模块先运行,gpu的后运行。

那如果char/agp/下的intel-agp编译成模块,gpu/drm/下的i915编译进内核不就挂了?

是的,所以这不会出现。Kconfig文件中,i915的编译必须依赖intel-agp的编译为y。

基于Makefile的顺序的内容就是这些,一句话就是:部分大目录链接顺序由Makefile中的赋值语句决定,其余目录的链接顺序由Makefile行序确定。

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值