如何管理驱动模块
由于Linux驱动模块众多,系统对模块加载顺序有要求,一些基础模块在系统启动时需要很早就被加载;开发者加入自己的模块时,需要维护一个模块初始化列表,上面两方面的做起来很困难,为了科学地管理这些模块,首先要解决两个问题:
- 如何方便开发者快捷加入自己的模块
- 如何管理模块的加载顺序
Linux 内核开发者是怎么实现的呢?在内核镜像文件中,自定义了一个段,这个段里面专门用来存放这些初始化函数的地址,内核启动时,只需要在这个段地址处取出函数指针,依次执行即可。
对模块的开发者,Linux内核提供了统一的宏定义接口,驱动开发者只需要将驱动程序用这些宏定义来修饰,这个模块的初始化函数接口的指针就被自动添加到了上述的段中,开发者完全不需要关心这个实现的细节。
对于各种各样的驱动而言,会存在一定的依赖关系,需要遵循先后顺序来进行初始化,考虑到这个问题,Linux内核开发者也对这一部分的初始化顺序做了分级处理。
Linux驱动模块的加载方式
Linux 驱动模块有两种加载方式,一是静态编译链接进内核,在系统启动过程中进行初始化;另外一是编译成可动态加载的module,通过insmod动态加载重定位到内核。
Linux 使用宏定义
Linux提供了一组宏定义对模块进行静态和动态加载,同时对不同的模块加载顺序做了处理,提供了不同的宏定义方法。这组宏定义在路径/kernel/include/linux/init.h中。
#ifndef MODULE
#define __define_initcall(level,fn,id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" level ".init"))) = fn
#define early_initcall(fn) __define_initcall("early",fn,early)
#define pure_initcall(fn) __define_initcall("0",fn,0)
#define core_initcall(fn) __define_initcall("1",fn,1)
#define core_initcall_sync(fn) __define_initcall("1s",fn,1s)
#define postcore_initcall(fn) __define_initcall("2",fn,2)
#define postcore_initcall_sync(fn) __define_initcall("2s",fn,2s)
#define arch_initcall(fn) __define_initcall("3",fn,3)
#define arch_initcall_sync(fn) __define_initcall("3s",fn,3s)
#define subsys_initcall(fn) __define_initcall("4",fn,4)
#define subsys_initcall_sync(fn) __define_initcall("4s",fn,4s)
#define fs_initcall(fn) __define_initcall("5",fn,5)
#define fs_initcall_sync(fn) __define_initcall("5s",fn,5s)
#define rootfs_initcall(fn) __define_initcall("rootfs",fn,rootfs)
#define device_initcall(fn) __define_initcall("6",fn,6)
#define device_initcall_sync(fn) __define_initcall("6s",fn,6s)
#define late_initcall(fn) __define_initcall("7",fn,7)
#define late_initcall_sync(fn) __define_initcall("7s",fn,7s)
#define __initcall(fn) device_initcall(fn)
#define module_init(x) __initcall(x);
#else /* MODULE */
/* Don't use these in modules, but some people do... */
#define early_initcall(fn) module_init(fn)
#define core_initcall(fn) module_init(fn)
#define postcore_initcall(fn) module_init(fn)
#define arch_initcall(fn) module_init(fn)
#define subsys_initcall(fn) module_init(fn)
#define fs_initcall(fn) module_init(fn)
#define device_initcall(fn) module_init(fn)
#define late_initcall(fn) module_init(fn)
#define security_initcall(fn) module_init(fn)
/* Each module must use one module_init(). */
#define module_init(initfn) \
static inline initcall_t __inittest(void) \
{ return initfn; } \
int init_module(void) __attribute__((alias(#initfn)));
/* This is only required if you want to be unloadable. */
#define module_exit(exitfn) \
static inline exitcall_t __exittest(void) \
{ return exitfn; } \
void cleanup_module(void) __attribute__((alias(#exitfn)));
#endif /*end ifndef MODULE*/
Makefile是如何控制模块的加载模式的?在init.h中可以看到下面的宏定义:
#ifndef MODULE
// 宏定义--->(静态加载方式)
#else
// 宏定义--->(动态加载方式)
#endif
当配置Makefiel时,将某个module配置为obj-m时,MODULE 这个宏就被定义,此时当前模块就被编译到内核代码中,内核启动时这个模块就被静态加载,反之,模块配置为obj-y,当前模块被配置为动态加载方式。
宏定义 __define_initcall 分析
我们看看如何解析这个宏__define_initcall:
#define __define_initcall(level,fn,id) \
static initcall_t __initcall_##fn##id __used \
__attribute__((__section__(".initcall" level ".init"))) = fn
_ * attribute * _ () 是gnu C中的扩展语法,它可以用来实现很多灵活的定义行为。
_ * attribute * _ ((_ * section * _ (“.initcall” #id “.init”)))表示编译时将目标符号放置在括号指定的段中。
在宏定义中,# 的作用是将目标字符串化,## 在宏定义中的作用是符号连接,将多个符号连接成一个符号,并不字符串化。
level是一个数字或者是数字+s,这个数字代表这个fn执行的优先级,数字越小,优先级越高,带s的fn优先级低于不带s的fn优先级
__used是一个宏定义
#define __used __attribute__((__used__))
使用前提是在编译器编译过程中,如果定义的符号没有被引用,编译器就会对其进行优化,不保留这个符号,而__attribute__((used))的作用是告诉编译器这个静态符号在编译的时候即使没有使用到也要保留这个符号。
这里的 initcall_t 是函数指针类型,对应的段:.initcall,如下:
typedef int (*initcall_t)(void);
上面所述,这个宏将我们的初始化函数放在".initcall" level ".init"中。这个段可以在Vmlinux.lds.h里面找到,如下:
#define INITCALLS \
*(.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 可以在vmlinux.lds.S里面找到:
.init.text : AT(ADDR(.init.text) - LOAD_OFFSET) {
__init_begin = .;
_sinittext = .;
*(.init.text)
_einittext = .;
}
.init.data : AT(ADDR(.init.data) - LOAD_OFFSET) { *(.init.data) }
. = ALIGN(16);
.init.setup : AT(ADDR(.init.setup) - LOAD_OFFSET) {
__setup_start = .;
*(.init.setup)
__setup_end = .;
}
.initcall.init : AT(ADDR(.initcall.init) - LOAD_OFFSET) {
__initcall_start = .;
INITCALLS //这里
__initcall_end = .;
}
.con_initcall.init : AT(ADDR(.con_initcall.init) - LOAD_OFFSET) {
__con_initcall_start = .;
*(.con_initcall.init)
__con_initcall_end = .;
}
Vmlinux.lds.h 中是系统启动时存放初始化数据的指针,执行完成后会被释放掉内存。根据上面的内存布局,可以列出初始化宏和 内存的对应关系:
_init_begin |-------------------|
| .init.text | ---- __init
|-------------------|
| .init.data | ---- __initdata
_setup_start |-------------------|
| .init.setup | ---- __setup_param
__initcall_start |-------------------|
| .initcall1.init | ---- core_initcall
|-------------------|
| .initcall2.init | ---- postcore_initcall
|-------------------|
| .initcall3.init | ---- arch_initcall
|-------------------|
| .initcall4.init | ---- subsys_initcall
|-------------------|
| .initcall5.init | ---- fs_initcall
|-------------------|
| .initcall6.init | ---- device_initcall
|-------------------|
| .initcall7.init | ---- late_initcall
__initcall_end |-------------------|
| |
| ... ... ... |
| |
__init_end |-------------------|
宏定义 #define module_init(initfn)
#define module_init(initfn) \
static inline initcall_t __inittest(void) \
{ return initfn; } \
int init_module(void) __attribute__((alias(#initfn)));
前两句话只是做了一个检测,当传进来的函数指针的参数和返回值与initcall_t不一致时,就会有告警。第三句,是使用alias将initfn变名为init_module,当调用insmod将module加载进内核时,就会去找init_module作为入口地址,即传进来的initfn, 这样module就被加载了。
举例:
为了更方便地理解,我们举个例子来说明,开发者声明了这样一个函数:module_init(hello_init);
首先宏展开成:__define_initcall(“6”,hello_init, 6)
然后接着展开:static initcall_t __initcall_hello_init6 = hello_init; 定义了函数指针变量。
同时声明 __initcall_hello_init6 这个变量即使没被引用也保留符号,且将其放置在内核镜像的.initcall6.init段处。
xxx_initcall()宏定义调用追踪
从上面的分析,我们知道xxx_initcall是如何被定义,知道目标函数的放置位置,那么使用xxx_initcall()修饰的函数是怎么被调用的呢?下面就从内核 init/main.c函数起始部分start_kernel开始往下追踪,它的调用顺序为:
start_kernel
-> rest_init();
-> kernel_thread(kernel_init, NULL, CLONE_FS);
-> kernel_init()
-> do_basic_setup();
-> do_initcalls();
rest_init();启动 RCU 锁调度器 ,调用函数 kernel_thread 创建 kernel_init 进程,也就是 init 内核进程, init 进程的 PID 为 1。调用函数 kernel_thread 创建 kthreadd 内核进程,此内核进程的 PID 为 2。kthreadd 进程负责所有内核进程的调度和管理。
do_initcalls() 在这个函数中执行所有使用xxx_initcall()声明的函数,完成 Linux 下驱动模型子系统的初始化。
static void __init do_initcalls(void)
{
initcall_t *fn;
for (fn = __early_initcall_end; fn < __initcall_end; fn++)
do_one_initcall(*fn);
}
函数中的 fn 为函数指针,fn++ 相当于函数指针+1,相当于内存地址+sizeof(fn)。文章来源:https://www.toymoban.com/news/detail-531424.html
int do_one_initcall(initcall_t fn)
{
ret.result = fn();//执行功能函数
}
在do_one_initcall函数里执行被初始化的模块。文章来源地址https://www.toymoban.com/news/detail-531424.html
到了这里,关于Linux中驱动模块加载方法分析的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!