让你的驱动提早加载的方法和原理

学习目标:

学会让自定义驱动提早加载的方法

了解能早加载的原理。


学习内容:

一、让你的驱动提早加载的方法

        假如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的就可以确定了。

        这一套降龙十八掌打完收功!


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值