在本章节中,我们将了解如何编写一个linux内核模块,以及其相关概念与结构。
一、Linux内核模块简介
向Linux内核添加组件的方式有两种,一是将组件编入内核,二是以模块的方式来实现。内核具有如下的特点:
- 模块本身不编入内核,从而减小内核的大小。
- 模块一旦被加载,就和内核的其他部分一样。
下面是一个最简单的Hello World模块,其位于kernel/drivers/myHelloWorld/
目录下:
#include <linux/module.h>
#include <linux/init.h>
#include <linux/kernel.h>
static int __init myHelloWorld_init()
{
printk(KERN_INFO "Hello World init\n");
}
module_init(myHelloWorld_init);
static void __exit myHelloWorld_exit()
{
printk(KERN_INFO "Hello World exit\n");
}
module_exit(myHelloWorld_exit);
MODULE_LICENSE("GPL v2");
这个模块只包含了简单的加载与退出功能,以及一个关于许可证的说明。加载模块可以使用insmod ./myHelloWorld.ko
命令加载,卸载模块则是通过rmmod myHelloWorld
来实现。
内核空间中用于输出的的函数是printk
而不是用户空间使用的printf
。printk
和printf
的使用方法基本相似,但是可以指定输出级别。
在linux中,可以使用lsmod
命令来获取已加载的所有模块以及其依赖关系,例如:
Module Size Used by
ccm 20480 9
rfcomm 77824 0
bnep 20480 2
nls_iso8859_1 16384 1
snd_hda_codec_hdmi 49152 1
arc4 16384 2
snd_soc_skl 86016 0
...
lsmod
实际上是读取并分析/proc/modules
文件,与之相同的是cat /proc/modules
。内核中已加载的模块同样位于/sys/module
目录下,加载hello.ko后,内核中也将包含/sys/module/hello
目录。该目录下仅有一个refcnt文件和一个sections目录,
modprobe
比insmod
更强大,前者在加载mod时不仅会加载mod本身,同时也一并加载mod所依赖的其他mod。对于使用modprobe
加载的模块,如果使用modprobe -r filename
卸载,则同时将所有依赖的模块一并卸载。模块之间的依赖关系存放于/lib/modules/<kernel-version>/modules.dep
文件中。使用modinfo modname
可以查看mod的信息。
二、Linux内核模块程序结构
Linux内核模块由以下几部分组成:
- 模块加载函数。当通过
insmod
或者modprobe
命令加载内核模块时,模块的加载函数会自动的执行,完成本模块相关的初始化工作。 - 模块卸载函数。当通过
rmmod
命令卸载某模块时,该函数会自动执行,并执行与加载函数相反的操作。 - 内核许可声明。LICENSE声明描述内核模块的许可权限,如果不声明LICENSE,模块加载到内核时将收到
Kernel Tainted
警告。可接受的LICENSE包括GPL
、GPL v2
、GPL and additional rights
、Dual BSD/GPL
、Dual MPL/GPL
和Proprietary
。 - 模块参数(可选)。加载模块时传递的值,它本身对应模块的全局变量。
- 模块导出符号(可选)。一个可导出的变量或函数,提供给其他的模块来使用。
- 其他声明信息(可选)。包括作者、描述、别名等。
三、模块加载函数
Linux内核模块加载函数一般由__init
声明,例如:
static int __init moduleInitFunction()
{
/* init */
}
module_init(moduleInitFunction);
模块加载函数以module_init(MODULE_NAME)
的形式被指定。它返回int型,若初始化成功则返回0,失败则返回错误编码。在Linux内核中,错误编码是一个接近于0的负值,定义于<linux/errno.h>
中,包含-ENODEV
、-ENOMEM
之类的值。
在Linux中,可以使用request_module(const char* fmt,...)
函数加载内核模块,驱动开发人员可以通过调用request_module(MODULE_NAME)
来加载其模块。
在Linux中,所有标识为__init
的函数如果直接编译入内核,成为内核镜像的一部分。其在连接的时候都会放在.init.text
这个区段内。
#define __init __attribute__((__section__(".init.text")))
所有的init函数在区段.initcall.init
中还保存了一份函数指针,在初始化内核会通过这些指针去调用这些init函数,并在初始化完成之后释放__init区段(包括.init.text
和.initcall.init
等内容)的内存。
除了函数之外,变量也可以被定义为__initdata
,对于只是初始化阶段需要的数据,内核在初始化完成之后,也可以释放其占有的内存。例如:
static int myData __initdata = 1;
四、模块卸载函数
模块卸载函数一般以__exit
标识声明,典型的模块卸载函数定义如下:
static void __exit moduleExitFunction()
{
/* exit */
}
module_eixt(moduleExitFunction);
exit函数在模块卸载时执行,并且无任何返回值,且必须以module_exit(MODULE_NAME)
的形式来指定。通常来说,模块卸载函数要完成与模块加载函数相反的功能。
我们用__exit
来修饰模块卸载函数,可以告内核如果相关的模块被直接编译进内核(即build-in),则exit函数会被省略,直接不链接入最后的镜像。除了函数外,只是退出阶段采用的数据也可以用__exitdata
来修饰。
五、模块参数
我们可以使用module_param(PARAM_NAME, PARAM_TYPE, READ_WRITE_PERMISSION)
,来为模块定义一个参数,如下代码例举了整形参数和字符型指针:
static char* bookName = "BookName";
module_param(bookName, charp, S_IRUGO);
static int bookNum = 1;
module_param(bookNum, int, S_IRUGO);
在装载内核模块时,用户可以向模块传递参数,形式为insmod MODULE_NAME=_MODULE_NAME_ PARAM_NAME=_PARAM_NAME_
,如果不传入参数,则将使用模块内的默认参数。如果以build in的方式构建,则无法使用insmod
,但是bootloader里可以在bootargs里设置MODULE_NAME.PARAM_NAME=VALUE
的形式来传递参数。同时,还可以使用module_param_array(ARRAY_NAME, ARRAY_TYPE, ARRAY_LONGTH, READ_WRITE_PERMISSION)
来设置参数数组。
模块被加载后,在/sys/module/
目录下将出现以此模块命名的目录。当READ_WRITE_PERMISSION
为0时,表示此参数不存在sysfs
文件系统下对应的文件节点,如果此模块存在读写权限不为0的命令行参数,在此模块的目录下还将出现parameters目录,其中包括一系列以参数名命名的文件节点,这些文件的权限值就是传入module_param()
的参数读写权限,而文件的内容为参数的值。
运行insmod
或modprobe
命令时,应用逗号分隔输入的数组元素。
下面是一个带参数的内核模块示例:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
static char* bookName = "book_name";
module_param(bookName, charp, S_IRUGO);
static int bookNum = 1;
module_param(bookNum, int, S_IRUGO);
static int __init book_init()
{
printk(KERN_INFO "book name is %s\n", bookName);
printk(KERN_INFO "book num is %d\n", bookNum);
return 0
}
module_init(book_init);
static void __exit book_exit()
{
// do nothing
}
module_exit(book_exit);
MODULE_LICENSE("GPL v2");
六、导出符号
Linux的/proc/kallsyms
文件对应着内核符号表,它记录了符号以及符号所在的内存地址。模块可以使用如下宏导出:
EXPORT_SYMBOL(_SYMBOL_NAME_);
EXPORT_SYMBOL_GPL(_SYMBOL_NAME_);
导出的符号可以被其他模块使用,只需要在使用前声明即可。EXPORT_SYMBOL_GPL()
只适用于包含GPL许可权的模块。如下是一个导出加减法函数的实例:
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>
int add_int(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(add_int);
int sub_int(int a, int b)
{
return a - b;
}
EXPORT_SYMBOL(sub_int);
MODULE_LICENSE("GPL v2");
从/proc/kallsyms
文件中可以找出add_int
和sub_int
的相关信息。
七、模块声明与描述
在Linux内核模块中,我们常用如下函数来声明模块的作者等信息:
MODULE_AUTHOR(author)
,模块作者。MODULE_DRSCRIPTION(description)
,模块描述。MODULE_VERSION(version_string)
,模块版本。MODULE_DEVICE_TABLE(table_info)
,设备表。MODULE_ALIAS(alternate_name)
,别名。
对于USB和PCI等驱动设备,通常会建一个MODULE_DEVICE_TABLE
,以表明该模块所支持的设备:
static struct usb_device_id skel_table[] =
{
{ USB_DEVICE(USB_SKEL_VENDOR_ID, USB_SKEL_PRODUCT_ID) },
{ /* terminating enttry */ }
};
在后续的章节中我们将介绍MODULE_DEVICE_TABLE
的作用。
八、模块的使用计数
Linux 2.6之后的内核提供了模块计数接口try_module_get(&module)
和module_put(&module)
,以取代2.4当中的宏。模块的使用计数一般不必由模块自身管理,而且模块计数管理还考虑了SMP与PREEMPT的影响。
int try_module_get(struct module* module);
该函数用于增加模块的计数;若返回0则表示调用失败,希望使用的模块没有被加载或正在被卸载。
void module_put(struct module* module);
该函数用于减少模块的计数。
九、模块的编译
我们尝试为之前的myHelloWorld模块写一个Makefile:
KVERS = $(shell uname -r)
# Kernel Modules
obj-m += hello.o
build: kernel_modules
kernel_modules:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) modules
clean:
make -C /lib/modules/$(KVERS)/build M=$(CURDIR) clean