模块参数与导出符号:让内核模块真正可配置、可复用

模块写好了,怎么让它更灵活?怎么让其他模块用你的功能?

这两个问题,说白了就是模块参数和符号导出。我刚开始写内核模块时,总觉得把功能写死最省事。后来被产品经理追着改参数,改得我头皮发麻——嗯,从那以后我再也不敢硬编码了。

module_param:让模块在加载时接受参数

你想想看,一个驱动要支持不同设备地址、不同中断号,难道每次都要重新编译?

当然不。Linux 内核提供了 module_param 宏,让你可以在 insmod 或 modprobe 时传参。

基本用法

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

static int irq_num = 10;
static char *device_name = "mydev";
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)");

static int __init my_init(void)
{
    printk(KERN_INFO "Loading %s with IRQ=%d, debug=%d\n",
           device_name, irq_num, debug_level);
    return 0;
}

module_init(my_init);
MODULE_LICENSE("GPL");

加载时这样传参:

sudo insmod mymodule.ko irq_num=33 device_name="eth0" debug_level=2

参数权限说明

权限值 含义 典型场景
0 不暴露给用户空间 内部调试参数
0444 只读,所有人可查看 版本号、状态标志
0644 root 可写,其他人只读 运行时可调参数
我的习惯:调试阶段用 0644,这样可以在 /sys/module/<name>/parameters/ 下动态修改参数值,省得反复卸载加载模块。

参数类型对照表

类型标识 C 类型 说明
bool bool 布尔值,支持 0/1、y/n
int int 整型
long long 长整型
charp char * 字符串指针
uint unsigned int 无符号整型
ullong unsigned long long 无符号长长整型
我曾经踩过的坑:字符串参数 charp 传的是指针,不是拷贝。如果传的是栈上的临时字符串,模块卸载后指针就悬空了。建议用 kstrdup 做一次深拷贝。

EXPORT_SYMBOL:让其他模块调用你的函数

模块之间怎么协作?

比如你写了一个加密模块,别的模块想用你的加密函数。这时候就需要导出符号。

导出方式

// 模块 A:导出函数
#include <linux/module.h>

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

int my_calculate_gpl(int a, int b)
{
    return a * b + 200;
}
EXPORT_SYMBOL_GPL(my_calculate_gpl);

MODULE_LICENSE("GPL");
// 模块 B:使用导出的符号
#include <linux/module.h>

extern int my_calculate(int a, int b);
extern int my_calculate_gpl(int a, int b);

static int __init b_init(void)
{
    int ret;

    ret = my_calculate(3, 5);
    printk("my_calculate(3,5) = %d\n", ret);

    ret = my_calculate_gpl(3, 5);
    printk("my_calculate_gpl(3,5) = %d\n", ret);

    return 0;
}

module_init(b_init);
MODULE_LICENSE("GPL");

EXPORT_SYMBOL vs EXPORT_SYMBOL_GPL

宏 谁可以用 典型场景
EXPORT_SYMBOL 任何模块(包括非 GPL) 通用接口、硬件抽象层
EXPORT_SYMBOL_GPL 仅 GPL 许可证模块 核心算法、内部机制
我的建议:如果你写的是开源模块,尽量用 EXPORT_SYMBOL_GPL。这既是许可证合规的要求,也能防止闭源模块依赖你的内部实现——哪天你想改接口,不用顾虑太多。

模块依赖关系:谁依赖谁,一清二楚

模块 B 用了模块 A 导出的符号,那加载时就得先加载 A,再加载 B。卸载时顺序反过来。

内核怎么知道这个依赖关系?

依赖的建立

当你编译模块 B 时,如果它引用了模块 A 的符号,depmod 工具会自动分析并记录依赖。

# 查看模块依赖
modinfo mymodule_b.ko
# 输出中会显示 "depends: mymodule_a"

# 查看所有模块依赖关系
cat /lib/modules/$(uname -r)/modules.dep

手动指定依赖

有时候自动分析不够用,你可以在模块里显式声明:

MODULE_SOFTDEP("pre: mymodule_a");
MODULE_SOFTDEP("post: mymodule_c");
声明方式 含义
pre: module_name 在目标模块之前加载
post: module_name 在目标模块之后加载
我曾经遇到的死锁:模块 A 依赖 B,B 又依赖 A。这种循环依赖会导致 modprobe 卡死。解决办法是重新设计模块分层,或者把公共部分抽成第三个模块。

知识结构图:模块参数与符号导出全景

模块参数与符号导出知识结构 模块参数与符号导出知识结构 Linux 内核模块 模块参数 (module_param) 符号导出 (EXPORT_SYMBOL) 参数类型 权限控制 运行时修改 MODULE_PARM_DESC EXPORT_SYMBOL EXPORT_SYMBOL_GPL 模块依赖 depmod 分析 模块参数让模块可配置 符号导出让模块可复用

实战经验:一个完整的例子

我去年做一个嵌入式项目,需要让多个驱动共享一个硬件定时器。我是这么设计的:

  1. 定时器模块:导出 timer_start()、timer_stop()、timer_set_period() 三个函数,用 EXPORT_SYMBOL_GPL 导出。
  2. 参数化配置:定时器基地址、中断号都用 module_param 传入,方便不同板卡适配。
  3. 依赖声明:所有使用定时器的模块,在 MODULE_SOFTDEP 中声明 pre: timer_module。
// timer_module.c 片段
static unsigned long timer_base = 0x3F006000;
static int timer_irq = 29;

module_param(timer_base, ulong, 0444);
module_param(timer_irq, int, 0444);

void timer_start(void)
{
    writel(1, timer_base + 0x00);
}
EXPORT_SYMBOL_GPL(timer_start);

void timer_stop(void)
{
    writel(0, timer_base + 0x00);
}
EXPORT_SYMBOL_GPL(timer_stop);
调试小技巧:用 cat /proc/kallsyms | grep <你的符号名> 可以查看导出的符号是否被正确加载。如果看不到,检查模块是否真的加载了,或者符号名有没有拼错。

模块参数和符号导出,说白了就是让内核模块从「写死」变成「可配置、可复用」。参数让模块适应不同环境,符号导出让模块之间能协作。这两个机制用好,你的模块设计会灵活很多。