2. Linux内核模块基础:模块加载与卸载、模块参数、模块依赖与符号导出

Linux内核是一个单体内核(Monolithic Kernel),但通过内核模块(Kernel Module)机制实现了动态扩展能力。模块是可以在运行时加载到内核或从内核卸载的代码片段,无需重新编译整个内核或重启系统。本章深入剖析模块的生命周期管理、参数传递、依赖关系与符号导出,为后续驱动开发奠定基础。

核心认知: 内核模块本质上是目标文件(.ko),但运行在内核空间,拥有最高特权级。模块的加载与卸载由 insmod/rmmod 工具触发,内核通过 module_init() 和 module_exit() 宏管理入口/出口函数。

2.1 模块加载与卸载机制

每个内核模块必须声明初始化函数和清理函数。加载时内核调用初始化函数完成资源申请、设备注册等操作;卸载时调用清理函数释放资源。

2.1.1 模块生命周期状态机

模块在内核中经历以下状态转换:

模块生命周期状态机 未加载 加载中 活动 卸载中 错误 insmod init 成功 rmmod exit 成功 回到未加载 init 失败 模块引用计数为0才能卸载

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 展示三个模块之间的依赖链:

模块依赖与符号导出关系 模块 A (core) EXPORT_SYMBOL(func_a) 模块 B (driver) 调用 func_a() 模块 C (test) 调用 func_a() + func_b() 依赖 依赖 间接依赖 加载顺序约束 必须先加载 A → 再加载 B → 最后加载 C 卸载顺序相反:C → B → A

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 接口,并与用户态进行数据交互。