1. Linux内核模块基础
1.1 什么是内核模块
内核模块(Kernel Module)是一段可以在Linux系统运行时动态加载到内核或从内核卸载的代码片段。它允许在不重新编译整个内核、不重启系统的情况下扩展内核功能。模块通常用于实现设备驱动、文件系统、网络协议、系统调用等内核级功能。
核心特性: 动态加载、无需重启、按需扩展、独立编译。
1.2 模块与应用程序的区别
内核模块运行在内核空间,应用程序运行在用户空间,两者在运行环境、资源访问、开发调试等方面存在本质差异。下表从多个维度进行对比:
| 对比维度 | 内核模块 | 应用程序 |
|---|---|---|
| 运行空间 | 内核空间(Ring 0) | 用户空间(Ring 3) |
| 入口函数 | module_init() / module_exit() |
main() |
| 内存访问 | 直接访问物理内存、内核地址空间 | 只能访问虚拟地址空间,受MMU保护 |
| 系统调用 | 不能直接调用用户态系统调用 | 通过glibc封装调用系统调用 |
| 标准库 | 不能链接libc,只能使用内核API | 可链接任意用户态库 |
| 调试方式 | printk、kgdb、ftrace、kprobe | gdb、strace、valgrind |
| 崩溃影响 | 可能导致整个系统崩溃(Oops/Panic) | 通常只影响当前进程 |
| 浮点运算 | 默认不支持,需手动保存FPU状态 | 正常支持 |
⚠ 关键提醒: 内核模块运行在特权级,一个指针错误就可能导致系统死机。开发模块时必须格外谨慎,做好边界检查和错误处理。
1.3 模块生命周期(init/exit)
每个内核模块都有明确的生命周期,从加载到卸载经历四个阶段:加载 → 初始化 → 运行 → 卸载。其中 module_init() 和 module_exit() 是开发者必须实现的两个核心入口。
💡 记忆口诀: "init 负责初始化,exit 负责清理;加载调 init,卸载调 exit。"
生命周期流程图
标准模块代码框架
一个最简的内核模块至少包含以下结构:
#include <linux/module.h> // 所有模块都需要
#include <linux/kernel.h> // printk 等内核函数
#include <linux/init.h> // __init / __exit 宏
static int __init my_module_init(void)
{
printk(KERN_INFO "Hello, Kernel Module!\n");
// 注册设备、分配资源、初始化硬件等
return 0; // 返回0表示成功,负值表示失败
}
static void __exit my_module_exit(void)
{
printk(KERN_INFO "Goodbye, Kernel Module!\n");
// 释放资源、注销设备、清理状态等
}
module_init(my_module_init);
module_exit(my_module_exit);
MODULE_LICENSE("GPL"); // 许可证声明
MODULE_AUTHOR("Your Name"); // 作者信息
MODULE_DESCRIPTION("A simple module"); // 模块描述
MODULE_VERSION("1.0"); // 版本号
生命周期各阶段详解
- 加载阶段(insmod / modprobe):
- 用户态工具将模块 .ko 文件读入内核空间
- 内核模块加载器解析 ELF 格式,进行符号解析和重定位
- 检查模块依赖和版本信息(vermagic)
- 调用
module_init注册的初始化函数
- 初始化阶段(module_init):
- 执行开发者编写的初始化代码
- 注册设备驱动、分配内存、申请中断、创建文件系统等
- 返回 0 表示成功;返回负值(如 -ENOMEM、-EIO)表示失败
- 初始化失败时,内核自动卸载模块,不会进入运行态
- 运行阶段:
- 模块注册的各类回调函数(如 open/read/write/ioctl)被内核调用
- 模块代码常驻内核空间,直到被卸载
- 此阶段模块可能被多个进程并发访问,需考虑竞态条件
- 卸载阶段(module_exit):
- 用户执行
rmmod或系统关机时触发 - 执行开发者编写的清理代码
- 释放所有已分配的资源,注销设备节点
- 如果模块被其他模块引用,卸载会被拒绝(
rmmod: ERROR: Module is in use)
- 用户执行
📌 最佳实践: init 函数中每分配一个资源,都要考虑 exit 中如何释放。建议采用"goto 错误处理"模式,确保任何失败路径都能正确回滚已分配的资源。
模块引用计数
内核通过 module_refcount 跟踪模块的使用情况。当模块被其他模块依赖、或设备文件被打开时,引用计数会增加。卸载模块前,内核会检查引用计数是否为 0,不为 0 则拒绝卸载。
// 查看模块引用计数
$ lsmod
Module Size Used by
my_driver 16384 1 # 引用计数为1
usb_storage 73728 0
模块参数传递
模块支持在加载时通过参数传递配置信息,方便调试和适配不同硬件:
// 模块代码中声明参数
static int debug_level = 0;
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
static char *device_name = "default";
module_param(device_name, charp, 0644);
MODULE_PARM_DESC(device_name, "Device name string");
// 加载时传递参数
$ sudo insmod my_module.ko debug_level=2 device_name="mydev0"
💡 调试技巧: 使用
modinfo 命令可以查看模块的 license、作者、描述、参数等信息:modinfo my_module.ko。
模块依赖关系
模块之间可以存在依赖。例如,USB 存储驱动依赖于 USB 核心模块。依赖关系由内核自动管理,加载模块时会先加载其依赖模块。
// 查看模块依赖树
$ modprobe --show-depends usb_storage
insmod /lib/modules/5.15.0/kernel/drivers/usb/core/usbcore.ko
insmod /lib/modules/5.15.0/kernel/drivers/scsi/scsi_mod.ko
insmod /lib/modules/5.15.0/kernel/drivers/usb/storage/usb-storage.ko
模块版本校验(vermagic)
内核在加载模块时会校验 vermagic 字符串,确保模块与当前运行的内核版本、编译器版本、SMP/抢占配置等匹配。不匹配时加载会失败:
$ sudo insmod my_module.ko
insmod: ERROR: could not insert module my_module.ko: Invalid module format
$ dmesg | tail
[ 1234.5678] my_module: version magic '5.15.0-generic SMP mod_unload '
should be '5.15.0-rc7+ SMP mod_unload '
⚠ 常见错误: 模块编译环境的内核头文件版本与运行内核版本不一致,是导致 "Invalid module format" 的最常见原因。务必使用
uname -r 确认目标内核版本。