2. 内核模块基础:模块的加载与卸载、module_init/module_exit机制、MODULE_LICENSE/MODULE_AUTHOR宏
内核模块是Linux内核动态扩展能力的核心载体。在编写proc文件系统驱动之前,必须透彻理解模块的生命周期管理、入口/出口函数注册机制以及元信息声明规范。本章从工程实战角度拆解这些基础要素,并给出可直接编译验证的代码模板。
2.1 模块生命周期:加载与卸载
一个内核模块在系统中的完整生命周期包含四个阶段:加载 → 初始化 → 运行 → 卸载。用户态通过insmod和rmmod命令触发内核态的模块加载与卸载流程。
关键路径:
insmod → 系统调用 init_module → 内核执行模块构造函数 → 模块进入“存活”状态;rmmod → 系统调用 delete_module → 内核执行模块析构函数 → 模块从内存中移除。
工程提示: 使用
lsmod 可查看当前已加载模块列表及依赖关系;modinfo 可读取模块的元信息(LICENSE、AUTHOR、DESCRIPTION等)。
2.2 module_init / module_exit 机制
Linux内核使用两个宏——module_init() 和 module_exit()——来声明模块的入口函数和出口函数。这两个宏在编译时会被展开为特定的段属性,使得内核在加载/卸载时能够自动找到并调用对应的函数指针。
| 宏 | 触发时机 | 函数原型 | 典型用途 |
|---|---|---|---|
module_init(x) |
模块加载时(insmod) | int __init x(void) |
注册设备、分配资源、创建proc条目 |
module_exit(x) |
模块卸载时(rmmod) | void __exit x(void) |
注销设备、释放资源、移除proc条目 |
常见错误: 如果
module_init() 返回非0值,内核认为模块初始化失败,不会将模块标记为“存活”状态,且会自动回滚已分配的部分资源。此时 module_exit() 不会被调用,因此初始化函数中必须做好错误路径的清理工作。
2.3 MODULE_LICENSE / MODULE_AUTHOR 宏
这些宏用于向内核和用户空间声明模块的元信息。它们被编译到模块的 .modinfo 段中,可通过 modinfo 命令读取。
| 宏 | 参数 | 作用 | 注意事项 |
|---|---|---|---|
MODULE_LICENSE(license) |
字符串,如 "GPL" | 声明模块许可证,影响内核是否允许使用GPL-only符号 | 非GPL兼容许可证可能导致内核拒绝加载某些功能 |
MODULE_AUTHOR(author) |
字符串,如 "Zhang San" | 声明模块作者信息 | 纯信息性,不影响功能 |
MODULE_DESCRIPTION(desc) |
字符串 | 模块功能描述 | 建议填写,便于维护 |
MODULE_VERSION(ver) |
字符串,如 "1.0" | 模块版本号 | 用于版本追踪 |
许可证选择建议: 绝大多数内核驱动使用
"GPL" 或 "GPL v2"。如果模块需要调用 EXPORT_SYMBOL_GPL 导出的内核符号,则必须声明为 GPL 兼容许可证,否则链接阶段会报错。
2.4 完整工程模板:最小proc模块
下面给出一个可直接编译运行的内核模块模板,它实现了基本的加载/卸载流程,并声明了完整的元信息。该模板是后续章节创建proc文件系统的基础骨架。
/* minimal_proc_module.c */
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/proc_fs.h>
#include <linux/uaccess.h>
#define PROC_NAME "myproc_demo"
static int __init myproc_init(void)
{
int ret = 0;
pr_info("myproc: module loading...\n");
/* 在 /proc 下创建一个条目 */
/* 具体实现将在后续章节展开 */
pr_info("myproc: /proc/%s created\n", PROC_NAME);
return ret;
}
static void __exit myproc_exit(void)
{
/* 移除 /proc 下的条目 */
pr_info("myproc: /proc/%s removed\n", PROC_NAME);
pr_info("myproc: module unloading...\n");
}
module_init(myproc_init);
module_exit(myproc_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Driver Expert <expert@example.com>");
MODULE_DESCRIPTION("Minimal proc filesystem demo module");
MODULE_VERSION("1.0");
编译与测试: 将上述代码与一个简单的
Makefile 配合(obj-m += minimal_proc_module.o),执行 make -C /lib/modules/$(uname -r)/build M=$(pwd) modules 编译。然后 sudo insmod minimal_proc_module.ko 加载,sudo rmmod minimal_proc_module 卸载,通过 dmesg | tail 查看内核日志输出。
2.5 常见问题与避坑指南
- 模块加载失败(insmod: ERROR: could not insert module):检查
dmesg查看具体错误;常见原因包括内核版本不匹配、符号未导出、依赖模块未加载。 - 模块卸载时系统崩溃(kernel oops):通常是因为
module_exit()中释放了仍在被其他模块或进程使用的资源。务必在卸载前确认引用计数归零。 - MODULE_LICENSE 声明为 "Proprietary" 导致某些内核功能不可用:如果模块使用了
EXPORT_SYMBOL_GPL导出的符号,内核会拒绝加载非GPL模块。 - __init 和 __exit 标记的作用:
__init标记的函数在初始化完成后会被内核回收内存;__exit标记的函数在模块编译进内核(非模块方式)时会被丢弃。不要在这些函数中放置需要持久存在的逻辑。
调试建议: 在
module_init() 和 module_exit() 函数中始终使用 pr_info() 或 printk() 输出关键日志,包括函数进入、参数值、错误码。这是内核驱动开发中最基础也最有效的调试手段。