2. Linux 内核模块基础
Linux 内核采用宏内核架构,但通过内核模块(Loadable Kernel Module, LKM)机制实现了运行时的动态扩展能力。在嵌入式 ARM Linux 驱动开发中,绝大多数设备驱动、文件系统和协议栈都以模块形式存在。理解模块的加载/卸载、参数传递和依赖管理,是编写可维护、可移植 HAL 适配层的前提。
2.1 模块概念与生命周期
内核模块是一段可以在内核空间独立编译、独立加载和卸载的目标代码(.ko 文件)。与直接编译进内核(built-in)相比,模块化具有以下优势:
- 按需加载:仅在硬件存在或功能被使用时才加载,节省内存和启动时间。
- 快速迭代:驱动开发阶段无需反复编译整个内核和重启系统。
- 热插拔支持:配合
udev或mdev,在设备插入/移除时自动加载/卸载对应模块。 - 版本隔离:模块与内核通过
modversions机制校验符号版本,避免 ABI 不兼容。
一个标准内核模块的入口和出口由两个宏声明:
#include <linux/module.h>
#include <linux/init.h>
static int __init my_driver_init(void)
{
pr_info("My HAL driver loaded\n");
return 0;
}
static void __exit my_driver_exit(void)
{
pr_info("My HAL driver unloaded\n");
}
module_init(my_driver_init);
module_exit(my_driver_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded HAL Team");
MODULE_DESCRIPTION("Sample HAL adapter for ARM platform");
模块的生命周期分为四个阶段:编译 → 加载 → 运行 → 卸载。下图展示了模块从用户空间到内核空间的完整流程:
2.2 模块加载与卸载
Linux 提供两组工具用于模块管理:传统工具(insmod、rmmod、lsmod)和依赖感知工具(modprobe、modinfo)。在嵌入式 ARM 系统中,modprobe 是推荐方式,因为它会自动解析并加载依赖模块。
| 命令 | 功能 | 依赖处理 | 嵌入式常用场景 |
|---|---|---|---|
insmod <module.ko> |
直接加载指定模块文件 | 不处理依赖 | 调试单个模块 |
rmmod <module> |
卸载指定模块 | 检查是否有其他模块依赖 | 热插拔设备移除 |
modprobe <module> |
加载模块及其所有依赖 | 自动解析 modules.dep |
生产环境、启动脚本 |
modprobe -r <module> |
卸载模块及其未使用的依赖 | 递归卸载 | 清理驱动栈 |
lsmod |
列出当前已加载模块 | 显示模块间依赖关系 | 诊断驱动状态 |
modprobe 依赖 /lib/modules/$(uname -r)/ 目录下的 modules.dep 文件。交叉编译后必须执行 depmod 生成该文件,否则 modprobe 会报 "module not found" 错误。
2.3 模块参数传递
内核模块支持在加载时通过命令行传递参数,无需重新编译。这对于 HAL 适配层非常有用——例如在加载 I2C 触摸屏驱动时,动态指定中断引脚或 I2C 地址。
参数使用 module_param() 和 module_param_array() 宏声明:
#include <linux/moduleparam.h>
static int irq_num = 100; // 默认中断号
static char *i2c_bus = "i2c-1"; // 默认 I2C 总线
static int gpio_pins[4] = {0}; // GPIO 引脚数组
static int gpio_count = 0; // 数组实际元素个数
module_param(irq_num, int, 0644);
module_param(i2c_bus, charp, 0644);
module_param_array(gpio_pins, int, &gpio_count, 0644);
MODULE_PARM_DESC(irq_num, "IRQ number for HAL device");
MODULE_PARM_DESC(i2c_bus, "I2C bus name (e.g. i2c-0, i2c-1)");
MODULE_PARM_DESC(gpio_pins, "GPIO pin list (up to 4 pins)");
加载时传递参数:
# 传递标量参数
insmod hal_touch.ko irq_num=42 i2c_bus="i2c-0"
# 传递数组参数
insmod hal_touch.ko gpio_pins=17,22,25
# 使用 modprobe 传递参数(语法相同)
modprobe hal_touch irq_num=42
参数权限位(如 0644)控制 /sys/module/<name>/parameters/ 下的文件权限,允许运行时通过 sysfs 查看或修改参数值。
| 参数类型 | 声明宏 | sysfs 可读/写 | 典型用途 |
|---|---|---|---|
int |
module_param(name, int, perm) |
是 | 中断号、DMA 通道 |
charp |
module_param(name, charp, perm) |
是 | 设备节点名、总线名 |
bool |
module_param(name, bool, perm) |
是 | 调试开关、特性使能 |
int 数组 |
module_param_array(name, int, &count, perm) |
是 | GPIO 引脚列表、寄存器地址 |
2.4 模块依赖关系
模块依赖是 Linux 内核模块管理的核心机制。当一个模块调用另一个模块导出的符号(函数或变量)时,就形成了依赖关系。依赖关系通过 EXPORT_SYMBOL 和 EXPORT_SYMBOL_GPL 宏建立。
典型的 HAL 驱动依赖链示例:
// i2c_core.c — 导出 I2C 核心函数
#include <linux/module.h>
#include <linux/i2c.h>
int i2c_master_send(struct i2c_client *client, const char *buf, int count)
{
// ... 实现
}
EXPORT_SYMBOL(i2c_master_send);
// hal_touch.c — 依赖 i2c_core 模块
#include <linux/module.h>
#include <linux/i2c.h>
static int touch_probe(struct i2c_client *client)
{
char cmd[] = {0x01, 0x00};
i2c_master_send(client, cmd, 2); // 调用导出符号
return 0;
}
依赖关系在编译时由 depmod 工具分析并记录在 modules.dep 文件中。加载 hal_touch 时,modprobe 会先加载 i2c_core,再加载 hal_touch。
查看依赖关系的常用命令:
# 查看模块详细信息(包含依赖)
modinfo hal_touch.ko
# 查看已加载模块的依赖关系
lsmod
# 手动查看依赖文件
cat /lib/modules/$(uname -r)/modules.dep | grep hal_touch
modules.dep 中清晰可见,便于调试和移植。
2.5 模块信息与许可证
每个内核模块应包含元数据描述,这些信息通过 MODULE_* 宏声明,并可通过 modinfo 命令查看:
MODULE_LICENSE("GPL"); // 必须:影响符号导出范围
MODULE_AUTHOR("HAL Team <hal@arm.com>");
MODULE_DESCRIPTION("HAL adapter for ARM SPI touch controller");
MODULE_VERSION("1.2.0");
MODULE_ALIAS("platform:hal-spi-touch"); // 用于自动加载的别名
许可证选择直接影响 EXPORT_SYMBOL_GPL 导出的符号是否可用。在商业嵌入式项目中,如果 HAL 层使用 GPL 许可证,则整个驱动栈必须开源;若使用 MODULE_LICENSE("Proprietary"),则无法调用 GPL-only 的内核符号。
| 许可证宏 | 含义 | 能否调用 EXPORT_SYMBOL_GPL | 常见场景 |
|---|---|---|---|
"GPL" |
GNU General Public License v2 | 是 | 开源驱动、主线内核贡献 |
"GPL v2" |
GPL v2 专用 | 是 | 与内核版本严格一致 |
"Proprietary" |
闭源专有 | 否 | 商业 HAL 适配、NDA 硬件 |
"Dual BSD/GPL" |
双许可证 | 是 | 兼容性中间件 |
EXPORT_SYMBOL_GPL 符号(如 i2c_transfer、spi_sync),则模块必须声明为 GPL 兼容许可证,否则加载时会报 "module uses symbol from GPL-only, but is not GPL" 错误。