学习目标:
学会让自定义驱动提早加载的方法
了解能早加载的原理。
学习内容:
一、让你的驱动提早加载的方法
假如A 和 B两个驱动程序,A是你的,B是我的,我们的驱动程序都被编进了内核,怎样才能让你的A 提早加载呢?
方法挺简单:把你的驱动代码里面的驱动入口函数module_init 修改为arch_initcall。然后重新编译内核,系统运行后通过dmesg查看打印信息验证。
二、驱动能提早加载的原理
很多事做起来挺简单,但是要研究原理还是挺复杂的。
我们既然知道了要修改这个入口函数,那么这两个函数的区别在哪里?
1、module_init
首先我们看看module_init,module_init 是驱动在内核中启动的时候的入口函数,其原型可以在内核源码的include/linux/module.h 当中可以找到,如图:
可以看出,module_init 和 module_exit 的定义是一个条件编译,这个条件就是 MODULE 宏定义。注意:在我们的举例中只分析module_init,为啥子嘞?因为module_exit 在编译进内核的时候没有意义,因为静态编译的驱动无法卸载!
如果没有定义MODULE,则 module_init 为 __initcall(x); 如果定义了 MODULE,module_init 为 int init_module(void) __attribute__((alias(#initfn)));
module_init 的具体内容由MODULE 宏定义来决定,该宏定义在内核源码的顶层Makefile中,由具体的KBUILD_CFLAGS_KERNEL 和 KBUILD_CFLAGS_MODULE 两个宏来决定。如图:
如果把驱动编译进内核,则由KBUILD_CFLAGS_KERNEL 宏定义决定 MODULE;如果把驱动编译成模块,则由KBUILD_CFLAGS_MODULE 宏定义决定 MODULE。由图可以看出我们选择编进内核是没有声明 MODULE的。因此,我们的module_init的声明如下:
// include/linux/module.h
#define module_init(x) __initcall(x);
// include/linux/init.h
#define __initcall(fn) device_initcall(fn)
#define device_initcall(fn) __define_initcall(fn, 6)
#define __define_initcall(fn, 6) ____define_initcall(fn, id, .initcall##id)
#define ___define_initcall(fn, id, __sec) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(#__sec ".init"))) = fn;
// 注意:## 代表强制连接, #表示对这个变量替换后,用双引号引起来
由此处可以发现,我们平时使用的 module_init 在这里用到的是__define_initcall(fn, 6),这里fn就是我们的helloworld_init, 6表示的就是驱动程序在启动过程中加载的优先等级。
那我们要修改的arch_initcall 的优先等级是什么呢?
2、arch_initcall
直接上图片:
看到了么,arch_initcall(fn) ,声明时给的优先级那是3 。
那你又问了,你只说3、6了,那到底哪个高呢?有个口诀:数字越小越高,数字相同没s的高。
老赵问了,那你说说原因。
3、__define_initcall(fn, id)
接着看没看完的代码。
#define __define_initcall(fn, id) ___define_initcall(fn, id, .initcall##id)
#define ___define_initcall(fn, id, __sec) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(#__sec ".init"))) = fn;
可以看到,从 __define_initcall(fn, id)开始 就有了差别,这个差别就在于优先等级。这个有优先等级进一步就确定了___define_initcall(fn, id, __sec) 中的 __sec也就是上面 .initcall##id。
对比举例:
函数原型 | 举例 |
---|---|
module_init(fn) | module_init(helloworld_init) |
__initcall(fn) | __initcall(helloworld_init) |
device_initcall(fn) | device_initcall(helloworld_init) |
__define_initcall(fn, 6) | __define_initcall(helloworld_init, 6) |
____define_initcall(fn, id, .initcall##id) | ____define_initcall(helloworld_init, 6, .initcall6) |
static initcall_t __initcall_##fn##id __used \ __attribute__((__section__(#__sec ".init"))) = fn; | __initcall_helloworld_init6 __used \ __attribute__((__section__(".initcall6.init"))) = helloworld_init; |
从上表举例可以看出,当时用module_init宏定义以后,经过一些列的嵌套声明,最终声明了一个__initcall_hellowrold_init6函数指针变量(注意:initcall_t 是一个函数指针),将这个函数指针初始化为helloworld_init,编译的时候将这个函数指针放在.initcall6.init段中。
内核中有很多驱动都用了module_init,这些函数帧中会按照编译的先后顺序放在.initcall6.init段中。
除了module_init,还有其他的宏定义接口,他们的原型都是__define_initcall,区别就在于优先级别不一样,优先级不同函数指针存放的段也就不同了,例子中的A会存放在.initcall3.init 段中,当然系统启动时的启动顺序也就不一样了
当然,到这里还是没有说清楚到底是3优先级高还是6优先级高。
4、__initcall##level##_start 关联到 ".initcall##level##.init"段 和 ".initcall##level##s.init"段
打开include/asm-generic/vmlinux.lds.h文件
可以看到 INIT_CALLS 执行了参数分别是0-7的 INIT_CALLS_LEVEL 宏,因此展开后就是
#define INIT_CALLS \
__initcall_start= .; \
*(.initcallearly.init) \
__initcall0_start= .; \
*(.initcall0.init) \
__initcall0s_start= .; \
*(.initcall0s.init) \
__initcall1_start= .; \
*(.initcall1.init) \
__initcall1s_start= .; \
*(.initcall1s.init) \
__initcall2_start= .; \
*(.initcall2.init) \
__initcall2s_start= .; \
*(.initcall2s.init) \
__initcall3_start= .; \
*(.initcall3.init) \
__initcall3s_start= .; \
*(.initcall3s.init) \
__initcall4_start= .; \
*(.initcall4.init) \
__initcall4s_start= .; \
*(.initcall4s.init) \
__initcall5_start= .; \
*(.initcall5.init) \
__initcall5s_start= .; \
*(.initcall5s.init) \
__initcallrootfs_start= .;\
*(.initcallrootfs.init) \
__initcallrootfss_start= .; \
*(.initcallrootfss.init) \
__initcall6_start= .; \
*(.initcall6.init) \
__initcall6s_start= .; \
*(.initcall6s.init) \
__initcall7_start= .; \
*(.initcall7.init) \
__initcall7s_start= .; \
*(.initcall7s.init) \
__initcall_end = .;
所以宏INIT_CALLS 的作用就是相同等级的段会被放在同一块内存区域,不同等级的段的内存区域会按照登记的大小一次链接在一起。
其中__initcall0_start 变量记录initcall0.init段的起始地址,以此类推。
现在我们已经知道了这些段中存放了我们初始化的函数指针,并且这些段在内存区域的首地址也知道了。那么这些段中的函数是怎么被执行的呢?
5、执行.initcall##level##.init段中的函数
这些段首地址变量会在init/main.c中,通过extern 的方式引入,并将这些首地址存放在数组initcall_levels 中。如图:
追start_kernel函数,发现initcall_levels[] 数组会在上图中的do_initcalls(void)函数中被使用
现在可以确定了。for循环线从level数字小的先执行,那么数字小的优先级高。那没带s的呢?这个可以看看每个优先级段中的首地址是不带s的就可以确定了。
这一套降龙十八掌打完收功!