目录
1、内核模块介绍
Linux提供了一种 需要时可以被动态加载和移除的代码的机制,这种机制称为模块(Module),内核模块具有以下两个特点:
- 模块本身不被编译入内核映像,从而使内核映像比较精简。
- 模块被加载后,其与其它内核进程没有区别。
内核模块的本质:一段隶属于内核的“动态”代码,与其它内核代码是同一个运行实体,共用同一套运行资源,只是存在形式上是独立的。
记住,内核模块的运行是在内核空间里的。
2、内核模块的结构
2.1 hello world例程
#include <linux/init.h>
#include <linux/module.h>
/***************************************/
static int __init my_init(void){
printk("Hello Driver initalized!\n")
return 0;
}
static void __exit my_exit(void){
printk("Hello Driver exit!\n")
}
module_init(my_init);
module_exit(my_exit);
/***************************************/
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Micher Lee");
MODULE_DESCRIPTION("");
MODULE_ALIAS("");
这是一个典型的内核模块,用注释符分隔成了三个部分。
- 第一部分为包含的头文件;
- 第二部分为模块的加载与卸载函数;
- 第三部分为许可权声明等描述信息。
而实际,只要记住模块三要素:入口函数 、出口函数、 MODULE__LICENSE
2.2 结构说明
2.2.1 包含库
#include <linux/module.h> //包含内核编程最常用的函数声明,如printk
#include <linux/kernel.h> //包含模块编程相关的宏定义,如:MODULE_LICENSE
2.2.2 __init的作用 :
原型:
int __init my_init(void){};
- __init 是一个宏,展开后为:attribute ((section (“.init.text”))) 实际是gcc的一个特殊链接标记。
- 指示链接器将该函数放置在 .init.text区段。.init.text区段是Linux内核的一种特殊的代码段。该区段中的代码只在内核启动阶段执行一次,启动完成后不再执行,一旦内核启动完成,.init.text节就可以被释放。
- 在模块插入时方便内核从ko文件指定位置读取入口函数的指令到特定内存位置 。
2.2.2.1 补充:内核模块代码段的结构
Linux内核的链接过程与普通程序有所不同。主要有以下几点:
1. 段的种类更丰富。除了普通段(.text、.data、.bss)外,还有.init.text、.exit.text、.rodata等。
2. 段的作用更专门。如.init.text只包含初始化代码,.exit.text只包含退出代码。
3. 段的生命周期不同。如.init.text在启动后被释放,.exit.text在退出前被建立。
Linux内核的主要代码段有:
1. .text - 包含模块的主体函数代码,生命周期与模块相同。
2. .init.text - 包含初始化函数代码,启动后被释放。
3. .exit.text - 包含退出函数代码,退出前被建立。
4. .rodata - 包含常量数据,生命周期与模块相同,内容不可变。
5. .data - 包含可变全局变量和局部静态变量,生命周期与模块相同。
6. .bss - 包含未初始化的全局变量,生命周期与模块相同。
7. .init.data/.exit.data - 包含.init.text/.exit.text对应的变量,生命周期与对应的文本段相同。
除此之外,Linux内核还有其他段如.got等。
链接器在链接内核模块时,会根据代码和数据的标记、属性,将它们放入适当的段中:
1. __init/__exit宏标记的函数放入.init.text/.exit.text。
2. 常量数据放入.rodata。
3. 未初始化数据放入.bss。
4. 其他代码和数据放入.text/.data。
5. 生成段的属性,如.init.text属性为可释放等。
最终链接器生成的ko文件,包含着这些正确分类的代码段,并且这些段的生命周期和访问属性都已正确确定。这为内核执行这些模块的代码提供了必要的信息。
2.2.3 关于printk
内核是裸机程序,不可以调用C库中printf函数来打印程序信息, Linux内核源码自身实现了一个用法与printf差不多的函数,命名为printk (k-kernel), printk不支持浮点数打印。
2.2.4 __exit的作用:
void __exit my_exit(void){};
1.__exit是一个宏,展开后为:attribute ((section (“.exit.text”))) 实际也是gcc的一个特殊链接标记
2.指示链接器将该函数放置在 .exit.text区段
3.在模块插入时方便内核从ko文件指定位置读取出口函数的指令到另一个特定内存位置
2.2.5 MODULE_LICENSE(字符串常量);
MODULE_LICENSE(“GPL”);
字符串常量内容为源码的许可证协议 可以是"GPL" “GPL v2” “GPL and additional rights” “Dual BSD/GPL” “Dual MIT/GPL” "Dual MPL/GPL"等, "GPL"最常用
其本质也是一个宏,宏体也是一个特殊链接标记,指示链接器在ko文件指定位置说明本模块源码遵循的许可证
2.2.6 module_init 宏
原型:
#include <linux/init.h>
#define module_init(x) __initcall(x);
使用
module_init(my_init);
- 用法:module_init(模块入口函数名)
- 对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.initcall段),方便系统初始化统一调用。
- 对于动态加载的模块,由于内核模块的默认入口函数名是init_module,用该宏可以给对应模块入口函数起别名
2.2.7 module_exit宏
原型:
#include <linux/init.h>
#define module_exit(x) __exitcall(x);
使用:
module_exit(myhello_exit);
1.用法:module_exit(模块出口函数名)
2.对于静态加载的模块其本质是定义一个全局函数指针,并将其赋值为指定函数,链接时将地址放到特殊区段(.exitcall段),方便系统必要时统一调用,实际上该宏在静态加载时没有意义,因为静态编译的驱动无法卸载。
3.对于动态加载的模块,由于内核模块的默认出口函数名是cleanup_module,用该宏可以给对应模块出口函数起别名
2.2.8 内核模块信息宏
MODULE_AUTHOR(字符串常量); //字符串常量内容为模块作者说明
MODULE_DESCRIPTION(字符串常量); //字符串常量内容为模块功能说明
MODULE_ALIAS(字符串常量); //字符串常量内容为模块别名
这些宏用来描述一些当前模块的信息,可选宏
这些宏的本质是定义static字符数组用于存放指定字符串内容,这些字符串内容链接时存放在.modinfo字段,可以用modinfo命令来查看这些模块信息,用法:modinfo 模块文件名
2.2.9 模块参数
- 说明
模块也可以被传入参数。只是和函数传参的方式不同。其形式如下:
module_param( name , type , perm);
或
module_param_array(数组name, type , 数组长度 , perm);
其中:
name为全局变量名
type为全局变量的模参专用类型
perm为对应文件 /sys/module/对应模块名/parameters/变量名的操作权限。 一般赋值0664即可。
关于perm这里多解释一下:模块被加载后,在/sys/module目录下会出现以此模块名命名的目录。当perm不为0时,在此目录下还将出现parameters目录,基中包含一系列以参数名命名的文件节点。这些文件的权限值就是perm的值。
- 例子
/*模块名:book 编译后文件名为 book.ko */
#include <linux/init.h>
#include <linux/module.h>
static char *book_name = "hello";
module_param(book_name , charp , 0664);
static int book_um = 400;
module_param(book_num , int , 0664);
static init __init my_init(void){
printk("book name:%s\n", book_name);
printk("book num :%d\n", book_num);
return 0
}
static void __exit my_exit(void){
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
- 加载
上面这段代码编译后的文件名为book.ko
如果用 “ insmod book.ko ”来加载,则没有参数传入, 变量book_name及book_num的值是默认初始值。
如果用 “ insmod book.ko book_name=“world” book_num=500 ” 语句来加载,则参数被传入。
2.2.10 导出符号
模块是内核的一部分,与内核是一个整体,因此就可以与内核其它程序去共享全局的符号。
Linux的“/proc/kallsyms”文件,对应着整个内核所有的符号表,它记录了符号以及符号所在在内存地址。
对于一个模块,可以通过如下的宏,将自身的符号导出到内核符号表中,可供其它内核进程使用。
- 导出语法
EXPORT_SYMBOL(符号名);
EXPORT_SYMBOL_GPL(符号名); - 使用导出符号
extern 符号名 - 编译及加载
- 案例
下面有两种驱动, 一个export.c用于导出符号,另一个extern.c用于接收使用导出的符号。下面是源码
对两个驱动进行加载后的输出结果:
3、常用操作命令
3.1 lsmod
3.2 insmod
3.3 rmmod
3.4 dmesg
3.5 modinfo
本篇结束