内核加载驱动机制详解(module_init & module_exit)
Posted 正在起飞的蜗牛
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了内核加载驱动机制详解(module_init & module_exit)相关的知识,希望对你有一定的参考价值。
1、驱动加载方式:静态加载 & 动态加载
Linux是高度可配置、可裁剪的,驱动程序是对内核功能的扩展。
静态加载:将驱动代码直接编译进内核,内核在启动过程中就会自动加载内核;
动态加载:将驱动代码单独编译成.ko格式的文件,再用insmod命令在需要的时候加载内核,在不需要驱动的时候用rmmod命令卸载驱动;
比较:静态加载一般用于基础功能的驱动,反正都是迟早是要加载的,编译进内核效率更高;动态加载一般用于扩展功能的驱动,这个设备可能
需要加载这个驱动也可能不需要加载这个驱动,这就要根据情况去加载和卸载驱动,比如:支持热插拔功能的设备。
补充:驱动代码是单独编译还是直接编译进内核,可以在配置内核是通过make menuconfig进行选择;也可以将驱动代码独立出来,自己编写Makefile编译成ko文件;
2、module_init宏和module_exit宏介绍
2.1、基本的驱动代码示例程序:
#include <linux/module.h> // module_init module_exit
#include <linux/init.h> // __init __exit
// 模块安装函数
static int __init chrdev_init(void)
printk(KERN_INFO "chrdev_init helloworld init\\n");
return 0;
// 模块卸载函数
static void __exit chrdev_exit(void)
printk(KERN_INFO "chrdev_exit helloworld exit\\n");
module_init(chrdev_init);
module_exit(chrdev_exit);
// MODULE_xxx这种宏作用是用来添加模块描述信息
MODULE_LICENSE("GPL"); // 描述模块的许可证
MODULE_AUTHOR("aston"); // 描述模块的作者
MODULE_DESCRIPTION("module test"); // 描述模块的介绍信息
MODULE_ALIAS("alias xxx"); // 描述模块的别名信息
module_init:用来指定加载驱动时要执行的模块加载函数;
module_eixt:用来指定卸载驱动时要执行的模块卸载函数;
备注:__init和__exit宏参考博客:
2.2、宏定义介绍
#ifndef MODULE
···
//静态加载
#define module_init(x) __initcall(x);
#define module_exit(x) __exitcall(x);
···
#else /* MODULE */
···
//动态加载
#define module_init(initfn) \\
static inline initcall_t __inittest(void) \\
return initfn; \\
int init_module(void) __attribute__((alias(#initfn)));
#define module_exit(exitfn) \\
static inline exitcall_t __exittest(void) \\
return exitfn; \\
void cleanup_module(void) __attribute__((alias(#exitfn)));
···
#endif
module_init宏和module_exit宏在内核中有两种定义,分别对应动态加载和静态加载。其中定义了MODULE宏的是动态加载,没有定义MODULE宏的是静态加载,
后面会对两种情况的宏定义进行分析;
3、module_init宏
3.1、静态加载方式
3.1.1、宏定义
//函数指针类型和驱动加载函数是对应的
typedef int (*initcall_t)(void);
#define __define_initcall(level,fn,id) \\
static initcall_t __initcall_##fn##id __used \\
__attribute__((__section__(".initcall" level ".init"))) = fn
#define device_initcall(fn) __define_initcall("6",fn,6)
#define __initcall(fn) device_initcall(fn)
#define module_init(x) __initcall(x)
3.1.2、宏定义展开分析
//假设用module_init宏指定驱动加载函数是chrdev_init函数
module_init(chrdev_init);
//将宏展开
static initcall_t __initcall_chrdev_init6 __used __attribute__((__section__(".initcall6.init"))) = chrdev_init
相当于定义了一个类型为initcall_t的静态局部变量__initcall_chrdev_init6,变量被赋值为chrdev_init函数指针,变量被赋予段属性".initcall6.init"。其中段属性需要重点关注
,后面加载驱动会用到,作用就是将驱动加载函数的函数指针都放到一起,将来可以通过遍历的方式去执行这些函数,这种段属性的使用方法很常用,可以参考博客:;
3.1.3、静态加载驱动的函数调用关系
start_kernel(void)
rest_init(void)
kernel_init(void * unused)
do_basic_setup(void)
do_initcalls(void)
do_one_initcall(*fn);
(1)do_initcalls(void):函数功能是遍历".initcall6.init"段,将放在该段的函数都执行一遍,也就是执行通过module_init宏指定的驱动加载函数;
(2)do_one_initcall(fn):具体执行驱动加载函数,其中传参fn就是驱动加载函数的地址,也就是函数指针;
补充:start_kernel()是内核启动C语言阶段的第一个函数,说明静态加载驱动的函数是在启动阶段被调用的;
3.1.4、do_initcalls(void)函数源码
static void __init do_initcalls(void)
initcall_t *fn;
for (fn = __early_initcall_end; fn < __initcall_end; fn++)
do_one_initcall(*fn);
/* Make sure there is no pending stuff from the initcall sequence */
flush_scheduled_work();
(1)__early_initcall_end和__initcall_end:这是在内核链接脚本vmlinux.lds中定义的两个符号,标明某些段的起始地址和结束地址。其中这两个符号包括的
段属性有*(.initcall0.init) – (.initcall7s.init),而module_init宏指定的(.initcall6.init)段属性就被包括其中;内核链接脚本参考博客:
(2)for循环:遍历每个段,并把注册在段中的函数指针都执行一遍。fn就是initcall_t类型的变量,这和我们在解析module_init宏时是相对应的,module_init就是定义一个initcall_t类型的变量并赋值为
驱动加载函数的函数指针;
3.2、动态加载方式
3.2.1、宏定义
//函数指针类型和驱动加载函数是对应的
typedef int (*initcall_t)(void);
#define module_init(initfn) \\
static inline initcall_t __inittest(void) \\
return initfn; \\
int init_module(void) __attribute__((alias(#initfn)));
3.2.2、宏解析
//假设用module_init宏指定驱动加载函数是chrdev_init函数
module_init(chrdev_init);
//将宏展开
static inline initcall_t __inittest(void) \\
return chrdev_init; \\
int init_module(void) __attribute__((alias(chrdev_init)));
//精简上面的宏定义
int init_module(void) __attribute__((alias(chrdev_init)));
(1)module_init宏定义分为两部分,一部分是定义了__inittest函数,另一部分是利用gcc的alias属性,给函数取别名;
(2)__inittest函数:函数没有实际的功能,返回chrdev_init函数指针,作用就是检查chrdev_init是否是initcall_t类型的函数指针,如果不是则编译报错;
(3)取别名:利用gcc的alias属性,给chrdev_init函数取别名init_module;作用就是后续的代码中调用init_module函数其实就是在调用你指定的驱动加载函数chrdev_init函数;
3.2.3、为什么取别名?
将驱动加载函数统一取别名为init_module(),意味着无论加载哪个驱动其驱动加载函数都是同一个名字,后续处理就会很方便。init_module函数别名会赋值给xxx.mod.c文件中的
__this_module结构体,__this_module结构体会被链接到ko文件中,进而内核再加载ko文件时可以解析处__this_module结构得到驱动加载函数。具体参考博客:《编译驱动的Makefile详解》;
3.2.4、insmod命令加载驱动
参考博客:《insmod命令加载驱动详解》
4、module_exit宏
4.1、静态卸载
4.1.1、宏定义
typedef void (*exitcall_t)(void);
#define __exit_call __used __section(.exitcall.exit)
#define __exitcall(fn) \\
static exitcall_t __exitcall_##fn __exit_call = fn
#define module_exit(x) __exitcall(x);
4.1.2、module_exit宏解析
//假设驱动卸载函数是chrdev_exit()
module_exit(chrdev_exit)
//宏定义展开
static exitcall_t __exitcall_chrdev_exit __used __section(.exitcall.exit) = chrdev_exit
定义了类型为exitcall_t的静态局部变量__exitcall_chrdev_exit,变量被赋予".exitcall.exit"段属性,赋值为chrdev_exit。简单来说就是定义函数指针变量__exitcall_chrdev_exit指向chrdev_exit;
4.1.3、静态方式卸载
静态方式卸载就是系统关机时去卸载掉驱动并释放资源,在内核关机的代码中肯定会去处理".exitcall.exit"段属性的代码,具体怎么处理的没去研究,内核都关机了怎么卸载驱动感觉也不是特别重要,
反正下次开机又重新开始。所以我觉得驱动的静态加载比较重要,静态卸载方式不怎么重要,以后有必要的时候再去研究静态卸载的细节。
4.2、动态卸载方式
4.2.1、module_exit宏定义
typedef void (*exitcall_t)(void);
#define module_exit(exitfn) \\
static inline exitcall_t __exittest(void) \\
return exitfn; \\
void cleanup_module(void) __attribute__((alias(#exitfn)));
4.2.2、module_exit宏定义解析
//假设驱动卸载函数是chrdev_exit()
module_exit(chrdev_exit)
//宏定义展开
static inline exitcall_t __exittest(void) \\
return chrdev_exit; \\
void cleanup_module(void) __attribute__((alias(chrdev_exit)));
(1)__exittest函数是检查卸载函数的函数类型是否和exitcall_t类型匹配;
(2)将chrdev_exit函数取别名为cleanup_module;
备注:更详细的分析参考module_init宏的动态加载分析,思路都是一样的;
4.2.3、rmmod命令卸载驱动
参考博客:《rmmod命令卸载驱动过程详解》;
以上是关于内核加载驱动机制详解(module_init & module_exit)的主要内容,如果未能解决你的问题,请参考以下文章