内核模块编程:从设备驱动到Netfilter规则编写 - 第1节
1.1 内核模块概念
Linux 内核模块(Loadable Kernel Module,LKM)是一段可以在运行时动态加载到内核或从内核卸载的代码。它允许在不重新编译整个内核、不重启系统的情况下扩展内核功能。模块机制是 Linux 内核可扩展性和动态性的核心体现。
核心思想: 内核模块将内核从“静态单体”转变为“动态可组合”的体系结构。驱动、文件系统、网络协议等都可以作为模块按需加载。
模块的本质
- 目标文件: 模块本质上是一个经过编译但未链接的
.ko(Kernel Object)文件。 - 内核空间执行: 模块代码运行在内核空间,拥有最高特权级(Ring 0)。
- 符号导出: 模块可以导出符号供其他模块使用,也可以使用内核或其他模块导出的符号。
- 无独立地址空间: 模块直接映射到内核地址空间,共享内核的全局数据。
1.2 模块与应用程序的区别
内核模块虽然也是“程序”,但与用户空间的应用程序有着本质区别。理解这些差异是编写安全、稳定模块的前提。
| 对比维度 | 内核模块 | 用户空间应用程序 |
|---|---|---|
| 运行空间 | 内核空间(Ring 0) | 用户空间(Ring 3) |
| 入口/出口 | module_init() / module_exit() |
main() / exit() |
| 内存访问 | 直接访问物理内存、内核数据结构 | 只能访问虚拟地址空间,通过系统调用访问内核 |
| 库支持 | 不能链接标准 C 库(如 glibc) | 可链接任意用户空间库 |
| 浮点运算 | 默认禁止,需手动保存/恢复 FPU 状态 | 正常使用 |
| 错误处理 | 错误通常导致内核 Oops 或系统崩溃 | 错误通常导致进程崩溃,不影响其他进程 |
| 调试难度 | 高,依赖 printk、kgdb、kdump |
低,可使用 gdb、valgrind 等工具 |
| 并发环境 | 多核抢占、中断上下文、软中断 | 进程调度、线程同步 |
⚠️ 安全警示: 内核模块运行在最高特权级,一个空指针解引用或缓冲区溢出就可能导致整个系统崩溃或被攻击者利用提权。编写模块时必须时刻考虑安全性和稳定性。
关键差异详解
- 无标准库: 模块只能使用内核导出的 API,如
printk(替代 printf)、kmalloc(替代 malloc)、copy_from_user(替代 memcpy 从用户空间拷贝)。 - 无 main 函数: 模块通过
module_init宏指定初始化函数,通过module_exit宏指定清理函数。 - 无独立进程: 模块没有自己的进程上下文,它运行在调用它的进程上下文中(如系统调用)或中断上下文中。
1.3 模块生命周期:init / exit
每个内核模块都有明确的生命周期,由两个核心函数控制:初始化函数和清理函数。理解这个生命周期是模块编程的第一步。
初始化函数(module_init)
当使用 insmod 或 modprobe 加载模块时,内核会执行由 module_init 宏指定的函数。该函数负责模块的初始化工作。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init my_module_init(void)
{
printk(KERN_INFO "Hello, Kernel Module!\n");
printk(KERN_INFO "Module loaded successfully.\n");
return 0; // 返回 0 表示成功,返回负数表示失败
}
module_init(my_module_init);
💡 关键点:
__init标记表示该函数仅在初始化阶段使用,初始化完成后内核会释放其占用的内存。- 返回值:返回 0 表示初始化成功;返回负数(如
-ENOMEM、-EINVAL)表示失败,模块加载将被中止。 - 初始化失败时,内核会自动回滚已分配的资源,但模块作者应尽量在失败路径中手动释放资源。
清理函数(module_exit)
当使用 rmmod 卸载模块时,内核会执行由 module_exit 宏指定的函数。该函数负责释放模块占用的所有资源。
static void __exit my_module_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel Module!\n");
printk(KERN_INFO "Module unloaded.\n");
}
module_exit(my_module_exit);
⚠️ 重要规则:
__exit标记表示该函数仅在模块卸载时使用。如果模块被编译进内核(非模块方式),该函数会被丢弃。- 清理函数必须释放所有在初始化函数中分配的资源:内存、设备号、proc 文件、sysfs 条目、Netfilter 钩子等。
- 如果清理函数未能正确释放资源,可能导致内核资源泄漏,严重时影响系统稳定性。
模块许可证与描述信息
每个模块还应包含许可证声明和描述信息,这是内核社区的要求,也是模块能够使用某些内核符号的前提。
MODULE_LICENSE("GPL"); // 必须声明,否则内核会标记为“污染”
MODULE_AUTHOR("Your Name <email@example.com>");
MODULE_DESCRIPTION("A simple example kernel module");
MODULE_VERSION("1.0");
| 宏 | 用途 | 说明 |
|---|---|---|
MODULE_LICENSE |
声明许可证 | 常用值:"GPL"、"GPL v2"、"Dual BSD/GPL" 等 |
MODULE_AUTHOR |
声明作者 | 建议包含姓名和联系方式 |
MODULE_DESCRIPTION |
模块功能描述 | 简要说明模块的作用 |
MODULE_VERSION |
版本号 | 遵循语义化版本规范 |
完整的最小模块示例
将以上代码组合,就构成了一个完整的内核模块:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello: module loaded\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Hello: module unloaded\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Expert <kernel@example.com>");
MODULE_DESCRIPTION("A minimal Hello World kernel module");
MODULE_VERSION("1.0");
模块的 Makefile
编译内核模块需要特定的 Makefile 结构,利用内核的构建系统(Kbuild):
obj-m += hello.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
📋 操作步骤:
- 将上述代码保存为
hello.c,Makefile 保存为Makefile。 - 执行
make编译,生成hello.ko。 - 执行
sudo insmod hello.ko加载模块。 - 执行
dmesg | tail查看内核日志,确认 "Hello: module loaded" 输出。 - 执行
sudo rmmod hello卸载模块。 - 再次执行
dmesg | tail查看 "Hello: module unloaded" 输出。
模块状态查看
使用 lsmod 命令可以查看当前系统中已加载的所有模块及其依赖关系:
$ lsmod
Module Size Used by
hello 16384 0
ext4 692224 1
...
| 列名 | 含义 |
|---|---|
| Module | 模块名称 |
| Size | 模块占用的内存大小(字节) |
| Used by | 被其他模块或内核引用的次数,以及引用者列表 |
💡 调试技巧:
- 使用
modinfo hello.ko查看模块的详细信息(许可证、作者、描述等)。 - 使用
dmesg -w实时监控内核日志,方便调试模块的 printk 输出。 - 如果模块加载失败,
dmesg通常会显示失败原因(如 "Unknown symbol"、"Invalid module format")。