2、Linux内核模块基础:模块概念、模块生命周期(init/exit)、模块依赖与符号导出
2.1 内核模块概念
Linux内核模块(Loadable Kernel Module,LKM)是一种可以在运行时动态加载到内核或从内核卸载的代码片段。它允许在不重新编译整个内核、不重启系统的情况下扩展内核功能,是设备驱动、文件系统、网络协议等内核功能的主要载体。
核心优势:动态加载机制使内核功能“按需装配”,既保持内核镜像的精简,又具备灵活的扩展能力。
| 对比维度 | 模块化内核 | 单体内核 |
|---|---|---|
| 镜像大小 | 小(基础内核 + 按需加载模块) | 大(所有功能静态编译) |
| 开发效率 | 模块独立编译,快速迭代 | 每次修改需重新编译整个内核 |
| 运行时灵活性 | 支持热插拔、动态加载/卸载 | 功能固定,无法运行时变更 |
| 内存占用 | 按需加载,节省内存 | 所有功能常驻内存 |
| 调试难度 | 模块级隔离,可单独调试 | 全内核调试,复杂度高 |
一个典型的内核模块源码结构包含以下要素:
- 头文件:
<linux/module.h>(模块API)、<linux/kernel.h>(内核打印等) - 模块许可证声明:
MODULE_LICENSE("GPL") - 模块作者/描述:
MODULE_AUTHOR()、MODULE_DESCRIPTION() - 初始化入口:
module_init()宏注册的函数 - 清理出口:
module_exit()宏注册的函数
// simple_module.c — 最小内核模块示例
#include <linux/module.h>
#include <linux/kernel.h>
static int __init simple_init(void)
{
printk(KERN_INFO "Simple module loaded.\n");
return 0; // 0 表示加载成功
}
static void __exit simple_exit(void)
{
printk(KERN_INFO "Simple module unloaded.\n");
}
module_init(simple_init);
module_exit(simple_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Expert");
MODULE_DESCRIPTION("A minimal loadable kernel module example");
2.2 模块生命周期:init / exit
每个内核模块都经历 加载 → 初始化 → 运行 → 卸载 → 清理 的完整生命周期。内核通过 module_init() 和 module_exit() 两个宏来注册模块的进入和退出函数。
关键点:
__init 和 __exit 标记不仅用于语义区分,还会影响代码的内存布局——__init 函数在初始化完成后会被内核释放回收,以节省内存。
2.3 模块依赖与符号导出
模块之间可以存在依赖关系:一个模块可能调用另一个模块导出的函数或变量。内核通过 符号导出(Symbol Exporting) 机制控制哪些符号可以被其他模块访问。
2.3.1 符号导出机制
内核使用以下宏控制符号的可见性:
| 宏 | 作用 | 可见范围 |
|---|---|---|
EXPORT_SYMBOL(func) |
导出符号,所有模块均可使用 | 所有模块 |
EXPORT_SYMBOL_GPL(func) |
仅导出给 GPL 许可的模块 | 仅 GPL 模块 |
static 函数/变量 |
不导出,仅本文件可见 | 本编译单元 |
// exporter_module.c — 导出符号供其他模块使用
#include <linux/module.h>
#include <linux/kernel.h>
int shared_counter = 0;
EXPORT_SYMBOL_GPL(shared_counter);
int increment_counter(void)
{
return ++shared_counter;
}
EXPORT_SYMBOL_GPL(increment_counter);
static int __init exporter_init(void)
{
printk(KERN_INFO "Exporter module loaded.\n");
return 0;
}
static void __exit exporter_exit(void)
{
printk(KERN_INFO "Exporter module unloaded.\n");
}
module_init(exporter_init);
module_exit(exporter_exit);
MODULE_LICENSE("GPL");
// consumer_module.c — 使用导出的符号
#include <linux/module.h>
#include <linux/kernel.h>
extern int shared_counter;
extern int increment_counter(void);
static int __init consumer_init(void)
{
int val = increment_counter();
printk(KERN_INFO "Consumer: shared_counter = %d\n", val);
return 0;
}
static void __exit consumer_exit(void)
{
printk(KERN_INFO "Consumer module unloaded.\n");
}
module_init(consumer_init);
module_exit(consumer_exit);
MODULE_LICENSE("GPL");
依赖加载顺序:使用
modprobe 加载模块时,它会自动解析 depmod 生成的依赖关系,先加载被依赖模块,再加载当前模块。如果使用 insmod 手动加载,必须确保依赖模块已先行加载,否则会报 Unknown symbol 错误。
2.3.2 模块依赖关系图
2.3.3 依赖管理命令
| 命令 | 功能 | 依赖处理 |
|---|---|---|
insmod module.ko |
直接插入模块 | 不自动处理依赖,失败时返回 Unknown symbol |
rmmod module |
移除模块 | 检查是否有其他模块依赖,有则拒绝卸载 |
modprobe module |
智能加载/卸载 | 自动解析 /lib/modules/$(uname -r)/modules.dep,按序加载所有依赖 |
depmod -a |
生成依赖关系文件 | 扫描所有模块的符号导出/引用,生成 modules.dep |
modinfo module.ko |
查看模块信息 | 显示模块作者、描述、依赖、参数等元数据 |
调试技巧:当模块加载失败时,使用
dmesg | tail -20 查看内核日志。常见的 Unknown symbol 错误会明确提示缺失的符号名称,帮助定位未加载的依赖模块。
2.4 模块加载失败常见原因(与依赖相关)
- 符号未导出:被依赖的符号未使用
EXPORT_SYMBOL或EXPORT_SYMBOL_GPL导出。 - 许可证不匹配:使用
EXPORT_SYMBOL_GPL导出的符号,非 GPL 模块无法引用。 - 依赖顺序错误:使用
insmod时未先加载依赖模块。 - 模块版本不匹配:模块编译时的内核版本与运行内核版本不一致(
vermagic校验失败)。 - 循环依赖:模块 A 依赖 B,B 又依赖 A,内核加载器无法解析。
# 查看模块依赖关系示例
$ modinfo ext4.ko
filename: ext4.ko
license: GPL
description: Fourth Extended Filesystem
depends: mbcache,jbd2,crc16,crc32c_generic # 依赖模块列表
vermagic: 5.15.0-generic SMP mod_unload modversions
# 查看已加载模块的依赖树
$ lsmod | grep ext4
ext4 786432 1
mbcache 16384 1 ext4
jbd2 163840 1 ext4
crc16 16384 1 ext4
crc32c_generic 16384 1 ext4
最佳实践:在开发自定义模块时,始终使用
modprobe 替代 insmod 进行加载测试,让工具自动处理依赖关系。同时,在模块的 MODULE_DESCRIPTION 中明确标注依赖关系,便于后续维护。