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 暴露的完整流程:

模块参数 sysfs 交互流程 用户空间 insmod / modprobe 内核参数解析 parse_args() module_param 宏 赋值全局变量 sysfs 条目创建 /sys/module/.../parameters/ 用户空间读写 cat / echo 注意事项 • perm=0 时不创建 sysfs • 运行时修改需 perm 含写权限 • 数组参数用逗号分隔

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 或模块链接器进行解析。

符号导出与引用内核机制 导出模块 (exporter.ko) • driver_add() • device_reset() • EXPORT_SYMBOL(driver_add) • EXPORT_SYMBOL_GPL(device_reset) 内核符号表 __ksymtab 段 • driver_add (GPL: no) • device_reset (GPL: yes) • 符号地址 + 校验和 引用模块 (importer.ko) extern 声明 调用导出函数 许可证检查 GPL_ONLY 标志校验 符号解析流程 1. 引用模块记录未解析符号 2. 内核链接器查找符号表 3. 校验许可证兼容性

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