2. 模块参数与导出符号

在Linux内核模块开发中,模块参数、导出符号和模块依赖是构建可配置、可复用驱动的基础设施。本章将深入剖析这三个核心机制,帮助你在Platform驱动开发中实现灵活的运行时配置和模块间协作。

2.1 模块参数:module_param宏

模块参数允许用户在加载模块时传递配置值,无需重新编译内核。通过module_param宏族,驱动开发者可以将模块内部的全局变量暴露为可调整的参数。

2.1.1 基本用法

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

static int irq_num = 10;
static char *device_name = "my_device";
static int debug_level = 0;

module_param(irq_num, int, 0644);
module_param(device_name, charp, 0644);
module_param(debug_level, int, 0644);

MODULE_PARM_DESC(irq_num, "IRQ number for the device");
MODULE_PARM_DESC(device_name, "Device name string");
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");

加载模块时,用户可以通过命令行指定参数值:

sudo insmod my_driver.ko irq_num=17 device_name="sensor0" debug_level=2

2.1.2 参数类型与权限

类型标识 C语言类型 说明 典型场景
byte unsigned char 单字节值 硬件寄存器掩码
short short 短整数 传感器阈值
int int 标准整数 IRQ号、GPIO引脚
long long 长整数 内存地址偏移
charp char * 字符串指针 设备名称、路径
bool bool 布尔值 启用/禁用特性
invbool bool 反转布尔值 反向逻辑控制

权限值使用标准的Linux文件权限八进制表示:

  • 0:参数完全隐藏,仅模块内部可见
  • 0444:所有人可读(通过sysfs)
  • 0644:所有者可读写,其他人只读
  • 0600:仅所有者可读写
实战建议:在Platform驱动中,将硬件资源(如IRQ号、内存基址)设计为模块参数,可以在不修改代码的情况下适配不同硬件平台。调试阶段建议将权限设为0644,便于运行时通过sysfs动态调整。

2.1.3 数组参数

当需要传递多个同类型值时,使用module_param_array:

static int gpio_pins[4] = {17, 18, 22, 23};
static int gpio_count;

module_param_array(gpio_pins, int, &gpio_count, 0644);
MODULE_PARM_DESC(gpio_pins, "GPIO pin numbers (up to 4)");
sudo insmod gpio_driver.ko gpio_pins=10,11,12,13

2.2 导出符号:EXPORT_SYMBOL

内核模块之间通过导出符号实现函数和变量的共享。EXPORT_SYMBOL将符号暴露给所有其他模块,EXPORT_SYMBOL_GPL则限制仅GPL许可的模块可以使用。

2.2.1 导出函数

// 在模块A中定义并导出
#include <linux/module.h>

int platform_reg_read(void __iomem *base, unsigned int offset)
{
    return ioread32(base + offset);
}
EXPORT_SYMBOL(platform_reg_read);

int platform_reg_write(void __iomem *base, unsigned int offset, int val)
{
    iowrite32(val, base + offset);
    return 0;
}
EXPORT_SYMBOL_GPL(platform_reg_write);

2.2.2 导出变量

// 导出全局变量
static struct my_device_data *global_dev_data;
EXPORT_SYMBOL(global_dev_data);

// 注意:导出变量需要去掉static,或通过指针间接导出

2.2.3 使用导出符号

// 在模块B中声明并使用
extern int platform_reg_read(void __iomem *base, unsigned int offset);
extern int platform_reg_write(void __iomem *base, unsigned int offset, int val);

void my_function(void __iomem *regs)
{
    int val = platform_reg_read(regs, 0x10);
    platform_reg_write(regs, 0x10, val | 0x01);
}
注意事项:
  • 导出符号会增加内核符号表大小,仅导出必要的接口
  • 使用EXPORT_SYMBOL_GPL保护核心API,防止非GPL模块滥用
  • 导出符号的模块必须先加载,依赖模块后加载
  • 符号命名建议使用模块前缀避免冲突,如mydrv_

2.3 模块依赖关系

模块依赖是内核模块加载顺序和符号解析的核心机制。当模块B使用了模块A导出的符号时,模块B就依赖于模块A。

2.3.1 依赖声明

使用MODULE_DEPEND宏声明依赖关系:

// 在模块B中声明
MODULE_DEPEND("module_a");
MODULE_DEPEND("i2c_core");
MODULE_DEPEND("platform_driver_base");

2.3.2 依赖关系图

模块依赖关系示意图 platform_core.ko i2c_adapter.ko gpio_expander.ko spi_controller.ko sensor_driver.ko display_driver.ko audio_codec.ko 箭头方向表示“依赖方向”:sensor_driver.ko → i2c_adapter.ko → platform_core.ko 加载顺序必须与依赖方向相反:先加载platform_core.ko,最后加载sensor_driver.ko

2.3.3 依赖解析与加载顺序

内核模块加载器自动处理依赖关系:

  1. 符号解析:加载模块时,内核检查其未定义符号是否在已加载模块的导出符号表中
  2. 递归依赖:如果依赖模块尚未加载,modprobe自动查找并加载依赖模块
  3. 循环依赖:内核不允许循环依赖,会导致加载失败
  4. 版本检查:依赖模块的版本必须兼容,否则符号解析失败

2.3.4 查看依赖关系

# 查看模块依赖
modinfo my_driver.ko
# 输出示例:
# depends: platform_core,i2c_core

# 查看已加载模块的依赖树
lsmod
# Module                  Size  Used by
# sensor_driver          16384  0
# i2c_adapter            24576  1 sensor_driver
# platform_core          20480  2 i2c_adapter,gpio_expander

# 查看导出符号
cat /proc/kallsyms | grep platform_reg_read
# ffffffc0800a1000 T platform_reg_read  [platform_core]

2.4 综合实战:Platform驱动中的参数与符号设计

以下是一个完整的Platform驱动示例,展示模块参数、导出符号和依赖关系的协同使用:

// platform_base.c - 基础Platform驱动库
#include <linux/module.h>
#include <linux/platform_device.h>

static int default_irq = 10;
module_param(default_irq, int, 0644);
MODULE_PARM_DESC(default_irq, "Default IRQ number");

// 导出核心API
int platform_base_probe(struct platform_device *pdev)
{
    pr_info("Platform base probed, default IRQ: %d\n", default_irq);
    return 0;
}
EXPORT_SYMBOL_GPL(platform_base_probe);

int platform_base_remove(struct platform_device *pdev)
{
    pr_info("Platform base removed\n");
    return 0;
}
EXPORT_SYMBOL_GPL(platform_base_remove);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Platform Base Driver Library");
MODULE_DEPEND("platform");
// my_device_driver.c - 依赖platform_base的驱动
#include <linux/module.h>
#include <linux/platform_device.h>

extern int platform_base_probe(struct platform_device *pdev);
extern int platform_base_remove(struct platform_device *pdev);

static int my_probe(struct platform_device *pdev)
{
    // 调用基础库的probe
    return platform_base_probe(pdev);
}

static int my_remove(struct platform_device *pdev)
{
    return platform_base_remove(pdev);
}

static struct platform_driver my_driver = {
    .probe  = my_probe,
    .remove = my_remove,
    .driver = {
        .name = "my_device",
        .owner = THIS_MODULE,
    },
};

module_platform_driver(my_driver);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("My Device Driver");
MODULE_DEPEND("platform_base");
关键要点总结:
  • module_param:实现驱动参数的运行时配置,支持基本类型和数组
  • EXPORT_SYMBOL:构建模块间API共享机制,_GPL变体保护核心接口
  • 模块依赖:通过MODULE_DEPEND声明依赖,内核自动管理加载顺序
  • 设计原则:将通用功能封装为基础模块,导出符号供上层驱动调用,形成清晰的层次化架构