2. 内核模块基础:模块加载与卸载
字符设备驱动在内核中以内核模块(Kernel Module)的形式存在。模块是 Linux 内核在运行时动态加载和卸载代码的机制,无需重新编译整个内核或重启系统。理解模块的加载/卸载生命周期、入口/出口函数声明以及许可证声明,是编写任何内核驱动的前提。
2.1 模块生命周期与核心宏
每个内核模块必须向内核注册两个关键函数:初始化函数(在模块加载时执行)和清理函数(在模块卸载时执行)。这两个函数通过 module_init() 和 module_exit() 宏告知内核入口点。
| 宏 | 触发时机 | 典型职责 | 返回值 |
|---|---|---|---|
module_init(x) |
insmod 加载模块时 |
注册设备号、初始化数据结构、注册字符设备 | 成功返回 0,失败返回负错误码 |
module_exit(x) |
rmmod 卸载模块时 |
注销设备号、释放内存、删除 /proc 条目 | void |
关键规则:初始化函数必须返回 0 表示成功;若返回负值(如
-ENOMEM、-EBUSY),内核会认为加载失败并自动撤销所有已分配的资源。清理函数必须彻底释放初始化阶段申请的所有资源,否则会造成资源泄漏。
2.2 最小内核模块代码结构
以下是一个符合内核编码规范的“Hello World”模块,展示了 module_init、module_exit、MODULE_LICENSE 和 MODULE_AUTHOR 的标准用法:
#include <linux/module.h> /* 模块核心宏 */
#include <linux/kernel.h> /* printk 等级 */
#include <linux/init.h> /* __init / __exit 标记 */
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 Engineer <engineer@example.com>");
MODULE_DESCRIPTION("A minimal character device driver example");
MODULE_VERSION("1.0");
编译提示:使用内核源码树中的
Makefile 编译,目标文件为 .ko(kernel object)。加载命令:sudo insmod hello.ko;卸载命令:sudo rmmod hello;查看日志:dmesg | tail。
2.3 __init / __exit 标记与内存优化
在函数声明中使用的 __init 和 __exit 标记并非可有可无。它们指示内核在特定阶段释放函数占用的内存:
__init:标记初始化函数。模块加载完成后,内核会释放该函数占用的代码段内存,减少运行时开销。__exit:标记清理函数。如果模块被编译进内核(非模块方式),该函数会被完全丢弃,因为内置模块无需卸载。
注意:不要对
module_init() 和 module_exit() 参数中的函数名使用 static 以外的存储类修饰符。内核宏已经处理了符号导出,不需要额外加 extern。
2.4 MODULE_LICENSE 与作者信息
内核模块必须声明许可证,否则内核会在加载时打印“tainted kernel”警告。许可证声明影响内核符号的导出范围:
| 许可证宏 | 含义 | 能否使用 EXPORT_SYMBOL_GPL |
|---|---|---|
MODULE_LICENSE("GPL") |
GNU General Public License v2 | 是 |
MODULE_LICENSE("GPL v2") |
GPL v2 专用 | 是 |
MODULE_LICENSE("Dual BSD/GPL") |
BSD 或 GPL 双许可 | 是 |
MODULE_LICENSE("Proprietary") |
专有许可证 | 否(内核标记为“污染”) |
其他可选元信息宏:
MODULE_AUTHOR("name <email>")— 作者姓名与联系方式MODULE_DESCRIPTION("text")— 模块功能简述MODULE_VERSION("x.y")— 版本号MODULE_ALIAS("alias-name")— 模块别名,用于自动加载
2.5 模块加载/卸载流程 SVG 图
下图展示了从用户空间执行 insmod/rmmod 到内核模块初始化/清理函数执行,再到字符设备注册/注销的完整流程:
2.6 模块加载/卸载的常见错误与排查
常见错误 1:初始化函数返回非零值,但未撤销已分配资源。例如先成功注册了设备号,后续内存分配失败返回
解决方法:使用 goto 错误处理模式,在返回前统一释放已获取的资源。
-ENOMEM,导致设备号泄漏。解决方法:使用 goto 错误处理模式,在返回前统一释放已获取的资源。
常见错误 2:卸载模块时,设备文件仍被用户进程打开(open 未 close)。内核会阻止卸载并打印“Device or resource busy”。
解决方法:在
解决方法:在
module_exit 中先检查设备引用计数,或强制关闭(不推荐生产环境)。
调试技巧:使用
lsmod 查看已加载模块列表及引用计数;使用 modinfo hello.ko 查看模块的许可证、作者、描述等元信息;使用 dmesg -w 实时监控内核日志输出。
2.7 模块参数:向内核模块传递配置
除了固定的初始化逻辑,内核模块还支持通过 module_param 宏接收用户空间传递的参数,使驱动行为可配置:
#include <linux/moduleparam.h>
static int debug_level = 0;
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Debug level (0=off, 1=info, 2=verbose)");
static char *device_name = "mychar";
module_param(device_name, charp, 0644);
MODULE_PARM_DESC(device_name, "Character device base name");
加载时传递参数:sudo insmod hello.ko debug_level=2 device_name="mydev"。参数在 /sys/module/hello/parameters/ 下以文件形式暴露,支持运行时读取(权限由第三个参数控制)。
2.8 本章知识结构总结
| 知识点 | 核心函数/宏 | 关键说明 |
|---|---|---|
| 模块入口 | module_init() |
指定初始化函数,返回 0 成功 |
| 模块出口 | module_exit() |
指定清理函数,释放所有资源 |
| 内存优化 | __init / __exit |
标记函数在特定阶段释放代码段 |
| 许可证声明 | MODULE_LICENSE() |
必须声明,否则内核污染 |
| 元信息 | MODULE_AUTHOR 等 |
作者、描述、版本、别名 |
| 模块参数 | module_param() |
向模块传递运行时配置 |