模块参数与导出符号:让内核模块“活”起来
模块写好了,怎么让它更灵活?
你想想看,一个驱动程序,如果每次改个参数都要重新编译,那得多麻烦。我个人习惯,能用参数解决的问题,绝不动代码。这一章,我们就来聊聊怎么让模块在加载时接受参数,以及如何让模块之间互相调用函数。
module_param:加载时传参,省去编译之苦
说白了,module_param 就是一个宏,它把模块内部的变量暴露给加载器。你在 insmod 的时候,可以像给函数传参一样,给模块变量赋值。
基本用法很简单:
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
static int debug_level = 0;
static char *device_name = "default_dev";
module_param(debug_level, int, 0644);
module_param(device_name, charp, 0644);
MODULE_PARM_DESC(debug_level, "调试级别:0-关闭,1-基本信息,2-详细信息");
MODULE_PARM_DESC(device_name, "设备名称");
static int __init my_init(void)
{
printk(KERN_INFO "模块加载,调试级别:%d,设备:%s\n",
debug_level, device_name);
return 0;
}
module_init(my_init);
MODULE_LICENSE("GPL");
加载时这样用:
sudo insmod my_module.ko debug_level=2 device_name="my_usb_dev"
这里有个细节,0644 是 sysfs 中的文件权限。如果你设成 0,参数就不会出现在 /sys/module/ 下,运行时也就没法改了。
0644,方便运行时调整。发布版本给 0 或 0444,防止用户误操作。
参数类型与权限:不只是 int 和 charp
支持的参数类型比你想的要多。除了 int、charp,还有 long、uint、bool、invbool(反向布尔值)等。
数组参数怎么传?用 module_param_array:
static int irq_array[4];
static int irq_count;
module_param_array(irq_array, int, &irq_count, 0644);
// 加载时:insmod my_module.ko irq_array=10,20,30,40
嗯,这里要注意:数组长度由 irq_count 自动记录,你不需要手动指定个数。
| 参数类型 | 宏 | 示例值 | 说明 |
|---|---|---|---|
| int | module_param(name, int, perm) | debug_level=2 | 标准整型 |
| charp | module_param(name, charp, perm) | name="mydev" | 字符串指针 |
| bool | module_param(name, bool, perm) | enable=1 | 布尔值,接受 0/1 或 y/n |
| int 数组 | module_param_array(name, int, &count, perm) | arr=1,2,3 | 逗号分隔 |
charp 传的值如果带空格,必须用引号括起来。比如 name="my device",否则 insmod 会把它当成两个参数。
EXPORT_SYMBOL:把函数“借”给别的模块
模块之间怎么调用函数?你不能直接 #include 对方的 .c 文件。内核提供了一套符号导出机制——EXPORT_SYMBOL。
假设你写了一个计算模块 calc.c:
#include <linux/module.h>
#include <linux/init.h>
int add(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(add);
int sub(int a, int b)
{
return a - b;
}
EXPORT_SYMBOL(sub);
static int __init calc_init(void)
{
printk(KERN_INFO "计算模块加载\n");
return 0;
}
module_init(calc_init);
MODULE_LICENSE("GPL");
另一个模块 user.c 就可以这样用:
extern int add(int, int);
extern int sub(int, int);
static int __init user_init(void)
{
int result = add(10, 5);
printk(KERN_INFO "10 + 5 = %d\n", result);
return 0;
}
module_init(user_init);
MODULE_LICENSE("GPL");
加载顺序很重要:先加载 calc.ko,再加载 user.ko。反过来会报错,说符号找不到。
符号导出与模块依赖
内核怎么知道模块之间有依赖?它通过 Module.symvers 文件来追踪。当你编译 calc.c 时,会生成这个文件,里面记录了导出的符号和对应的 crc 校验值。
编译 user.c 时,需要把 calc 的 Module.symvers 复制过来,或者用 KBUILD_EXTRA_SYMBOLS 指定路径:
make -C /lib/modules/$(uname -r)/build M=$(PWD) \
KBUILD_EXTRA_SYMBOLS=/path/to/calc/Module.symvers
加载时,insmod 会自动处理依赖。你可以用 lsmod 查看模块间的引用计数:
$ lsmod
Module Size Used by
user 16384 0
calc 16384 1 user
看到 Used by 那一列了吗?calc 被 user 引用了一次,所以计数是 1。这时候你没法 rmmod calc,必须先卸载 user。
- EXPORT_SYMBOL 导出的符号对所有模块可见
- EXPORT_SYMBOL_GPL 只对 GPL 许可的模块可见
- 模块加载顺序由依赖关系决定,
insmod会自动处理 - 卸载时先卸引用者,再卸被引用者
GPL 与符号可见性
为什么要有 EXPORT_SYMBOL_GPL?这是内核社区的一个“君子协定”。如果你的模块声明了 MODULE_LICENSE("GPL"),才能调用 EXPORT_SYMBOL_GPL 导出的符号。非 GPL 模块调用这些符号,内核会打印警告,甚至拒绝加载。
我个人建议:如果你写的是开源驱动,一律用 EXPORT_SYMBOL_GPL。这既是对社区的尊重,也能避免一些法律风险。
知识结构图:模块参数与符号调用全景
实战避坑指南
我在项目中遇到过几次因为符号导出顺序搞错,导致系统启动时模块加载失败的情况。这里总结几个常见问题:
- 符号找不到:检查
Module.symvers是否包含目标符号,CRC 是否匹配。内核版本不同,CRC 会变。 - 加载顺序错误:用
modprobe代替insmod,它会自动处理依赖。 - 参数不生效:确认参数名和变量名一致,权限不为
0。 - GPL 冲突:非 GPL 模块调用
EXPORT_SYMBOL_GPL符号,内核会taint(污染)标记。
EXPORT_SYMBOL_GPL 导出的内核函数。加载时内核直接报 Unknown symbol,查了半天才发现是 GPL 限制。后来老老实实改成了 EXPORT_SYMBOL 或者自己重新实现。
嗯,这一章的内容就到这里。模块参数和符号导出,是让内核模块从“死代码”变成“活组件”的关键。参数让模块可配置,符号导出让模块可组合。你想想看,一个驱动如果能通过参数调整缓冲区大小,又能导出接口给上层协议栈调用,那它的复用性就大大提升了。