4. 内核模块参数与符号导出

在 Linux 内核驱动开发中,模块不仅需要接收外部配置(如设备数量、缓冲区大小),还需要将内部函数或变量暴露给其他模块使用。本章聚焦于 模块参数传递 与 符号导出 两大机制,并深入分析模块间函数调用的实现原理与注意事项。

核心要点: module_param 实现用户态/启动参数配置内核模块;EXPORT_SYMBOL 实现模块间符号共享;模块间调用需处理符号依赖与加载顺序。

4.1 模块参数:module_param 系列宏

模块参数允许在加载模块时(insmod 或 modprobe)或通过 /sys/module/<name>/parameters/ 动态调整模块行为,无需重新编译。

4.1.1 基本用法

#include <linux/moduleparam.h>

static int irq_num = 10;
static char *dev_name = "mydevice";
static int debug_level = 0;

module_param(irq_num, int, 0644);
module_param(dev_name, charp, 0444);
module_param(debug_level, int, 0660);

MODULE_PARM_DESC(irq_num, "IRQ number for the device");
MODULE_PARM_DESC(dev_name, "Device name string");
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
参数 说明
name 变量名,必须与模块内全局变量一致
type 数据类型:byte、short、int、uint、long、ulong、charp、bool、invbool
perm sysfs 文件权限:0 表示不创建 sysfs 条目;0444 只读;0644 读写
加载示例:
sudo insmod mydriver.ko irq_num=17 dev_name="uart0" debug_level=2
运行时修改:echo 3 > /sys/module/mydriver/parameters/debug_level

4.1.2 数组参数

static int ports[4] = {0x3f8, 0x2f8, 0x3e8, 0x2e8};
static int nr_ports;

module_param_array(ports, int, &nr_ports, 0444);
MODULE_PARM_DESC(ports, "Array of I/O port addresses");

加载时传递数组:insmod mydriver.ko ports=0x3f8,0x2f8,0x3e8,nr_ports 自动记录实际传入个数。

4.2 符号导出:EXPORT_SYMBOL 与 EXPORT_SYMBOL_GPL

内核模块默认只导出 init_module 和 cleanup_module。若希望其他模块调用本模块的函数或访问全局变量,必须显式导出符号。

4.2.1 导出宏对比

宏 可见性 使用场景
EXPORT_SYMBOL(func) 所有模块(含非 GPL) 通用接口,如字符设备注册函数
EXPORT_SYMBOL_GPL(func) 仅 GPL 许可模块 核心内部接口,限制商业闭源模块使用

4.2.2 导出示例

// exporter.c — 导出模块
#include <linux/module.h>

int my_core_calc(int a, int b)
{
    return a * b + 100;
}
EXPORT_SYMBOL(my_core_calc);

int my_gpl_only(void)
{
    return 0x55;
}
EXPORT_SYMBOL_GPL(my_gpl_only);

MODULE_LICENSE("GPL");
// importer.c — 导入模块
extern int my_core_calc(int a, int b);

static int __init importer_init(void)
{
    int result = my_core_calc(3, 7);  // 调用导出函数
    pr_info("Result from exporter: %d\n", result);
    return 0;
}

module_init(importer_init);
MODULE_LICENSE("GPL");
依赖顺序: 必须先加载导出模块(exporter),再加载导入模块(importer)。卸载时顺序相反。使用 modprobe 可自动处理依赖。

4.3 模块间函数调用机制

模块间调用本质上是 内核符号表的动态链接。加载器在解析未定义符号时,会遍历已加载模块的导出符号表。

模块间函数调用流程 模块 A(导出者) int func_a(int x) EXPORT_SYMBOL(func_a) 符号表条目: func_a → 0xffff888012345678 license: GPL 模块 B(导入者) extern int func_a(int); int ret = func_a(42); 未定义符号: func_a → 待解析 内核全局符号表 func_a → 0xffff888012345678 (模块A, GPL) my_core_calc → 0xffff888012345abc (模块A, 非GPL) insmod 解析未定义符号 重定位到实际地址 关键:模块B调用func_a时,实际跳转到模块A的代码段地址,两个模块共享同一内核地址空间

4.3.1 调用流程步骤

  1. 编译阶段: 导入模块(importer)记录未定义符号 func_a,生成重定位条目。
  2. 加载阶段: insmod 或 modprobe 扫描导入模块的未定义符号表。
  3. 符号查找: 遍历已加载模块的 __ksymtab 段,匹配符号名。
  4. 许可检查: 若符号用 EXPORT_SYMBOL_GPL 导出,检查导入模块 license 是否为 GPL。
  5. 重定位: 将导入模块中的符号引用替换为导出模块中的实际地址。
  6. 调用执行: 模块 B 调用 func_a() 时,直接跳转到模块 A 的代码段。

4.4 模块依赖与加载顺序管理

当模块间存在符号依赖时,必须保证加载顺序正确。内核提供两种管理方式:

方式 命令/文件 说明
手动顺序 insmod exporter.ko → insmod importer.ko 简单但易出错,卸载时需逆序
自动依赖 modprobe importer 读取 /lib/modules/$(uname -r)/modules.dep,自动加载依赖模块

4.4.1 查看模块依赖

# 查看已加载模块的依赖关系
lsmod | grep exporter

# 查看模块文件依赖(需先运行 depmod)
modinfo -F depends importer.ko

# 手动生成依赖文件
sudo depmod -a

4.4.2 模块间调用注意事项

避坑指南:
  • 循环依赖: 模块 A 导出符号给 B,B 又导出符号给 A,加载器无法解析,应重构设计。
  • 符号冲突: 不同模块导出同名符号,后加载者会覆盖前者,导致不可预期行为。
  • 卸载顺序: 若 B 正在使用 A 导出的函数,卸载 A 会导致 B 调用非法地址(内核 panic)。
  • GPL 污染: 非 GPL 模块调用 EXPORT_SYMBOL_GPL 符号,加载时被拒绝。

4.5 实战:构建可配置的字符设备驱动

综合运用模块参数与符号导出,实现一个可配置缓冲区大小的字符设备,并将核心操作函数导出供其他模块使用。

// char_device_exporter.c
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/slab.h>

static int buf_size = 1024;
module_param(buf_size, int, 0644);
MODULE_PARM_DESC(buf_size, "Buffer size in bytes (default: 1024)");

static char *device_buffer;
static int major;

int mydev_open(struct inode *inode, struct file *file)
{
    pr_info("Device opened\n");
    return 0;
}
EXPORT_SYMBOL(mydev_open);

ssize_t mydev_read(struct file *file, char __user *buf,
                   size_t len, loff_t *off)
{
    // 简化实现:返回缓冲区内容
    return simple_read_from_buffer(buf, len, off, device_buffer, buf_size);
}
EXPORT_SYMBOL(mydev_read);

ssize_t mydev_write(struct file *file, const char __user *buf,
                    size_t len, loff_t *off)
{
    return simple_write_to_buffer(device_buffer, buf_size, off, buf, len);
}
EXPORT_SYMBOL(mydev_write);

static struct file_operations mydev_fops = {
    .owner = THIS_MODULE,
    .open  = mydev_open,
    .read  = mydev_read,
    .write = mydev_write,
};

static int __init exporter_init(void)
{
    device_buffer = kmalloc(buf_size, GFP_KERNEL);
    if (!device_buffer)
        return -ENOMEM;

    major = register_chrdev(0, "mydev_exporter", &mydev_fops);
    if (major < 0) {
        kfree(device_buffer);
        return major;
    }
    pr_info("Exporter loaded: major=%d, buf_size=%d\n", major, buf_size);
    return 0;
}

static void __exit exporter_exit(void)
{
    unregister_chrdev(major, "mydev_exporter");
    kfree(device_buffer);
    pr_info("Exporter unloaded\n");
}

module_init(exporter_init);
module_exit(exporter_exit);
MODULE_LICENSE("GPL");
// char_device_importer.c
#include <linux/module.h>

extern int mydev_open(struct inode *, struct file *);
extern ssize_t mydev_read(struct file *, char __user *, size_t, loff_t *);
extern ssize_t mydev_write(struct file *, const char __user *, size_t, loff_t *);

static int __init importer_init(void)
{
    pr_info("Importer loaded, ready to use exported functions\n");
    pr_info("mydev_open @ %p\n", mydev_open);
    pr_info("mydev_read @ %p\n", mydev_read);
    pr_info("mydev_write @ %p\n", mydev_write);
    return 0;
}

static void __exit importer_exit(void)
{
    pr_info("Importer unloaded\n");
}

module_init(importer_init);
module_exit(importer_exit);
MODULE_LICENSE("GPL");
加载测试:
sudo insmod char_device_exporter.ko buf_size=2048
sudo insmod char_device_importer.ko
dmesg | tail -5 查看输出
ls -l /dev/mydev_exporter* 确认设备节点

4.6 本章小结

  • module_param 提供灵活的模块配置接口,支持运行时动态调整。
  • EXPORT_SYMBOL / EXPORT_SYMBOL_GPL 控制符号可见性,GPL 变体保护核心接口。
  • 模块间调用 依赖内核符号表动态重定位,需严格管理加载/卸载顺序。
  • 实战中 将模块参数与符号导出结合,可构建高度可配置、可扩展的驱动架构。