4. 模块参数与导出符号:module_param的使用、EXPORT_SYMBOL的机制、模块间依赖关系
模块写好了,怎么跟用户空间交互?模块之间怎么互相调用?这两个问题,说白了就是本章要讲的 模块参数 和 导出符号。
我刚开始做驱动开发时,经常为了调试一个参数反复编译模块,后来才发现有 module_param 这种好东西。至于模块间依赖,嗯,踩过的坑也不少,咱们一个一个说。
4.1 模块参数:让用户空间控制内核模块
模块参数允许你在加载模块时传递参数,或者在 /sys/module/<name>/parameters/ 下动态修改。说白了,就是给内核模块开了一个可控的“后门”。
4.1.1 基本用法
先看一个最简单的例子:
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/stat.h>
static int debug_level = 0;
static char *device_name = "default_dev";
static int array_param[4] = {0, 1, 2, 3};
module_param(debug_level, int, S_IRUGO | S_IWUSR);
module_param(device_name, charp, S_IRUGO);
module_param_array(array_param, int, NULL, S_IRUGO);
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
MODULE_PARM_DESC(device_name, "Device name string");
MODULE_PARM_DESC(array_param, "Sample array parameter");
加载时这样传参:
sudo insmod my_module.ko debug_level=2 device_name="mydev"
我个人习惯把调试开关都做成模块参数,这样在开发阶段不用反复编译。你想想看,每次改个打印级别都要重新编译,多浪费时间。
4.1.2 参数权限与类型
| 类型宏 | C类型 | 说明 |
|---|---|---|
module_param(name, byte, perm) |
unsigned char |
字节类型 |
module_param(name, int, perm) |
int |
整型,最常用 |
module_param(name, uint, perm) |
unsigned int |
无符号整型 |
module_param(name, long, perm) |
long |
长整型 |
module_param(name, charp, perm) |
char * |
字符串指针 |
module_param(name, bool, perm) |
bool |
布尔值,支持0/1或y/n |
module_param_array(name, type, num, perm) |
数组 | 数组参数,num返回元素个数 |
权限位 perm 使用 <linux/stat.h> 中的宏:
S_IRUGO— 所有人可读S_IWUSR— 所有者可写S_IRUSR | S_IWUSR— 所有者读写0— 不在sysfs中显示
S_IWUGO,结果用户空间随便一个进程就能改掉内核模块的行为。权限一定要收窄,生产环境建议只给 S_IRUGO 甚至 0。
4.2 EXPORT_SYMBOL:模块间的“API接口”
模块A想调用模块B里的函数,怎么办?这就用到了 EXPORT_SYMBOL。它把内核符号导出到全局符号表,其他模块就能通过 extern 声明来调用。
4.2.1 导出与导入
先看导出方(模块B):
// module_b.c
#include <linux/module.h>
int my_add(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(my_add);
int my_sub(int a, int b)
{
return a - b;
}
EXPORT_SYMBOL_GPL(my_sub); // 只允许GPL模块使用
再看导入方(模块A):
// module_a.c
#include <linux/module.h>
extern int my_add(int a, int b);
extern int my_sub(int a, int b);
static int __init a_init(void)
{
printk("3 + 5 = %d\n", my_add(3, 5));
printk("10 - 4 = %d\n", my_sub(10, 4));
return 0;
}
4.2.2 EXPORT_SYMBOL vs EXPORT_SYMBOL_GPL
| 宏 | 许可证要求 | 使用场景 |
|---|---|---|
EXPORT_SYMBOL |
无限制 | 通用接口,商业模块也可调用 |
EXPORT_SYMBOL_GPL |
必须GPL | 核心接口,保护GPL代码 |
EXPORT_SYMBOL_GPL_FUTURE |
当前无限制,未来可能变GPL | 过渡期使用,很少见 |
EXPORT_SYMBOL_GPL。这不仅是许可证合规问题,更是对开源社区的一种尊重。我在给某个商业公司做BSP时,他们要求所有导出符号都用 EXPORT_SYMBOL,结果后来被社区吐槽了很久。
4.3 模块间依赖关系
模块A依赖模块B,意味着加载A之前必须先加载B,卸载时则相反。这种依赖关系由内核自动维护,但你需要正确声明。
4.3.1 依赖声明
在模块A中,使用 MODULE_SOFTDEP 或 MODULE_DEPEND 声明依赖:
// module_a.c
MODULE_SOFTDEP("pre: module_b"); // 软依赖,建议先加载module_b
MODULE_LICENSE("GPL");
更常见的做法是,只要模块A调用了模块B导出的符号,内核在加载A时会自动检查B是否已加载。如果B没加载,insmod 会失败并提示 Unknown symbol。
4.3.2 查看依赖关系
用 lsmod 可以查看模块间的依赖:
$ lsmod
Module Size Used by
module_a 16384 0
module_b 20480 1 module_a
注意 Used by 列,module_b 显示 1 module_a,说明有一个模块在使用它。
用 modinfo 也能看到依赖信息:
$ modinfo module_a.ko
filename: module_a.ko
depends: module_b
...
4.3.3 依赖链与循环依赖
内核不允许循环依赖。比如模块A依赖B,B依赖C,C又依赖A——这种链在加载时会直接报错。
4.4 知识结构总览
下面这张图把本章的核心逻辑串起来了:
4.5 实战经验总结
最后分享几个我在项目中积累的经验:
- 参数命名规范: 我习惯用
模块名_参数名的格式,比如usb_debug_level,避免全局命名冲突。 - 导出符号最小化: 只导出必要的接口,不要图省事把所有函数都
EXPORT_SYMBOL。内核符号表越臃肿,维护成本越高。 - 依赖关系可视化: 用
lsmod和modinfo检查依赖链,复杂项目建议画个依赖图。 - 调试技巧: 如果遇到
Unknown symbol错误,先cat /proc/kallsyms | grep 符号名看看符号是否真的被导出了。
一句话总结: module_param 让模块可配置,EXPORT_SYMBOL 让模块可协作,依赖关系让模块可组合。这三者构成了内核模块生态的基础设施。