2. 内核模块基础
Linux内核模块是动态加载到内核中的代码片段,用于扩展内核功能而不需要重新编译整个内核或重启系统。理解模块的加载与卸载机制、初始化与退出函数以及许可证声明,是编写可靠驱动程序的基石。
2.1 模块的加载与卸载
内核模块的生命周期由两个核心操作控制:加载(insmod/modprobe)和卸载(rmmod)。加载时,内核将模块的二进制代码复制到内核地址空间,并执行模块的初始化函数;卸载时,内核执行模块的退出函数,并释放相关资源。
| 操作 | 用户态命令 | 内核态回调 | 说明 |
|---|---|---|---|
| 加载 | insmod mymodule.ko |
module_init() 指定的函数 |
将模块插入内核,执行初始化 |
| 卸载 | rmmod mymodule |
module_exit() 指定的函数 |
从内核移除模块,执行清理 |
| 依赖加载 | modprobe mymodule |
自动处理依赖 | 自动加载依赖模块 |
| 查看已加载 | lsmod |
— | 列出当前所有内核模块 |
提示:
modprobe 比 insmod 更智能,它会自动解析并加载模块依赖(如 depmod 生成的依赖关系),推荐在开发后期使用。
2.2 module_init / module_exit 机制
每个内核模块必须声明初始化函数和退出函数,并通过宏 module_init() 和 module_exit() 注册到内核框架中。这两个宏本质上是将函数指针放入特定的 ELF section(.init.text 和 .exit.text),内核在加载/卸载时通过符号表找到并调用它们。
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init mymodule_init(void)
{
printk(KERN_INFO "MyModule: 模块已加载\n");
/* 在此处注册设备、分配资源、初始化定时器等 */
return 0; /* 返回0表示成功,负值表示失败 */
}
static void __exit mymodule_exit(void)
{
printk(KERN_INFO "MyModule: 模块已卸载\n");
/* 在此处释放资源、注销设备等 */
}
module_init(mymodule_init);
module_exit(mymodule_exit);
关键点:
__init标记:初始化函数在运行后会被内核自动丢弃,释放内存。__exit标记:退出函数在模块编译进内核(非模块)时会被忽略。- 初始化函数返回
0表示成功;返回负的错误码(如-ENOMEM)表示加载失败,内核会回滚加载操作。
下面的SVG图展示了模块从用户态命令到内核态回调的完整流程:
2.3 LICENSE 与作者信息
Linux内核是GPL许可证下的开源项目。内核模块必须声明其许可证,否则内核会认为该模块污染了内核,并在加载时打印警告信息。此外,良好的模块应包含作者和描述信息,便于维护和追踪。
MODULE_LICENSE("GPL"); /* 必须:声明许可证 */
MODULE_AUTHOR("Zhang San <zhangsan@example.com>"); /* 推荐:作者信息 */
MODULE_DESCRIPTION("A sample kernel module for timer and interrupt demo"); /* 推荐:模块描述 */
MODULE_VERSION("1.0"); /* 可选:版本号 */
MODULE_ALIAS("mymodule"); /* 可选:模块别名 */
| 宏 | 参数示例 | 说明 |
|---|---|---|
MODULE_LICENSE |
"GPL", "GPL v2", "Dual BSD/GPL" |
声明许可证类型;非GPL兼容许可证会导致内核标记为“污染” |
MODULE_AUTHOR |
"Name <email>" |
标识模块作者,便于问题反馈 |
MODULE_DESCRIPTION |
"简短描述" |
说明模块功能,modinfo 命令可查看 |
MODULE_VERSION |
"1.0" |
版本号,用于模块版本管理 |
MODULE_ALIAS |
"char-major-200-*" |
为模块提供别名,便于自动加载 |
警告: 如果模块使用非GPL兼容许可证(如
"Proprietary"),内核将禁止该模块调用标记为 EXPORT_SYMBOL_GPL 的符号。这会导致许多核心内核API不可用,强烈建议驱动开发者使用 "GPL" 或 "Dual BSD/GPL"。
下面的SVG图展示了模块元数据与内核符号导出机制之间的关系:
2.4 完整的最小模块示例
将上述所有元素组合在一起,得到一个完整的最小内核模块:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello: 模块加载成功\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Hello: 模块卸载成功\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Expert <expert@kernel.org>");
MODULE_DESCRIPTION("Minimal hello world kernel module");
MODULE_VERSION("1.0");
编译与测试: 使用内核构建系统(Kbuild)编译此模块,生成
hello.ko。执行 sudo insmod hello.ko 加载,dmesg | tail 查看内核日志;执行 sudo rmmod hello 卸载,再次查看日志确认退出函数被调用。
2.5 常见错误与调试
- 加载失败: 使用
dmesg查看内核日志,定位初始化函数返回的错误码。 - 卸载失败: 模块被其他模块或进程引用时无法卸载,使用
lsmod查看引用计数。 - 许可证污染: 内核日志出现
module license 'unspecified' taints kernel,需添加MODULE_LICENSE("GPL")。 - 符号未找到: 模块依赖的符号未导出或未加载,使用
modinfo查看依赖,modprobe自动处理。