内核加载驱动机制详解(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)的主要内容,如果未能解决你的问题,请参考以下文章

linux内核驱动module_init解析

内核驱动最简demo

内核驱动最简demo

内核驱动最简demo

向Linux内核添加驱动的步骤详解

向Linux内核添加驱动的步骤详解