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 函数在初始化完成后会被内核释放回收,以节省内存。
模块生命周期状态机 未加载状态 初始化中 运行中 清理中 加载失败 insmod / modprobe init 返回 0 rmmod exit 执行完毕 init 返回非 0 说明:加载失败状态可由 init 返回负错误码、依赖缺失、符号未解析或硬件不存在等原因触发。 清理中状态保证 exit 函数释放所有已分配资源,避免内存泄漏。

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 模块依赖关系图

模块依赖与符号导出关系 consumer_module exporter_module 内核符号表 依赖(调用导出符号) 注册符号 查找符号 加载顺序:必须先加载 exporter_module,再加载 consumer_module。 卸载顺序相反:先卸载 consumer_module,再卸载 exporter_module,否则内核会阻止卸载。

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 中明确标注依赖关系,便于后续维护。