3. 内核模块基础:模块概念、模块加载与卸载、模块参数传递、模块依赖与符号导出
Linux 内核是一个单体内核(Monolithic Kernel),但通过内核模块(Loadable Kernel Module, LKM)机制实现了动态扩展能力。模块是可以在运行时独立加载到内核或从内核卸载的代码段,无需重新编译整个内核或重启系统。本章深入剖析模块的完整生命周期、参数化配置、依赖管理以及符号导出规则,为后续 GPIO 驱动开发奠定基础。
核心要点:模块机制是嵌入式 Linux 驱动开发的基础设施。理解模块的加载/卸载流程、参数传递方式以及符号导出规则,是编写可维护、可配置驱动程序的必备技能。
3.1 模块概念与生命周期
内核模块本质上是一个目标文件(.ko),包含初始化函数、清理函数以及可选的导出符号。模块的生命周期分为四个阶段:加载 → 初始化 → 运行 → 卸载。
提示:模块的 init 函数返回 0 表示成功,返回负数表示失败。内核会记录模块的引用计数,当引用计数不为 0 时,rmmod 操作会被拒绝。
3.2 模块加载与卸载
模块的加载与卸载通过用户空间工具 insmod、rmmod、modprobe 完成,内核则通过系统调用 init_module() 和 delete_module() 实现底层操作。
3.2.1 加载流程
- insmod 读取 .ko 文件到用户空间缓冲区。
- 调用
init_module()系统调用,将模块代码复制到内核空间。 - 内核执行模块重定位,解析未定义符号。
- 调用模块的
module_init()指定的初始化函数。 - 初始化成功则模块进入运行状态,失败则回滚并释放资源。
3.2.2 卸载流程
- rmmod 调用
delete_module()系统调用。 - 内核检查模块引用计数是否为 0。
- 调用模块的
module_exit()指定的清理函数。 - 释放模块占用的内核内存。
3.2.3 基础模块示例
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int __init hello_init(void)
{
printk(KERN_INFO "Hello, GPIO Module loaded!\n");
return 0;
}
static void __exit hello_exit(void)
{
printk(KERN_INFO "Goodbye, GPIO Module unloaded!\n");
}
module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Driver Expert");
MODULE_DESCRIPTION("A simple GPIO demo module");
注意:__init 和 __exit 标记告诉内核,初始化函数在调用后可以释放内存,清理函数在模块内置时可以不编译。这有助于减少内核映像大小。
3.3 模块参数传递
模块参数允许在加载时向模块传递配置值,无需修改代码即可调整驱动行为。参数通过 module_param() 宏声明,支持多种数据类型。
3.3.1 参数声明宏
| 宏 | 数据类型 | 示例 | 说明 |
|---|---|---|---|
module_param(name, type, perm) |
int, long, charp, bool 等 | module_param(gpio_num, int, 0644) |
基本类型参数 |
module_param_array(name, type, nump, perm) |
数组 | module_param_array(pins, int, &count, 0444) |
数组类型参数 |
module_param_string(name, string, len, perm) |
字符串 | module_param_string(dev_name, buf, 32, 0) |
字符串参数 |
3.3.2 参数权限位
perm 参数控制参数在 /sys/module/<module_name>/parameters/ 下的文件权限:
- 0:参数不在 sysfs 中显示,仅加载时可用。
- 0444:只读,所有用户可查看。
- 0644:所有者可读写,其他用户只读。
3.3.3 带参数的模块示例
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
static int gpio_pin = 17; // 默认 GPIO 17
static char *gpio_label = "LED"; // 默认标签
static int gpio_pins[4]; // 数组参数
static int gpio_count;
module_param(gpio_pin, int, 0644);
MODULE_PARM_DESC(gpio_pin, "GPIO pin number (default: 17)");
module_param(gpio_label, charp, 0444);
MODULE_PARM_DESC(gpio_label, "GPIO label string");
module_param_array(gpio_pins, int, &gpio_count, 0444);
MODULE_PARM_DESC(gpio_pins, "Array of GPIO pins");
static int __init param_demo_init(void)
{
int i;
printk(KERN_INFO "GPIO pin: %d\n", gpio_pin);
printk(KERN_INFO "GPIO label: %s\n", gpio_label);
printk(KERN_INFO "GPIO count: %d\n", gpio_count);
for (i = 0; i < gpio_count; i++)
printk(KERN_INFO " pin[%d] = %d\n", i, gpio_pins[i]);
return 0;
}
static void __exit param_demo_exit(void)
{
printk(KERN_INFO "Param demo module unloaded\n");
}
module_init(param_demo_init);
module_exit(param_demo_exit);
MODULE_LICENSE("GPL");
加载时传递参数:
sudo insmod param_demo.ko gpio_pin=23 gpio_label="BUZZER" gpio_pins=10,11,12,13
3.4 模块依赖与符号导出
模块之间可以存在依赖关系:一个模块可以导出符号(函数或变量),供其他模块使用。内核通过 EXPORT_SYMBOL 和 EXPORT_SYMBOL_GPL 宏控制符号的可见性。
3.4.1 符号导出宏
| 宏 | 可见范围 | 使用场景 |
|---|---|---|
EXPORT_SYMBOL(func) |
所有模块(包括非 GPL 模块) | 通用接口,如 GPIO 库函数 |
EXPORT_SYMBOL_GPL(func) |
仅 GPL 许可的模块 | 内核核心接口,保护知识产权 |
3.4.2 模块依赖管理
当模块 B 使用了模块 A 导出的符号时,模块 B 依赖于模块 A。加载时必须先加载 A,再加载 B。卸载时顺序相反。
// 模块 A:gpio_core.c
#include <linux/module.h>
#include <linux/gpio.h>
int gpio_core_set_value(int pin, int value)
{
// 实际 GPIO 操作
gpio_set_value(pin, value);
return 0;
}
EXPORT_SYMBOL(gpio_core_set_value);
static int __init core_init(void) { return 0; }
static void __exit core_exit(void) {}
module_init(core_init);
module_exit(core_exit);
MODULE_LICENSE("GPL");
// 模块 B:gpio_user.c
extern int gpio_core_set_value(int pin, int value);
static int __init user_init(void)
{
gpio_core_set_value(17, 1); // 使用模块 A 导出的函数
return 0;
}
module_init(user_init);
MODULE_LICENSE("GPL");
3.4.3 查看模块依赖
使用 modinfo 和 lsmod 查看模块依赖关系:
# 查看模块信息
modinfo gpio_core.ko
# 输出示例:
# filename: gpio_core.ko
# license: GPL
# depends:
# vermagic: 5.10.0-rc7+
modinfo gpio_user.ko
# 输出示例:
# filename: gpio_user.ko
# license: GPL
# depends: gpio_core
# vermagic: 5.10.0-rc7+
# 查看已加载模块的依赖树
lsmod | grep gpio
# gpio_user 16384 0
# gpio_core 20480 1 gpio_user
3.5 模块工具与调试
掌握以下工具链,可以高效管理模块生命周期:
| 工具 | 功能 | 常用选项 |
|---|---|---|
insmod |
加载模块 | insmod module.ko [param=value] |
rmmod |
卸载模块 | rmmod module_name |
modprobe |
智能加载/卸载(自动处理依赖) | modprobe -r module_name(卸载) |
lsmod |
列出已加载模块 | 显示模块名、大小、被谁使用 |
modinfo |
查看模块信息 | modinfo module.ko |
depmod |
生成模块依赖文件 | depmod -a |
调试技巧:使用
dmesg 查看模块的 printk 输出。在开发阶段,建议将模块参数 perm 设为 0,避免 sysfs 暴露内部配置;调试完成后根据需要调整权限。
3.6 本章小结
内核模块是 Linux 驱动开发的基石。通过本章学习,你应掌握:
- 模块的生命周期:加载 → 初始化 → 运行 → 卸载。
- module_init() 和 module_exit() 的正确使用。
- module_param() 系列宏实现灵活的运行时配置。
- EXPORT_SYMBOL 和 EXPORT_SYMBOL_GPL 控制符号可见性。
- 使用 modprobe/depmod 管理模块依赖。