一、Linux内核模块简介
1. 概述
由于Linux内核的整体架构庞大且组件多,若将所有需要的功能都打包到内核,会导致内核会很大且臃肿,为了解决这个问题,Linux提供了Module的机制。
Module机制就是在编译内核时本身并不需要包含所有功能,在需要某些功能时再将对应模块动态的加载到内核中,且模块一旦被加载就和内核中其他部分完全一样。
include <linux/init.h>
include <linux/module.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello World enter\n");
return 0;
}
module_init(hello_init);
static void __exit hello_exit(void)
{
printk(KERN_INFO "Hello World exit\n ");
}
module_exit(hello_exit);
MODULE_AUTHOR("studey");
MODULE_LICENSE("GPL v2");
MODULE_DESCRIPTION("A simple Hello World Module");
MODULE_ALIAS("a simplest module");
以上是一个最简单的内核模块,编译会产生hello.ko目标文件。
insmode ./hello.ko
命令可以加载模块,加载时会输出"Hello World enter",通过rmmode hello
命令卸载时会输出"Hello World exit"。modprobe
命令比insmod
命令强大,其在加载某模块时会同时加载该模块所依赖的其他模块,通过modprobe -r
卸载时也将同时卸载其依赖的模块。各模块的依赖信息存放在“/lib/modules/$kernel-version/modules.dep”文件中。lsmod
命令可以获取系统中已加载的所有模块及模块间的依赖关系,其实际上是读取并分析"/proc/modules"文件。已经加载模块的信息也存在于"/sys/module"目录下,加载hello.ko后将包含"sys/module/hello"目录modinfo hello
可以获取模块的信息,包括模块作者、模块说明等
printk()函数是内核空间的输出函数,与用户空间的printf()的差别在于可以定义输出的级别
2. 结构
一个LInux内核模块主要由以下几部分组成:
- 模块加载函数:通过
insmod/modprobe
命令加载内核模块时,模块的加载函数会自动被内核调用执行,完成本模块的相关初始化工作。 - 模块卸载函数:当通过
rmmod
命令卸载某模块时,模块的卸载函数会自动被内核调用执行,完成与模块加载函数相反的功能。 - 模块许可证声明:LICENSE声明描述内核模块的许可权限,大多数情况下内核都应该遵循GPL兼容许可权,如果不声明LICENSE会在模块加载时收到内核被污染的警告(Kernel Tainted)。
- 模块参数(可选): 模块加载时可以传递给它的值,对应模块内部的全局变量。
- 模块导出符号(可选):内核模块可以导出符号(对应模块内部的函数或变量)给其他模块使用
- 模块信息声明(可选 ):如模块作者等信息
二、具体介绍
1. 模块加载函数
Linux内核模块加载函数编写的标准:返回整型值,若初始化成功返回0,失败返回对应错误码(可以根据<linux/errno.h>中的错误符号定义)。 一般以__init
标识声明,通过module_init()
函数来指定。
static int _ _init initialization_function(void)
{
/* 初始化代码 */
}
module_init(initialization_function);
所有的标识为__init
的函数如果直接编译进内核而非生成.ko档的话,在链接的时候都会生成.init.text段,并在.intcall.init也保存了一份函数指针,初始化时内核会通过这些函数指针调用__init
函数,初始化完成后释放init(包括.init.text、.initcall.init)区段的内存。
#define __init __attribute__ ((__section__ (".init.text")))
除了函数之外,对于知识初始化阶段需要的数据也可以被定义为__initdata
,内核在初始化完后也会释放对应的内存。
static int hello_data __initdata = 1;
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, world %d\n", hello_data);
return 0;
}
module_init(hello_init);
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, world\n");
}
module_exit(hello_exit);
代码中可以直接通过request_module(module_name)
函数灵活地加载内核模块
2. 模块卸载函数
Linux内核模块卸载函数不返回任何值,要完成与模块加载函数相反的功能。一般以__exit
标识声明,通过module_exit()
函数来指定。
static void _ _exit cleanup_function(void)
{
/* 释放代码 */
}
module_exit(cleanup_function);
如果对应的模块直接编译进内核而非生成.ko档的话,就不可能再卸载该模块了,所以标识为__exit
的函数此时不会链接到内核。
除了函数之外,推出阶段需要的数据也可以用__exitdata
来形容。
3. 模块参数
模块的参数通过module_param(参数名,参数类型,参数读/写权限)
函数来指定,在装载内核模块时,用户可以向模块传递参数,形式为:insmode/modprobe 模块名 参数名1= 参数值, 参数名2=参数值
,如果不传递则使用模块内定义的缺省值。
若是参数数组则通过module_param_array(数组名,数组类型,数组长,参数读/写权限)
来指定。运行insmod/modprobe
命令时,使用逗号分隔输入的数组元素。
如果模块被内置无法使用insmode
,可以使用bootloader中通过bootargs设置“模块名 参数名1= 参数值, 参数名2=参数值”给内置的模块传递参数。
static char *book_name = "dissecting Linux Device Driver";
module_param(book_name, charp, S_IRUGO);
static int book_num = 4000;
module_param(book_num, int, S_IRUGO);
参数的类型可以是byte、short、ushort、int、uint、long、ulong、charp(字符指针)、bool或invbool(布尔的反)
,模块在编译时会将module_param()
中声明的类型与变量定义的类型进行比较是否一致。
模块被加载后,在"/sys/module"目录下会出现该模块的目录,若模块参数指定的读/写权限不为0,则会在该模块目录下的“parameters”创建对应参数的节点,节点的读/写权限就是参数指定的权限,节点内容就是参数的值。
#include <linux/init.h>
#include <linux/module.h>
static char *book_name = "dissecting Linux Device Driver";
module_param(book_name, charp, S_IRUGO);
static int book_num = 4000;
module_param(book_num, int, S_IRUGO);
static int __init book_init(void)
{
printk(KERN_INFO "book name:%s\n", book_name);
printk(KERN_INFO "book num:%d\n", book_num);
return 0;
}
...
对上述模块运行insmode book.ko
命令装载时,相应参数都输出的是模块内的默认值,通过查看“/var/log/message”日志文件可以看出内核的输出:
# tail -n 2 /var/log/messages
Jul 2 01:03:10 localhost kernel: <6> book name:dissecting Linux Device Driver
Jul 2 01:03:10 localhost kernel: book num:4000
当用户运行insmod book.ko book_name='GoodBook'book_num=5000
命令时,输出的是用户传递的参数:
# tail -n 2 /var/log/messages
Jul 2 01:06:21 localhost kernel: <6> book name:GoodBook
Jul 2 01:06:21 localhost kernel: book num:5000
另外在“/sys/module/book/paameters”路径下可以看到对应的参数, 并且可以通过cat book_name”和“cat book_num
查看它们的值。
stydy@test:/sys/module/book/parameters$ tree
. ├── book_name └── book_num
4. 导出符号
导出的符号可以被其他模块使用,只需要像使用普通的函数一样,提前声明一下即可。
模块可以使用EXPORT_SYMBOL/EXPORT_SYMBOL_GPL(符号表)
导出符号到内核符号表中。
EXPORT_SYMBOL_GPL
只适用于包含GPL许可权的模块,若想要被非GPL模块引用,则可以将GPL去掉,或者通过写一个wrapper内核模块对GPL模块的函数以EXPORT_SYMBOL()
形式封装后使用(可能会构成侵权)。
“/proc/kalllsyms”文件对应着内核符号表,记录了符号以及符号所在的内存地址。
#include <linux/init.h>
#include <linux/module.h>
int add_integar(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL_GPL(add_integar);
int sub_integar(int a, int b)
{
return a - b;
}
EXPORT_SYMBOL_GPL(sub_integar);
MODULE_LICENSE("GPL v2");
# grep integar /proc/kallsyms
e679402c r __ksymtab_sub_integar [export_symb]
e679403c r __kstrtab_sub_integar [export_symb]
e6794038 r __kcrctab_sub_integar [export_symb]
e6794024 r __ksymtab_add_integar [export_symb]
e6794048 r __kstrtab_add_integar [export_symb]
e6794034 r __kcrctab_add_integar [export_symb]
e6793000 t add_integar [export_symb]
e6793010 t sub_integar [export_symb]
5. 模块声明与描述
模块MODULE_AUTHOR、MODULE_DESCRIPTION、MODULE_VERSION、MODULE_DEVICE_TABLE、MODULE_ALIAS分别声明模块的作者、描述、版本、设备表和别名
MODULE_AUTHOR(author);
MODULE_DESCRIPTION(description);
MODULE_VERSION(version_string);
MODULE_DEVICE_TABLE(table_info);
MODULE_ALIAS(alternate_name);
对于USB、PCI等设备驱动,通常会创建一个MODULE_DEVICE_TABLE
表明该驱动模块所支持的设备。
/* table of devices that work with this driver */
static struct usb_device_id skel_table [] = {
{ USB_DEVICE(USB_SKEL_VENDOR_ID,
USB_SKEL_PRODUCT_ID) },
{ } /* terminating enttry */
};
MODULE_DEVICE_TABLE (usb, skel_table);
6. 模块的编译
# Kernel modules
obj-m += hello.o
hello-objs := file1.o file2.o
# Specify flags for the module compilation.
# EXTRA_CFLAGS=-g -O0
build: kernel_modules
kernel_modules:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
clean:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean
开启EXTRA_CFLAGS=-g -O0
后编译出的ko当可以包含调试信息。