4. 模块参数与导出符号

在Linux内核模块开发中,模块参数与符号导出是构建灵活、可配置驱动的基础设施。模块参数允许用户在加载模块时动态调整行为,而符号导出则使模块间能够共享函数与变量,实现内核空间的模块化协作。本章将深入剖析 module_param 宏的用法、参数类型与权限控制,以及 EXPORT_SYMBOL 的导出机制与最佳实践。

4.1 模块参数:module_param 宏

模块参数是内核模块向用户空间暴露的配置接口。通过 module_param 宏,开发者可以在 insmod 或 modprobe 时传递参数值,无需重新编译模块即可改变其运行行为。

4.1.1 module_param 宏定义与基本用法

module_param 宏定义在 <linux/moduleparam.h> 中,其标准形式为:

module_param(name, type, perm);
  • name:模块内已定义的全局变量名,作为参数名称。
  • type:参数的数据类型,决定如何解析用户输入的字符串。
  • perm:参数在 /sys/module/<module_name>/parameters/ 下的文件权限,控制用户空间的可读/可写性。

示例:定义一个整型参数和一个字符串参数:

#include <linux/module.h>
#include <linux/moduleparam.h>

static int debug_level = 0;
static char *device_name = "default_dev";

module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");

module_param(device_name, charp, 0444);
MODULE_PARM_DESC(device_name, "Device name string");

加载模块时传递参数:

sudo insmod my_driver.ko debug_level=2 device_name="my_char_dev"

4.1.2 支持的内核参数类型

内核为 module_param 预定义了多种标准类型,覆盖常见需求:

类型标识 C 语言对应类型 说明
byte unsigned char 单字节无符号整数
short short 短整数
ushort unsigned short 无符号短整数
int int 标准整数
uint unsigned int 无符号整数
long long 长整数
ulong unsigned long 无符号长整数
charp char * 字符串指针(内核会复制字符串)
bool bool 布尔值(接受 0/1, y/n, Y/N)
invbool bool 反转布尔值
最佳实践:对于字符串参数,始终使用 charp 类型。内核会在模块加载时自动分配并复制字符串,模块卸载时自动释放,避免内存泄漏。

4.1.3 参数权限(perm)详解

perm 参数控制 /sys/module/<name>/parameters/<param> 文件的访问权限,使用标准的 Linux 文件权限八进制表示:

  • 0:不创建 sysfs 文件条目,参数完全隐藏,用户空间无法查看或修改。
  • 0444:所有用户可读,不可写(只读参数)。
  • 0644:所有者可读写,其他用户只读。
  • 0660:所有者和组可读写,其他用户无权限。
安全警告:切勿将参数权限设置为 0666(全局可写)或 0777,除非你明确知道风险。运行时修改关键参数可能导致内核崩溃或安全漏洞。对于调试参数,建议使用 0644 并仅在开发环境中启用。

4.2 导出内核符号:EXPORT_SYMBOL

内核模块虽然运行在内核空间,但每个模块默认拥有独立的符号命名空间。若要让一个模块中的函数或变量被其他模块使用,必须显式导出符号。EXPORT_SYMBOL 及其变体提供了这一能力。

4.2.1 EXPORT_SYMBOL 与 EXPORT_SYMBOL_GPL

内核提供两个主要的导出宏:

EXPORT_SYMBOL(symbol_name);
EXPORT_SYMBOL_GPL(symbol_name);
  • EXPORT_SYMBOL:导出的符号对所有模块可见,无论其许可证类型。
  • EXPORT_SYMBOL_GPL:导出的符号仅对声明为 GPL 兼容许可证的模块可见。非 GPL 模块尝试引用该符号时,链接会失败。

示例:在驱动 A 中导出函数:

// driver_a.c
#include <linux/module.h>

int my_driver_helper(int arg)
{
    return arg * 2;
}
EXPORT_SYMBOL(my_driver_helper);

// 仅允许 GPL 模块使用
void gpl_only_function(void)
{
    printk(KERN_INFO "GPL only\n");
}
EXPORT_SYMBOL_GPL(gpl_only_function);

在驱动 B 中引用:

// driver_b.c
extern int my_driver_helper(int arg);

static int __init init_b(void)
{
    int result = my_driver_helper(42);
    printk(KERN_INFO "Result: %d\n", result);
    return 0;
}

4.2.2 符号导出与模块依赖

当模块 B 使用了模块 A 导出的符号时,加载顺序必须满足依赖关系:

  1. 先加载模块 A(提供符号)。
  2. 再加载模块 B(使用符号)。
  3. 卸载时顺序相反:先卸载 B,再卸载 A。

使用 modprobe 时,内核会自动处理依赖关系,前提是模块间通过 MODULE_SOFTDEP 或 MODULE_DEPEND 声明了依赖。

查看导出符号:使用 cat /proc/kallsyms | grep <symbol_name> 可以查看内核中所有已导出的符号及其地址。模块加载后,其导出的符号也会出现在此文件中。

4.3 模块参数与符号导出综合架构

下图展示了模块参数与符号导出在典型字符设备驱动中的协作关系:

模块参数与符号导出协作架构 用户空间 insmod my_driver.ko debug_level=2 模块参数 (module_param) debug_level (int, 0644) device_name (charp, 0444) /sys/module/.../parameters/ 导出符号 (EXPORT_SYMBOL) my_driver_helper() gpl_only_function() /proc/kallsyms 可见 字符设备驱动核心 file_operations / ioctl / read / write 使用模块参数配置行为,导出符号供其他模块调用 其他 模块 内核符号表 (/proc/kallsyms) 动态链接与符号解析

4.4 高级用法与注意事项

4.4.1 数组参数:module_param_array

当需要传递一组相同类型的值时,使用 module_param_array:

static int irq_numbers[4];
static int irq_count;

module_param_array(irq_numbers, int, &irq_count, 0444);
MODULE_PARM_DESC(irq_numbers, "Array of IRQ numbers (max 4)");

加载时传递:

sudo insmod my_driver.ko irq_numbers=11,12,13,14

4.4.2 符号导出的命名冲突与前缀

内核符号表是全局的,不同模块导出的符号名称必须唯一。建议为导出的符号添加模块特定的前缀,避免与内核或其他模块冲突:

// 推荐:使用模块名作为前缀
int mydrv_register_buffer(struct mydrv_device *dev);
EXPORT_SYMBOL(mydrv_register_buffer);

// 不推荐:通用名称容易冲突
int register_buffer(void);
EXPORT_SYMBOL(register_buffer);

4.4.3 模块参数与 EXPORT_SYMBOL 的对比

特性 模块参数 (module_param) 导出符号 (EXPORT_SYMBOL)
主要用途 模块加载时配置,运行时可选修改 模块间共享函数/变量
访问方式 insmod 命令行或 sysfs 文件 其他模块直接函数调用
作用域 仅限本模块内部使用 全局内核符号表
生命周期 模块加载时设置,卸载后消失 模块加载后持续存在,卸载后消失
权限控制 通过 perm 参数控制 sysfs 权限 通过 EXPORT_SYMBOL_GPL 限制许可证
典型场景 调试级别、设备名称、缓冲区大小 驱动核心 API、硬件抽象层函数

4.5 实践建议与常见陷阱

模块参数设计原则:
  • 将可变配置(如调试级别、缓冲区大小)设计为模块参数,避免硬编码。
  • 为每个参数添加 MODULE_PARM_DESC 描述,方便 modinfo 查看。
  • 敏感参数(如密码、密钥)设置权限为 0,不在 sysfs 中暴露。
常见陷阱:
  • 字符串参数未初始化:使用 charp 时,必须确保指针初始化为 NULL 或有效地址,否则可能导致内核崩溃。
  • 符号导出后未声明 extern:引用模块必须使用 extern 声明导出的符号,否则编译会报隐式声明警告。
  • 模块卸载顺序错误:卸载提供符号的模块前,必须先卸载所有依赖它的模块,否则内核会 panic。
  • 权限设置过于宽松:将参数设为 0666 允许任意用户修改,可能被恶意利用。

4.6 本章小结

模块参数与符号导出是内核模块编程中实现配置灵活性与模块化复用的两大基石。通过 module_param 宏,驱动开发者可以在不重新编译的情况下调整模块行为;通过 EXPORT_SYMBOL 和 EXPORT_SYMBOL_GPL,可以构建可复用的内核 API 库。掌握这些机制,是编写高质量、可维护字符设备驱动的必备技能。