2. Linux内核模块基础:模块加载与卸载、模块参数、模块依赖与符号导出
Linux内核是一个单体内核(Monolithic Kernel),但通过内核模块(Kernel Module)机制实现了动态扩展能力。模块是可以在运行时加载到内核或从内核卸载的代码片段,无需重新编译整个内核或重启系统。本章深入剖析模块的生命周期管理、参数传递、依赖关系与符号导出,为后续驱动开发奠定基础。
核心认知: 内核模块本质上是目标文件(.ko),但运行在内核空间,拥有最高特权级。模块的加载与卸载由
insmod/rmmod 工具触发,内核通过 module_init() 和 module_exit() 宏管理入口/出口函数。
2.1 模块加载与卸载机制
每个内核模块必须声明初始化函数和清理函数。加载时内核调用初始化函数完成资源申请、设备注册等操作;卸载时调用清理函数释放资源。
2.1.1 模块生命周期状态机
模块在内核中经历以下状态转换:
2.1.2 模块加载与卸载命令
| 命令 | 功能 | 依赖处理 | 常用选项 |
|---|---|---|---|
insmod |
直接加载模块文件 | 不自动解决依赖 | -f 强制加载 |
modprobe |
智能加载(含依赖) | 自动加载依赖模块 | -r 卸载,--show-depends |
rmmod |
卸载模块 | 不检查依赖 | -f 强制卸载 |
modinfo |
查看模块信息 | — | -p 查看参数,-d 描述 |
lsmod |
列出已加载模块 | 显示依赖关系 | — |
工程实践: 开发阶段使用
insmod 直接加载;生产部署使用 modprobe 自动处理依赖链。卸载前务必确认模块未被其他模块引用,否则 rmmod 会返回 EBUSY。
2.2 模块参数传递
内核模块支持在加载时通过命令行传递参数,无需重新编译。使用 module_param() 和 module_param_array() 宏声明参数。
2.2.1 参数声明宏
| 宏 | 用途 | 参数类型 | 示例 |
|---|---|---|---|
module_param(name, type, perm) |
声明单个参数 | byte, short, int, long, charp, bool |
module_param(debug, int, 0644); |
module_param_array(name, type, num, perm) |
声明数组参数 | 同上 | module_param_array(ports, int, &count, 0); |
MODULE_PARM_DESC(name, desc) |
参数描述 | 字符串 | MODULE_PARM_DESC(debug, "Enable debug output"); |
2.2.2 参数权限位(perm)
perm 参数控制 /sys/module/<name>/parameters/ 下文件的访问权限:
- 0:不在 sysfs 中暴露,仅加载时可用
- S_IRUGO (0444):只读,所有用户可查看
- S_IWUSR | S_IRUGO (0644):所有者可写,其他只读
- S_IRUSR (0400):仅所有者可读
安全注意: 生产环境避免使用
0644 或更高权限,防止用户态进程意外修改内核参数导致系统不稳定。调试阶段可临时使用 0644 方便测试。
2.2.3 完整参数示例
#include <linux/module.h>
#include <linux/moduleparam.h>
static int debug = 0;
static char *device_name = "default_dev";
static int ports[4] = {100, 200, 300, 400};
static int port_count = 0;
module_param(debug, int, 0644);
MODULE_PARM_DESC(debug, "Debug level (0=off, 1=on)");
module_param(device_name, charp, 0444);
MODULE_PARM_DESC(device_name, "Device name string");
module_param_array(ports, int, &port_count, 0);
MODULE_PARM_DESC(ports, "Array of port numbers");
static int __init my_init(void)
{
int i;
printk(KERN_INFO "Module loaded: debug=%d, name=%s\n", debug, device_name);
for (i = 0; i < port_count; i++)
printk(KERN_INFO " ports[%d] = %d\n", i, ports[i]);
return 0;
}
static void __exit my_exit(void)
{
printk(KERN_INFO "Module unloaded\n");
}
module_init(my_init);
module_exit(my_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Driver Course");
MODULE_DESCRIPTION("Module parameter demo");
加载命令:
sudo insmod mymodule.ko debug=1 device_name="mydev" ports=8080,9090,10000
2.3 模块依赖与符号导出
模块之间可以存在依赖关系:一个模块可能调用另一个模块导出的函数或变量。内核通过符号导出(Symbol Export)机制实现模块间通信。
2.3.1 符号导出宏
| 宏 | 可见性 | 使用场景 |
|---|---|---|
EXPORT_SYMBOL(symbol) |
所有模块可见 | 通用接口,如字符设备操作函数 |
EXPORT_SYMBOL_GPL(symbol) |
仅 GPL 许可模块可见 | 保护知识产权,限制非 GPL 模块使用 |
EXPORT_SYMBOL_NS(symbol, ns) |
命名空间限定 | 内核 5.x+ 引入,避免符号冲突 |
2.3.2 依赖关系图
以下 SVG 展示三个模块之间的依赖链:
2.3.3 查看符号表
使用以下命令查看模块导出的符号:
# 查看内核全局符号表
cat /proc/kallsyms | grep "module_name"
# 查看特定模块的导出符号
nm mymodule.ko | grep "T" # T 表示导出符号
# 查看模块依赖关系
modinfo mymodule.ko | grep "depends"
2.3.4 依赖与符号导出最佳实践
- 最小化导出:只导出必要的接口,避免暴露内部实现细节
- 使用 GPL 保护:商业驱动若不想被非 GPL 模块调用,使用
EXPORT_SYMBOL_GPL - 依赖检查:使用
modprobe --show-depends验证依赖链完整性 - 循环依赖禁止:内核不允许模块间循环依赖,否则加载死锁
- 符号命名规范:使用模块名前缀避免符号冲突,如
mydev_register
工程案例: 某嵌入式项目需要 SPI 控制器驱动(模块 A)和 SPI 触摸屏驱动(模块 B)。A 导出
spi_register_controller() 和 spi_transfer(),B 调用这些函数。使用 EXPORT_SYMBOL_GPL 确保只有 GPL 许可的触摸屏驱动可以使用 SPI 控制器接口,保护核心代码知识产权。
2.4 模块信息与许可证
每个模块应包含元数据描述,这些信息通过 modinfo 命令查看:
| 宏 | 描述 | 示例 |
|---|---|---|
MODULE_LICENSE(license) |
许可证声明 | "GPL", "GPL v2", "Proprietary" |
MODULE_AUTHOR(author) |
作者信息 | "John Doe <john@example.com>" |
MODULE_DESCRIPTION(desc) |
模块功能描述 | "SPI controller driver for i.MX6" |
MODULE_VERSION(ver) |
版本号 | "1.2.3" |
MODULE_ALIAS(alias) |
设备别名,用于自动加载 | "platform:my-device" |
许可证影响: 如果模块声明
"Proprietary" 或非 GPL 兼容许可证,则无法调用 EXPORT_SYMBOL_GPL 导出的符号。内核会打印 tainted 标记,影响社区支持。
2.5 本章小结
内核模块是 Linux 驱动开发的基石。掌握模块加载/卸载流程、参数传递、依赖管理和符号导出,是编写可维护、可扩展驱动的必要条件。关键要点:
- 生命周期:理解
module_init→module_exit的完整状态机 - 参数传递:使用
module_param系列宏实现运行时配置 - 依赖管理:通过
EXPORT_SYMBOL建立模块间接口契约 - 加载顺序:依赖模块必须先加载,卸载顺序相反
- 元数据:完整填写许可证、作者、描述等信息,便于维护和调试
下节预告: 第3章将深入字符设备驱动框架,学习如何注册设备号、实现 file_operations 接口,并与用户态进行数据交互。