4. 模块参数与符号导出
在嵌入式 Linux 驱动开发中,模块参数与符号导出是驱动与用户空间、驱动与驱动之间交互的基础设施。模块参数允许在加载模块时动态调整行为,而符号导出则让一个模块可以被其他模块调用。本章将深入剖析 module_param、module_param_array 以及 EXPORT_SYMBOL / EXPORT_SYMBOL_GPL 的用法与内核实现机制。
4.1 模块参数:module_param 与 module_param_array
模块参数是 Linux 内核提供的一种机制,允许在 insmod 或 modprobe 加载模块时,通过命令行传递参数值,从而在不重新编译的情况下改变驱动行为。
4.1.1 module_param 基础用法
module_param 宏用于声明一个模块参数,其原型定义在 <linux/moduleparam.h> 中:
module_param(name, type, perm);
| 参数 | 说明 | 示例值 |
|---|---|---|
| name | 变量名,必须与模块内已定义的全局变量同名 | int debug_level |
| type | 参数类型,支持 byte、short、ushort、int、uint、long、ulong、charp、bool、invbool |
int |
| perm | 文件权限位,用于在 /sys/module/<name>/parameters/ 下创建 sysfs 条目;0 表示不创建 |
0644 |
关键点:perm 参数控制参数在 sysfs 中的可见性。设置为 0 时,参数仅在加载时可用,运行时不可见;设置为 0444 时,用户空间可读但不可写;设置为 0644 时,用户空间可读写。
以下是一个完整的模块参数使用示例:
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
static int debug_level = 1; /* 默认调试级别 */
static char *device_name = "mydev"; /* 默认设备名 */
static int irq_num = 17; /* 默认中断号 */
module_param(debug_level, int, 0644);
module_param(device_name, charp, 0444);
module_param(irq_num, int, 0); /* 不创建 sysfs 条目 */
static int __init param_demo_init(void)
{
pr_info("Module loaded: debug_level=%d, device_name=%s, irq=%d\n",
debug_level, device_name, irq_num);
return 0;
}
static void __exit param_demo_exit(void)
{
pr_info("Module unloaded\n");
}
module_init(param_demo_init);
module_exit(param_demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Developer");
MODULE_DESCRIPTION("Module parameter demo");
加载模块时传递参数:
sudo insmod param_demo.ko debug_level=3 device_name="uart0" irq_num=42
4.1.2 module_param_array 用法
当需要传递一组相同类型的参数时,使用 module_param_array 宏:
module_param_array(name, type, nump, perm);
| 参数 | 说明 |
|---|---|
| name | 数组变量名 |
| type | 数组元素类型 |
| nump | 指向整数的指针,内核会将实际传入的元素个数写入该变量 |
| perm | sysfs 权限位 |
static int gpio_pins[6]; /* 最多支持 6 个 GPIO */
static int gpio_count;
module_param_array(gpio_pins, int, &gpio_count, 0644);
static int __init array_demo_init(void)
{
int i;
pr_info("GPIO pins count: %d\n", gpio_count);
for (i = 0; i < gpio_count; i++)
pr_info(" gpio_pins[%d] = %d\n", i, gpio_pins[i]);
return 0;
}
加载时传递数组参数:
sudo insmod array_demo.ko gpio_pins=17,23,31,45
实践建议:数组参数在嵌入式驱动中常用于配置多个 GPIO 引脚、多个 I2C 地址或多个通道号。注意数组大小应预先定义足够大,并通过 nump 参数获取实际使用数量。
4.1.3 模块参数 sysfs 交互流程
下图展示了模块参数从加载到 sysfs 暴露的完整流程:
4.2 符号导出:EXPORT_SYMBOL 与 EXPORT_SYMBOL_GPL
符号导出允许一个模块将其函数或变量暴露给其他模块使用。这是 Linux 内核模块间依赖与复用的核心机制。
4.2.1 导出宏对比
| 宏 | 导出范围 | 使用限制 | 典型场景 |
|---|---|---|---|
EXPORT_SYMBOL(symbol) |
所有模块(包括非 GPL 模块) | 无许可证限制 | 通用 API、硬件抽象层 |
EXPORT_SYMBOL_GPL(symbol) |
仅 GPL 许可证模块 | 非 GPL 模块无法引用 | 内核核心接口、专有驱动禁止使用的 API |
许可证合规警告:如果模块声明了 MODULE_LICENSE("GPL"),则可以引用 EXPORT_SYMBOL_GPL 导出的符号。非 GPL 模块(如 MODULE_LICENSE("Proprietary"))引用 EXPORT_SYMBOL_GPL 符号会导致内核 taint 标记,甚至加载失败。
4.2.2 导出与引用示例
导出模块(exporter.c):
#include <linux/module.h>
#include <linux/init.h>
/* 导出一个简单的加法函数 */
int driver_add(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(driver_add);
/* 导出一个 GPL 专用的设备复位函数 */
void device_reset(void)
{
pr_info("Device reset performed\n");
}
EXPORT_SYMBOL_GPL(device_reset);
static int __init exporter_init(void)
{
pr_info("Exporter module loaded\n");
return 0;
}
static void __exit exporter_exit(void)
{
pr_info("Exporter module unloaded\n");
}
module_init(exporter_init);
module_exit(exporter_exit);
MODULE_LICENSE("GPL");
引用模块(importer.c):
#include <linux/module.h>
#include <linux/init.h>
/* 声明外部符号 */
extern int driver_add(int a, int b);
extern void device_reset(void);
static int __init importer_init(void)
{
int result;
result = driver_add(10, 20);
pr_info("driver_add(10, 20) = %d\n", result);
device_reset();
return 0;
}
static void __exit importer_exit(void)
{
pr_info("Importer module unloaded\n");
}
module_init(importer_init);
module_exit(importer_exit);
MODULE_LICENSE("GPL");
加载顺序:必须先加载导出模块(exporter),再加载引用模块(importer)。使用 modprobe 时,内核会自动处理依赖关系,前提是导出模块已安装到 /lib/modules/ 目录。
4.2.3 符号导出内核机制
内核通过 __ksymtab 和 __kcrctab 段管理导出的符号。每个导出的符号在编译时被记录到内核符号表中,运行时通过 kallsyms 或模块链接器进行解析。
4.3 模块参数与符号导出的协同使用
在实际驱动开发中,模块参数和符号导出经常配合使用,构建可配置、可扩展的驱动框架。
4.3.1 典型应用场景
- 参数化驱动核心:通过模块参数配置缓冲区大小、超时时间、调试级别等运行时参数
- 导出硬件操作接口:将底层硬件操作(GPIO 控制、寄存器读写)导出为符号,供上层驱动调用
- 分层驱动架构:基础驱动层导出通用 API,功能驱动层通过符号引用实现具体功能
4.3.2 综合示例:可配置的 GPIO 控制器驱动
/* gpio_ctrl.c - 基础 GPIO 控制器 */
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/gpio.h>
static int gpio_base = 100; /* GPIO 起始编号 */
static int gpio_count = 4; /* 控制的 GPIO 数量 */
static int gpio_pins[8]; /* 具体引脚数组 */
static int gpio_pins_count;
module_param(gpio_base, int, 0644);
module_param(gpio_count, int, 0644);
module_param_array(gpio_pins, int, &gpio_pins_count, 0644);
/* 导出:设置 GPIO 输出值 */
void gpio_ctrl_set(int pin, int value)
{
if (pin >= 0 && pin < gpio_count)
gpio_set_value(gpio_base + pin, value);
}
EXPORT_SYMBOL(gpio_ctrl_set);
/* 导出:读取 GPIO 输入值 */
int gpio_ctrl_get(int pin)
{
if (pin >= 0 && pin < gpio_count)
return gpio_get_value(gpio_base + pin);
return -EINVAL;
}
EXPORT_SYMBOL_GPL(gpio_ctrl_get);
static int __init gpio_ctrl_init(void)
{
int i, ret;
pr_info("GPIO controller: base=%d, count=%d\n", gpio_base, gpio_count);
/* 如果指定了具体引脚,使用数组中的值 */
if (gpio_pins_count > 0) {
for (i = 0; i < gpio_pins_count; i++) {
ret = gpio_request(gpio_pins[i], "gpio_ctrl");
if (ret)
pr_err("Failed to request GPIO %d\n", gpio_pins[i]);
}
}
return 0;
}
static void __exit gpio_ctrl_exit(void)
{
int i;
for (i = 0; i < gpio_pins_count; i++)
gpio_free(gpio_pins[i]);
pr_info("GPIO controller unloaded\n");
}
module_init(gpio_ctrl_init);
module_exit(gpio_ctrl_exit);
MODULE_LICENSE("GPL");
设计模式:基础驱动层使用 EXPORT_SYMBOL 导出通用接口,功能驱动层通过 extern 声明引用。这种分层设计使得驱动代码可复用、可测试,且便于维护。
4.4 本章小结
| 知识点 | 核心要点 | 注意事项 |
|---|---|---|
module_param |
声明单个模块参数,支持多种基本类型 | perm 为 0 时不创建 sysfs 条目 |
module_param_array |
声明数组类型模块参数,通过 nump 获取实际元素个数 |
数组大小需预定义,避免越界 |
EXPORT_SYMBOL |
导出符号给所有模块使用 | 无许可证限制 |
EXPORT_SYMBOL_GPL |
导出符号仅限 GPL 模块使用 | 非 GPL 模块引用会导致 taint |