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 依赖关系图

模块间依赖关系与符号导出流程 模块 A hw_reg_write() hw_reg_read() 模块 B sensor_init() 调用 hw_reg_write() 依赖(符号引用) 内核符号表(/proc/kallsyms) hw_reg_write + hw_reg_read 已注册 EXPORT_SYMBOL_GPL 符号解析 依赖管理要点 加载顺序:先加载导出符号的模块 A,再加载依赖模块 B 卸载顺序:先卸载模块 B,再卸载模块 A(否则导致悬空符号)

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 查看内核日志。符号解析错误、参数类型不匹配、许可冲突等都会在日志中留下明确提示。