模块参数与导出符号:让内核模块真正可配置、可复用
模块写好了,怎么让它更灵活?怎么让其他模块用你的功能?
这两个问题,说白了就是模块参数和符号导出。我刚开始写内核模块时,总觉得把功能写死最省事。后来被产品经理追着改参数,改得我头皮发麻——嗯,从那以后我再也不敢硬编码了。
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 卡死。解决办法是重新设计模块分层,或者把公共部分抽成第三个模块。
知识结构图:模块参数与符号导出全景
实战经验:一个完整的例子
我去年做一个嵌入式项目,需要让多个驱动共享一个硬件定时器。我是这么设计的:
- 定时器模块:导出
timer_start()、timer_stop()、timer_set_period()三个函数,用EXPORT_SYMBOL_GPL导出。 - 参数化配置:定时器基地址、中断号都用
module_param传入,方便不同板卡适配。 - 依赖声明:所有使用定时器的模块,在
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 <你的符号名> 可以查看导出的符号是否被正确加载。如果看不到,检查模块是否真的加载了,或者符号名有没有拼错。
模块参数和符号导出,说白了就是让内核模块从「写死」变成「可配置、可复用」。参数让模块适应不同环境,符号导出让模块之间能协作。这两个机制用好,你的模块设计会灵活很多。