2. Linux 内核模块基础

Linux 内核采用宏内核架构,但通过内核模块(Loadable Kernel Module, LKM)机制实现了运行时的动态扩展能力。在嵌入式 ARM Linux 驱动开发中,绝大多数设备驱动、文件系统和协议栈都以模块形式存在。理解模块的加载/卸载、参数传递和依赖管理,是编写可维护、可移植 HAL 适配层的前提。

2.1 模块概念与生命周期

内核模块是一段可以在内核空间独立编译、独立加载和卸载的目标代码(.ko 文件)。与直接编译进内核(built-in)相比,模块化具有以下优势:

  • 按需加载:仅在硬件存在或功能被使用时才加载,节省内存和启动时间。
  • 快速迭代:驱动开发阶段无需反复编译整个内核和重启系统。
  • 热插拔支持:配合 udev 或 mdev,在设备插入/移除时自动加载/卸载对应模块。
  • 版本隔离:模块与内核通过 modversions 机制校验符号版本,避免 ABI 不兼容。
嵌入式场景提示:在资源受限的 ARM 目标板上,建议将基础驱动(如串口、MMC、NAND Flash)编译为 built-in,将外设驱动(如 USB、Wi-Fi、传感器)编译为模块,以平衡启动速度与灵活性。

一个标准内核模块的入口和出口由两个宏声明:

#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");

模块的生命周期分为四个阶段:编译 → 加载 → 运行 → 卸载。下图展示了模块从用户空间到内核空间的完整流程:

内核模块生命周期与加载/卸载流程 编译阶段 Makefile → .ko 加载阶段 insmod / modprobe 运行阶段 驱动接口调用 卸载阶段 rmmod 内核空间 insmod modprobe rmmod modinfo 用户空间工具通过系统调用(init_module / delete_module)与内核交互 关键点:module_init() 在加载时执行,module_exit() 在卸载时执行,两者必须配对

2.2 模块加载与卸载

Linux 提供两组工具用于模块管理:传统工具(insmod、rmmod、lsmod)和依赖感知工具(modprobe、modinfo)。在嵌入式 ARM 系统中,modprobe 是推荐方式,因为它会自动解析并加载依赖模块。

命令 功能 依赖处理 嵌入式常用场景
insmod <module.ko> 直接加载指定模块文件 不处理依赖 调试单个模块
rmmod <module> 卸载指定模块 检查是否有其他模块依赖 热插拔设备移除
modprobe <module> 加载模块及其所有依赖 自动解析 modules.dep 生产环境、启动脚本
modprobe -r <module> 卸载模块及其未使用的依赖 递归卸载 清理驱动栈
lsmod 列出当前已加载模块 显示模块间依赖关系 诊断驱动状态
ARM 嵌入式注意事项:在 ARM 目标板上,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。

模块依赖关系与加载顺序 i2c_core.ko EXPORT_SYMBOL hal_touch.ko 调用 i2c_master_send hal_display.ko 调用 i2c_master_send 加载顺序:先加载 i2c_core,再加载 hal_touch 和 hal_display 卸载顺序:先卸载 hal_touch 和 hal_display,再卸载 i2c_core 使用 lsmod 可查看当前模块的 Used by 计数,非零时禁止卸载

查看依赖关系的常用命令:

# 查看模块详细信息(包含依赖)
modinfo hal_touch.ko

# 查看已加载模块的依赖关系
lsmod

# 手动查看依赖文件
cat /lib/modules/$(uname -r)/modules.dep | grep hal_touch
HAL 架构实践建议:在设计 HAL 适配层时,应将底层硬件抽象(如 GPIO、I2C、SPI 控制器驱动)作为独立模块,并导出标准接口。上层 HAL 适配模块通过依赖关系调用这些接口,实现硬件无关性。这种分层依赖结构在 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" 双许可证 是 兼容性中间件
ARM 嵌入式合规提醒:如果 HAL 层调用了内核的 EXPORT_SYMBOL_GPL 符号(如 i2c_transfer、spi_sync),则模块必须声明为 GPL 兼容许可证,否则加载时会报 "module uses symbol from GPL-only, but is not GPL" 错误。