1. Linux内核模块基础
1.1 内核模块概念
Linux内核模块(Loadable Kernel Module, LKM)是一段可以在系统运行时动态加载到内核空间并执行的代码。它允许在不重新编译整个内核、不重启系统的情况下扩展内核功能,是Linux设备驱动开发的核心载体。
核心定义: 内核模块是运行在内核空间、拥有最高CPU特权级(Ring 0)的可动态装卸的二进制目标文件(.ko)。
1.2 模块与应用程序的本质区别
| 对比维度 | 内核模块 | 用户态应用程序 |
|---|---|---|
| 运行空间 | 内核空间(Ring 0) | 用户空间(Ring 3) |
| 入口/出口 | module_init() / module_exit() |
main() |
| 内存访问 | 直接访问物理内存、内核数据结构 | 通过虚拟地址,受MMU保护 |
| 库支持 | 只能使用内核导出的API(如printk) |
可使用glibc等用户态库 |
| 浮点运算 | 默认禁止,需手动保存/恢复FPU状态 | 正常支持 |
| 崩溃影响 | 导致整个系统宕机(kernel panic) | 仅终止当前进程 |
| 调试难度 | 高(依赖kgdb、kdump、ftrace) |
低(gdb、Valgrind等工具成熟) |
⚠️ 开发警示: 内核模块运行在Ring 0特权级,一个空指针解引用就会触发
Oops甚至kernel panic。编写模块时必须对每一处指针访问、内存拷贝做边界检查。
1.3 模块加载与卸载流程
内核模块的生命周期由insmod(加载)和rmmod(卸载)两个用户态工具驱动,背后涉及系统调用、内核符号解析和模块引用计数等关键步骤。
加载流程(insmod)
- 文件读取:
insmod将.ko文件读入用户空间缓冲区。 - 系统调用: 通过
init_module()或finit_module()系统调用进入内核。 - ELF解析: 内核解析模块的ELF节区,识别代码段、数据段、符号表。
- 符号解析: 将模块中引用的外部符号(如
printk)与内核符号表(/proc/kallsyms)匹配。 - 内存分配: 在内核空间分配连续虚拟内存,将模块代码和数据拷贝到目标地址。
- 重定位: 根据加载基地址修正模块内部的绝对地址引用。
- 调用初始化: 执行
module_init()指定的初始化函数。 - 注册完成: 模块加入内核模块链表,引用计数置为0。
卸载流程(rmmod)
- 引用检查: 内核检查模块引用计数是否为0(
try_module_get()计数)。 - 调用清理: 执行
module_exit()指定的清理函数(如注销设备、释放内存)。 - 符号清理: 从内核符号表中移除该模块导出的符号。
- 内存释放: 释放模块占用的内核内存。
- 链表移除: 将模块从内核模块链表中摘除。
💡 实用技巧: 使用
lsmod 查看当前已加载模块列表;modinfo <module_name> 查看模块作者、许可证、参数等元信息;modprobe 可自动处理模块依赖关系。
1.4 最小内核模块代码结构
一个完整的内核模块至少包含许可证声明、初始化入口和清理出口。以下是最简模板:
#include <linux/module.h> // 所有模块都需要
#include <linux/kernel.h> // printk、KERN_INFO等
#include <linux/init.h> // __init、__exit宏
static int __init my_module_init(void)
{
printk(KERN_INFO "Hello, kernel module loaded!\n");
return 0; // 0表示成功,负数表示失败
}
static void __exit my_module_exit(void)
{
printk(KERN_INFO "Goodbye, kernel module unloaded!\n");
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL"); // 必须声明
MODULE_AUTHOR("Your Name");
MODULE_DESCRIPTION("A minimal kernel module example");
MODULE_VERSION("1.0");
🔑 关键点:
__init标记的初始化函数在调用后会被内核自动释放,以节省内存。__exit标记的清理函数在模块被静态编译进内核时会被忽略。MODULE_LICENSE("GPL")是必须的,否则内核会标记模块为“污染”(tainted)。printk是内核空间的“printf”,输出可通过dmesg查看。
1.5 模块编译与实验
使用内核构建系统(Kbuild)编译模块,需要准备 Makefile:
obj-m := my_module.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
编译后生成 my_module.ko,执行以下命令验证:
# 加载模块
sudo insmod my_module.ko
# 查看内核日志
dmesg | tail -5
# 确认模块已加载
lsmod | grep my_module
# 卸载模块
sudo rmmod my_module
# 再次查看日志确认卸载
dmesg | tail -5
📌 实验输出示例:
[ 1234.567890] Hello, kernel module loaded![ 5678.901234] Goodbye, kernel module unloaded!
1.6 本章小结
- 内核模块是动态加载到内核空间的代码片段,用于扩展内核功能而不需重启系统。
- 模块与应用程序在运行空间、入口函数、库支持、崩溃影响等方面有本质区别。
- 加载流程涉及ELF解析、符号解析、内存分配、重定位和初始化函数调用。
- 卸载流程依赖引用计数检查,确保模块不再被使用时才释放资源。
- 最小模块必须包含
module_init、module_exit和MODULE_LICENSE。