Linux 内核模块
在 Linux 内核空间和用户空间 提到,在 32 位系统上,Linux 内核将 4GB 空间分为 0~3 GB 的用户空间和 3~4 GB 的内核空间。用户程序运行在用户空间,可通过中断或系统调用进入内核空间,而 Linux 内核及内核模块则只能运行在内核空间。
什么是内核模块
模块(module)就是可以在系统运行中动态加载到内核和从内核卸载的代码,这个过程无须重启系统,实现了对内核功能的动态扩展。Linux 内核具有很强的可裁剪性,很多功能或者外设驱动都可以编译成模块。
模块具有如下特点:
- 模块本身不被编译进内核映像,从而控制了内核的大小;
- 模块一旦被加载,它就和内核中的其他部分完全一样。
编写内核模块
头文件
内核模块需要包含内核相关头文件,不同的模块根据其功能的差异所需要的头文件也不一样,但是 <linux/init.h>
和 <linux/module.h>
所有模块都必需的。
#include <linux/init.h>
#include <linux/module.h>
模块加载函数
模块加载函数负责模块的初始化工作(例如注册模块)。如果一个内核模块没有被注册,则其内部的各种方法无法被应用程序使用,只有已注册模块的方法才能够被应用程序使用。模块并不是内核内部的代码,而是独立于内核之外的,通过初始化,能够让内核之外的代码来替内核完成本应该由内核完成的功能。模块注册相当于模块与内核之间衔接的桥梁,告诉内核“我是什么模块,我有什么功能,我已经准备好了”。
模块加载函数定义通常如下:
static int __init xxx_init(void)
{
/* 初始化代码 */
return 0;
}
module_init(xxx_init);
说明:
- 模块初始化函数一般都声明为 static 静态函数,因为它对于其他源码文件没有任何意义;
__init
表示初始化函数仅仅在初始化期间使用,一旦初始化完毕,则将释放初始化函数所占用的内存,类似的还有__initdata
;- 模块加载函数以
module_init(函数名)
的形式指定,如果没有这个定义,那么内核将无法执行初始化代码。module_init
宏定义会在模块的目标代码中增加一个特殊的代码段,用于说明该初始化函数所在的位置。
当使用 insmod 命令将模块加载进内核的时候,初始化函数的代码将会被执行。模块初始化代码只与内核模块管理子系统打交道,并不与应用程序交互。
模块卸载函数
当系统不再需要某个模块时,可以卸载这个模块,从而释放该模块所占用的资源。实现模块退出的函数通常称为模块的退出函数或者清除函数,其定义通常如下:
static void __exit xxx_exit(void)
{
/* 释放代码 */
}
module_exit(xxx_exit);
说明:
- 模块退出函数没有返回值;
__exit
标记这段代码仅用于模块卸载;- 模块卸载函数以
module_exit(函数名)
的形式指定,但这不是必须的,比如有些模块加载之后并不需要卸载,则可以不添加module_exit
定义,但需要支持模块卸载的必须添加。
当使用 rmmod 卸载模块时,退出函数的代码将被执行。模块退出代码只与内核模块管理子系统打交道,并不直接与应用程序交互。
许可证声明
Linux 内核是开源的,遵守 GPL 协议,所以要求加载进内核的模块也最好遵循相关协议。使用 MODULE_LICENSE
宏来声明,例如:
MODULE_LICENSE("GPL");
许可证(LICENSE)声明描述内核模块的许可权限,如果不声明 LICENSE,模块被加载时将收到内核被污染(Kernel Tainted)的警告。目前,内核可接收的 LICENSE 包括 GPL、GPL v2、GPL and additional rights、Dual BSD/GPL、Dual MPL/GPL 和 Proprietary(关于模块是否可以采用非 GPL 许可权,如 Proprietary,这在学术界和法律界都有争议)。
符号导出
在 Linux 2.4 内核中,默认的非 static 函数和变量都会自动导入到内核空间,出于安全考虑,Linux 2.6 开始修改为默认不导出所有的符号,需要导出的符号必须使用 EXPORT_SYMBOL()
进行标记。
EXPORT_SYMBOL(module_symbol);
Linux 的 /proc/kallsyms
文件对应着内核符号表,它记录了符号以及符号所在的内存地址。
除了使用 EXPORT_SYMBOL
宏,Linux 内核还提供了 EXPORT_SYMBOL_GPL
宏,它们的作用都是将符号导出到内核符号表。区别在于 EXPORT_SYMBOL_GPL
仅将符号导出到采用 GPL 许可的模块,而 EXPORT_SYMBOL
导出到所有可加载的模块。
模块描述
在内核模块中,我们还可以用 MODULE_AUTHOR
、MODULE_DESCRIPTION
、MODULE_VERSION
、MODULE_DEVICE_TABLE
和 MODULE_ALIAS
等宏分别声明模块的作者、描述、版本、设备表和别名。
MODULE_AUTHOR("作者");
MODULE_DESCRIPTION("功能描述");
MODULE_VERSION("版本");
MODULE_DEVICE_TABLE("设备表");
MODULE_ALIAS("别名");
使用 modinfo 命令可以查看模块描述信息。
最简单的内核模块
hello.c
#include <linux/init.h>
#include <linux/module.h>
static int __init hello_init(void)
{
printk(KERN_INFO "hello module init\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "hello module exit\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("Rudy <rudy@getiot.tech>");
MODULE_DESCRIPTION("A simple Hello-World module");
MODULE_ALIAS("a simplest module");
MODULE_VERSION("V1.0");
Makefile
PWD := $(shell pwd)
KVER := $(shell uname -r)
KDIR :=/lib/modules/$(KVER)/build/
# Kernel modules
obj-m := hello.o
# Specify flags for the module compilation
#EXTRA_CFLAGS=-g -O0
all:
$(MAKE) -C $(KDIR) M=$(PWD) modules
clean:
$(MAKE) -C $(KDIR) M=$(PWD) clean
编译运行
将 Makefile 和 hello.c 放在同一目录下,执行 make
即可编译 hello 模块,编译完成后会生成 hello.ko 目标文件。如果开启 EXTRA_CFLAGS=-g -O0
,可以得到包含调试信息的 hello.ko 模块。
make
通过 insmod
命令将模块加载到内核(非 root 用户需要加上 sudo)
insmod hello.ko
如果一个模块包含多个 .c 文件(例如 file1.c 和 file2.c),则应该以如下方式编写 Makefile:
obj-m := modulename.o
modulename-objs := file1.o file2.o