4. 模块参数与导出符号:module_param、EXPORT_SYMBOL、模块间依赖关系
在嵌入式 Linux 驱动开发中,模块参数(Module Parameters)和导出符号(Exported Symbols)是实现驱动灵活配置与模块间协作的核心机制。本章将深入讲解如何通过 module_param 在加载时传递参数,以及如何利用 EXPORT_SYMBOL 构建模块间的依赖关系。
4.1 模块参数:module_param 与 module_param_array
模块参数允许用户在 insmod 或 modprobe 时向驱动传递配置值,无需重新编译内核。这对于调试、硬件适配和产品化部署极为重要。
4.1.1 基本参数声明
使用 module_param(name, type, perm) 宏声明一个模块参数。其中 name 是变量名,type 是数据类型,perm 是 sysfs 文件权限。
| 类型宏 | C 数据类型 | 说明 |
|---|---|---|
byte |
unsigned char | 单字节 |
short |
short | 短整型 |
ushort |
unsigned short | 无符号短整型 |
int |
int | 整型 |
uint |
unsigned int | 无符号整型 |
long |
long | 长整型 |
ulong |
unsigned long | 无符号长整型 |
charp |
char * | 字符串指针 |
bool |
bool | 布尔值 |
invbool |
bool | 反转布尔值 |
#include <linux/module.h>
#include <linux/moduleparam.h>
/* 模块参数变量 */
static int debug_level = 0;
static int buffer_size = 1024;
static char *device_name = "mydev";
/* 声明模块参数 */
module_param(debug_level, int, 0644);
module_param(buffer_size, int, 0644);
module_param(device_name, charp, 0644);
/* 参数描述 */
MODULE_PARM_DESC(debug_level, "调试级别:0-关闭,1-基本信息,2-详细信息");
MODULE_PARM_DESC(buffer_size, "环形缓冲区大小(字节)");
MODULE_PARM_DESC(device_name, "设备名称");
4.1.2 数组参数:module_param_array
当需要传递多个同类型值时,使用 module_param_array(name, type, nump, perm)。其中 nump 是一个指向 int 的指针,用于接收实际传入的元素个数。
static int irq_numbers[4];
static int irq_count;
module_param_array(irq_numbers, int, &irq_count, 0644);
MODULE_PARM_DESC(irq_numbers, "IRQ 号列表,最多4个");
/* 加载示例:insmod mydriver.ko irq_numbers=17,23,45,67 */
4.1.3 参数权限与 sysfs 接口
perm 参数控制参数在 /sys/module/<name>/parameters/ 下的访问权限:
- 0:不在 sysfs 中暴露,仅加载时可用。
- S_IRUGO(0444):只读,所有用户可查看。
- S_IWUSR|S_IRUGO(0644):所有者可写,其他用户只读。
param_set_xxx 回调验证输入合法性,避免导致系统崩溃。
4.2 导出符号:EXPORT_SYMBOL 与 EXPORT_SYMBOL_GPL
导出符号允许一个模块将其函数或变量暴露给其他模块使用,是构建模块间依赖关系的基石。
4.2.1 导出机制对比
| 宏 | 许可限制 | 使用场景 |
|---|---|---|
EXPORT_SYMBOL(func) |
无限制 | 通用接口,任何模块均可调用 |
EXPORT_SYMBOL_GPL(func) |
仅 GPL 许可模块 | 保护知识产权,限制非 GPL 模块使用 |
4.2.2 导出符号实战
假设我们有一个底层硬件访问模块 hw_access,提供寄存器读写函数供其他驱动使用:
/* hw_access.c */
#include <linux/module.h>
void hw_reg_write(unsigned int addr, unsigned int val)
{
/* 硬件寄存器写入实现 */
writel(val, ioremap(addr, 4));
}
EXPORT_SYMBOL_GPL(hw_reg_write);
unsigned int hw_reg_read(unsigned int addr)
{
return readl(ioremap(addr, 4));
}
EXPORT_SYMBOL_GPL(hw_reg_read);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("底层硬件访问模块");
另一个模块 sensor_driver 可以调用这些导出函数:
/* sensor_driver.c */
extern void hw_reg_write(unsigned int addr, unsigned int val);
extern unsigned int hw_reg_read(unsigned int addr);
int sensor_init(void)
{
/* 通过导出函数访问硬件 */
hw_reg_write(0x1000, 0x01); /* 使能传感器 */
return 0;
}
4.3 模块间依赖关系
当模块 A 使用了模块 B 导出的符号时,模块 A 就依赖于模块 B。内核通过 module_layout 和符号表管理这种依赖。
4.3.1 依赖关系图
4.3.2 查看模块依赖
使用 lsmod 和 modinfo 命令查看模块依赖关系:
# 查看已加载模块及其依赖
lsmod
Module Size Used by
sensor_driver 16384 0
hw_access 20480 1 sensor_driver
# 查看模块详细信息
modinfo hw_access
filename: /lib/modules/5.10.0/hw_access.ko
license: GPL
description: 底层硬件访问模块
depends:
name: hw_access
modinfo sensor_driver
filename: /lib/modules/5.10.0/sensor_driver.ko
license: GPL
description: 传感器驱动
depends: hw_access
name: sensor_driver
4.3.3 自动处理依赖:modprobe
modprobe 命令会自动加载依赖模块,而 insmod 不会:
| 命令 | 依赖处理 | 推荐场景 |
|---|---|---|
insmod |
不处理依赖,需手动加载 | 调试阶段,精确控制加载顺序 |
modprobe |
自动解析并加载依赖 | 生产环境,简化操作 |
insmod 手动控制加载顺序,便于定位符号解析问题。产品部署时使用 modprobe 配合 /etc/modprobe.d/ 配置文件管理参数和依赖。
4.4 模块参数与导出符号的协同使用
在实际项目中,模块参数和导出符号经常协同工作。例如,一个核心驱动通过导出符号提供接口,同时使用模块参数控制其行为:
/* core_driver.c */
static int poll_interval = 100; /* 轮询间隔(ms) */
module_param(poll_interval, int, 0644);
MODULE_PARM_DESC(poll_interval, "轮询间隔(毫秒)");
void core_set_poll_interval(int ms)
{
poll_interval = ms;
pr_info("轮询间隔已更新为 %d ms\n", poll_interval);
}
EXPORT_SYMBOL_GPL(core_set_poll_interval);
/* 客户端驱动调用 */
extern void core_set_poll_interval(int ms);
void client_set_fast_mode(void)
{
core_set_poll_interval(10); /* 设置为快速轮询 */
}
4.5 常见问题与避坑指南
- 符号未导出错误:
Unknown symbol通常是因为忘记使用EXPORT_SYMBOL或模块加载顺序错误。 - 参数类型不匹配:
module_param的类型必须与变量类型严格一致,否则加载时静默失败。 - GPL 许可冲突:非 GPL 模块调用
EXPORT_SYMBOL_GPL导出的符号会导致加载失败,内核日志会显示module license 'Proprietary' taints kernel。 - 数组参数越界:
module_param_array不会自动检查数组边界,需在驱动中验证nump值。 - 卸载顺序死锁:模块 A 导出符号给模块 B,若先卸载 A 而 B 仍在运行,将导致内核崩溃。使用
try_module_get/module_put管理引用计数可避免此问题。
dmesg | tail -20 查看内核日志。符号解析错误、参数类型不匹配、许可冲突等都会在日志中留下明确提示。