3. 模块参数与符号导出:module_param宏的使用、EXPORT_SYMBOL机制、模块依赖关系
在嵌入式Linux驱动开发中,模块化设计不仅要求代码能够动态加载和卸载,还要求模块之间能够协同工作、传递配置参数。本章聚焦于驱动模块的“对外接口”:模块参数允许用户在加载时动态调整驱动行为;符号导出使得一个模块的函数或变量可以被其他模块调用;而模块依赖关系则确保内核在加载和卸载时能正确处理模块间的引用链。
3.1 模块参数:module_param 宏的使用
模块参数允许驱动开发者在 insmod 或 modprobe 时向模块内部传递配置值,而无需重新编译内核或驱动。这对于调试开关、缓冲区大小、设备模式等运行时可调参数非常有用。
3.1.1 基本宏与数据类型
内核提供了 module_param 和 module_param_named 等宏,定义在 <linux/moduleparam.h> 中。其基本语法为:
module_param(name, type, perm);
其中 name 是变量名,type 是参数类型,perm 是 sysfs 文件权限(0 表示不创建 sysfs 条目)。
| 类型标识 | C 数据类型 | 说明 |
|---|---|---|
byte |
unsigned char |
单字节值 |
short |
short |
短整型 |
ushort |
unsigned short |
无符号短整型 |
int |
int |
整型 |
uint |
unsigned int |
无符号整型 |
long |
long |
长整型 |
ulong |
unsigned long |
无符号长整型 |
charp |
char * |
字符串指针(指向用户传入的副本) |
bool |
bool |
布尔值(支持 0/1, y/n, Y/N) |
invbool |
bool |
反转布尔值 |
3.1.2 数组参数与描述
对于数组类型参数,使用 module_param_array:
module_param_array(name, type, nump, perm);
其中 nump 是一个指向 unsigned int 的指针,用于接收实际传入的元素个数。
内核还支持 MODULE_PARM_DESC 宏为参数添加描述信息,这些信息可通过 modinfo 命令查看:
static int debug_level = 0;
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Debug level (0=off, 1=info, 2=verbose)");
static char *mode = "normal";
module_param(mode, charp, 0);
MODULE_PARM_DESC(mode, "Operation mode: normal|fast|safe");
3.1.3 加载时传参示例
用户空间加载模块时传递参数:
# insmod my_driver.ko debug_level=2 mode="fast"
通过 modprobe 加载时,参数可以写在 /etc/modprobe.d/ 配置文件中:
options my_driver debug_level=1 mode=normal
perm 参数使用标准的 Linux 文件权限八进制表示。设置为 0 表示不在 /sys/module/ 下创建对应的参数文件;设置为 0444 表示只读;0644 表示 root 可写、其他人只读。运行时可通过 sysfs 修改参数值(如果权限允许)。
3.2 EXPORT_SYMBOL 机制
内核模块之间需要共享函数或全局变量时,必须显式导出符号。Linux 内核提供了 EXPORT_SYMBOL 和 EXPORT_SYMBOL_GPL 两个宏,用于将符号(函数名或变量名)加入到内核符号表中。
3.2.1 导出宏的对比
| 宏 | 导出范围 | 使用限制 | 典型场景 |
|---|---|---|---|
EXPORT_SYMBOL(name) |
所有模块(包括非GPL模块) | 无 | 通用API、硬件抽象层 |
EXPORT_SYMBOL_GPL(name) |
仅GPL兼容模块 | 非GPL模块无法引用 | 核心子系统、专有接口保护 |
3.2.2 导出与引用示例
导出模块(exporter.c):
#include <linux/module.h>
#include <linux/kernel.h>
int my_add(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(my_add);
int my_mul(int a, int b)
{
return a * b;
}
EXPORT_SYMBOL_GPL(my_mul);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Expert");
引用模块(importer.c):
#include <linux/module.h>
#include <linux/kernel.h>
extern int my_add(int a, int b);
extern int my_mul(int a, int b);
static int __init importer_init(void)
{
pr_info("my_add(3, 4) = %d\n", my_add(3, 4));
pr_info("my_mul(3, 4) = %d\n", my_mul(3, 4));
return 0;
}
static void __exit importer_exit(void)
{
pr_info("importer module unloaded\n");
}
module_init(importer_init);
module_exit(importer_exit);
MODULE_LICENSE("GPL");
- 导出符号的模块必须先于引用模块加载,否则引用模块在加载时会出现
Unknown symbol错误。 - 使用
EXPORT_SYMBOL_GPL导出的符号,如果引用模块未声明MODULE_LICENSE("GPL"),内核将拒绝加载该引用模块。 - 符号名在编译时会被内核附加
__ksymtab_前缀,可通过/proc/kallsyms查看所有导出的符号。
3.2.3 查看导出符号
加载模块后,可以通过以下方式验证符号是否成功导出:
# cat /proc/kallsyms | grep my_add
# nm exporter.ko | grep my_add
在编译引用模块时,需要确保能够找到导出符号的声明。通常通过头文件或 extern 声明来实现。
3.3 模块依赖关系
当模块 A 导出的符号被模块 B 引用时,模块 B 就依赖于模块 A。内核通过 模块引用计数 和 符号依赖链 来管理这种关系。
3.3.1 依赖关系的自动生成
使用 modprobe 加载模块时,它会自动解析 /lib/modules/$(uname -r)/modules.dep 文件,按依赖顺序加载所有需要的模块。该文件由 depmod 命令生成:
# depmod -a
# cat /lib/modules/5.10.0/modules.dep | grep importer
kernel/drivers/misc/importer.ko: kernel/drivers/misc/exporter.ko
3.3.2 手动管理依赖
如果不使用 modprobe,手动加载时必须遵循依赖顺序:
# insmod exporter.ko
# insmod importer.ko
卸载时顺序相反:
# rmmod importer
# rmmod exporter
如果尝试先卸载 exporter,内核会提示 Module is in use:
# rmmod exporter
rmmod: ERROR: Module exporter is in use by: importer
3.3.3 查看模块依赖
使用 lsmod 可以查看当前加载模块的引用计数和依赖模块列表:
# lsmod
Module Size Used by
importer 16384 0
exporter 16384 1 importer
其中 Used by 列显示引用该模块的其他模块名称。数字表示引用计数。
| 命令/文件 | 作用 | 示例输出 |
|---|---|---|
lsmod |
查看当前加载的模块及引用计数 | exporter 16384 1 importer |
modinfo -F depends exporter.ko |
查看模块的依赖列表 | depends: (空表示无依赖) |
cat /proc/kallsyms |
查看所有内核符号(包括导出的) | ffffffffc0001234 T my_add |
nm exporter.ko |
查看模块目标文件中的符号 | 0000000000000000 T my_add |
modprobe 可以自动处理依赖链,推荐在开发和生产环境中使用。
3.4 综合实践:构建带参数和符号导出的驱动模块
下面是一个完整的示例,展示如何在一个模块中同时使用模块参数和符号导出,并在另一个模块中引用这些符号。
文件:calc_ops.c(导出模块)
#include <linux/module.h>
#include <linux/kernel.h>
static int op_debug = 0;
module_param(op_debug, int, 0644);
MODULE_PARM_DESC(op_debug, "Enable debug output (0/1)");
int calc_add(int x, int y)
{
if (op_debug)
pr_info("calc_add: %d + %d = %d\n", x, y, x + y);
return x + y;
}
EXPORT_SYMBOL(calc_add);
int calc_sub(int x, int y)
{
if (op_debug)
pr_info("calc_sub: %d - %d = %d\n", x, y, x - y);
return x - y;
}
EXPORT_SYMBOL_GPL(calc_sub);
static int __init calc_init(void)
{
pr_info("calc_ops module loaded (debug=%d)\n", op_debug);
return 0;
}
static void __exit calc_exit(void)
{
pr_info("calc_ops module unloaded\n");
}
module_init(calc_init);
module_exit(calc_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Expert");
MODULE_DESCRIPTION("Basic arithmetic operations with module parameters");
文件:calc_user.c(引用模块)
#include <linux/module.h>
#include <linux/kernel.h>
extern int calc_add(int x, int y);
extern int calc_sub(int x, int y);
static int __init user_init(void)
{
int a = 10, b = 5;
pr_info("calc_user module loaded\n");
pr_info("calc_add(%d, %d) = %d\n", a, b, calc_add(a, b));
pr_info("calc_sub(%d, %d) = %d\n", a, b, calc_sub(a, b));
return 0;
}
static void __exit user_exit(void)
{
pr_info("calc_user module unloaded\n");
}
module_init(user_init);
module_exit(user_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Expert");
MODULE_DESCRIPTION("User of calc_ops module");
Makefile 片段:
obj-m += calc_ops.o
obj-m += calc_user.o
all:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules
clean:
make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean
加载与测试:
# insmod calc_ops.ko op_debug=1
# insmod calc_user.ko
# dmesg | tail -5
[ 1234.5678] calc_ops module loaded (debug=1)
[ 1234.5679] calc_user module loaded
[ 1234.5680] calc_add(10, 5) = 15
[ 1234.5681] calc_sub(10, 5) = 5
# rmmod calc_user
# rmmod calc_ops
- 在开发阶段,将
op_debug参数默认设为 1,方便调试;发布时再改为 0。 - 对于核心API,优先使用
EXPORT_SYMBOL_GPL以保护知识产权。 - 使用
modprobe -r可以自动处理依赖模块的卸载顺序。 - 在模块的
MODULE_DESCRIPTION中清晰描述其导出的符号和参数,方便其他开发者使用。
3.5 本章小结
| 知识点 | 核心要点 | 常用命令/宏 |
|---|---|---|
| 模块参数 | 允许加载时或运行时传递配置值 | module_param, module_param_array, MODULE_PARM_DESC |
| 符号导出 | 使模块的函数/变量可被其他模块引用 | EXPORT_SYMBOL, EXPORT_SYMBOL_GPL |
| 模块依赖 | 引用导出符号的模块自动形成依赖关系 | lsmod, depmod, modprobe |
| 加载顺序 | 被依赖模块必须先加载,后卸载 | insmod, rmmod |