4. 模块参数与导出符号:module_param宏、EXPORT_SYMBOL、参数传递与调试
内核模块在加载时,往往需要根据运行环境或用户意图调整行为,例如指定缓冲区大小、启用调试日志、设置设备编号等。Linux 内核提供了 模块参数 机制,允许在 insmod 或 modprobe 时向模块传递参数。同时,模块之间可以通过 导出符号 共享函数和变量,实现模块间的协作。本章将深入讲解 module_param 宏、EXPORT_SYMBOL 的使用方法,以及参数传递与调试的实战技巧。
核心要点:模块参数让模块具备运行时配置能力;导出符号是模块间依赖与通信的基础。掌握这两者,是编写可配置、可扩展内核模块的关键。
4.1 模块参数:module_param 宏
模块参数允许用户在加载模块时,通过命令行指定参数值,从而改变模块的内部行为。内核提供了 module_param 宏来声明这些参数。
4.1.1 基本语法与类型
#include <linux/moduleparam.h>
module_param(name, type, perm);
- name:参数变量名,必须是模块内已定义的全局变量。
- type:参数类型,内核支持多种基本类型。
- perm:文件权限位,用于控制
/sys/module/<module_name>/parameters/下对应文件的访问权限。若为 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 |
反转布尔值 |
4.1.2 参数传递示例
// param_demo.c
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
static int buffer_size = 1024;
static char *device_name = "default_dev";
static bool debug_enable = false;
module_param(buffer_size, int, 0644);
module_param(device_name, charp, 0644);
module_param(debug_enable, bool, 0644);
MODULE_PARM_DESC(buffer_size, "Size of internal buffer (default: 1024)");
MODULE_PARM_DESC(device_name, "Device name string");
MODULE_PARM_DESC(debug_enable, "Enable debug output (Y/N)");
static int __init param_demo_init(void)
{
printk(KERN_INFO "param_demo: buffer_size = %d\n", buffer_size);
printk(KERN_INFO "param_demo: device_name = %s\n", device_name);
printk(KERN_INFO "param_demo: debug_enable = %s\n",
debug_enable ? "true" : "false");
return 0;
}
static void __exit param_demo_exit(void)
{
printk(KERN_INFO "param_demo: module unloaded\n");
}
module_init(param_demo_init);
module_exit(param_demo_exit);
MODULE_LICENSE("GPL");
加载与验证:
# 加载模块并传递参数
sudo insmod param_demo.ko buffer_size=4096 device_name="mydev" debug_enable=Y
# 查看内核日志
dmesg | tail -3
# 输出示例:
# [ 1234.567890] param_demo: buffer_size = 4096
# [ 1234.567891] param_demo: device_name = mydev
# [ 1234.567892] param_demo: debug_enable = true
# 查看 sysfs 中的参数文件
cat /sys/module/param_demo/parameters/buffer_size
# 输出:4096
# 运行时修改参数(需 root 权限)
echo 8192 | sudo tee /sys/module/param_demo/parameters/buffer_size
提示:使用
MODULE_PARM_DESC 为每个参数添加描述信息,这些信息可通过 modinfo param_demo.ko 查看,有助于用户理解参数含义。
4.1.3 参数数组
内核还支持传递数组参数,使用 module_param_array 宏:
static int my_array[10];
static int array_count;
module_param_array(my_array, int, &array_count, 0644);
// 加载示例:insmod mymodule.ko my_array=1,2,3,4,5
array_count 会被自动设置为实际传入的元素个数。
4.2 导出符号:EXPORT_SYMBOL
内核模块可以导出符号(函数或全局变量),供其他模块使用。这是实现模块间依赖和功能扩展的基础机制。
4.2.1 导出符号的宏
| 宏 | 说明 |
|---|---|
EXPORT_SYMBOL(symbol) |
导出符号,所有模块均可使用 |
EXPORT_SYMBOL_GPL(symbol) |
仅导出给声明为 GPL 许可证的模块 |
EXPORT_SYMBOL_GPL_FUTURE(symbol) |
当前所有模块可用,但未来版本将仅限 GPL 模块 |
4.2.2 导出符号示例
导出模块(exporter.c):
// exporter.c
#include <linux/module.h>
#include <linux/init.h>
int exported_var = 42;
void exported_function(int value)
{
printk(KERN_INFO "exported_function called with value = %d\n", value);
}
EXPORT_SYMBOL(exported_var);
EXPORT_SYMBOL_GPL(exported_function);
static int __init exporter_init(void)
{
printk(KERN_INFO "exporter module loaded\n");
return 0;
}
static void __exit exporter_exit(void)
{
printk(KERN_INFO "exporter module unloaded\n");
}
module_init(exporter_init);
module_exit(exporter_exit);
MODULE_LICENSE("GPL");
使用模块(importer.c):
// importer.c
#include <linux/module.h>
#include <linux/init.h>
/* 声明外部符号 */
extern int exported_var;
extern void exported_function(int value);
static int __init importer_init(void)
{
printk(KERN_INFO "importer: exported_var = %d\n", exported_var);
exported_function(100);
return 0;
}
static void __exit importer_exit(void)
{
printk(KERN_INFO "importer module unloaded\n");
}
module_init(importer_init);
module_exit(importer_exit);
MODULE_LICENSE("GPL");
加载顺序:必须先加载导出模块,再加载使用模块。
sudo insmod exporter.ko
sudo insmod importer.ko
# 查看模块依赖
lsmod | grep importer
# 输出示例:
# importer 16384 0
# exporter 16384 0 importer
注意:如果加载顺序错误,importer 模块会报
Unknown symbol 错误。使用 modprobe 可以自动处理依赖关系,但需要将模块安装到标准路径。
4.3 参数传递与调试实战
在实际开发中,模块参数和导出符号是调试和故障排查的有力工具。以下是一些实战技巧。
4.3.1 使用参数控制调试输出
通过模块参数动态控制调试信息的打印级别,避免重新编译模块:
static int debug_level = 0;
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Debug level (0=off, 1=info, 2=verbose)");
#define DBG_INFO(fmt, ...) \
do { if (debug_level >= 1) \
printk(KERN_INFO "mymod: " fmt, ##__VA_ARGS__); \
} while (0)
#define DBG_VERBOSE(fmt, ...) \
do { if (debug_level >= 2) \
printk(KERN_DEBUG "mymod: " fmt, ##__VA_ARGS__); \
} while (0)
4.3.2 查看导出符号表
使用 nm 或 objdump 查看模块的导出符号:
# 查看模块导出的符号
nm exporter.ko | grep -E " [TtDd] "
# 输出示例:
# 0000000000000000 D exported_var
# 0000000000000020 T exported_function
# 查看系统全局导出符号(/proc/kallsyms)
grep "exported_var" /proc/kallsyms
# 输出示例:
# ffffffffc0a3b000 D exported_var [exporter]
4.3.3 常见故障与排查
| 故障现象 | 可能原因 | 排查方法 |
|---|---|---|
Unknown symbol |
依赖模块未加载或符号未导出 | 使用 modprobe --show-depends 检查依赖;nm 确认符号是否导出 |
| 参数值未生效 | 参数名拼写错误或类型不匹配 | 检查 dmesg 中的参数解析错误;使用 modinfo 查看参数定义 |
| sysfs 参数文件不可写 | perm 权限设置不正确 |
确认 module_param 的 perm 参数包含写权限(如 0644) |
| 模块加载顺序错误 | 使用 insmod 手动加载时顺序颠倒 |
改用 modprobe 自动处理依赖;或调整加载顺序 |
4.4 模块参数与导出符号关系图
下图展示了模块参数传递、导出符号以及 sysfs 接口之间的整体关系:
4.5 本章小结
- module_param 宏是模块参数的核心,支持多种基本类型和数组,通过 sysfs 可在运行时查看和修改。
- EXPORT_SYMBOL 系列宏用于导出符号,
EXPORT_SYMBOL_GPL可限制符号仅被 GPL 模块使用。 - 模块加载时,参数通过
insmod或modprobe命令行传递;导出符号的依赖关系要求正确的加载顺序。 - 利用模块参数控制调试级别、通过
nm和/proc/kallsyms检查符号表,是日常调试的有效手段。
最佳实践:为每个模块参数添加
MODULE_PARM_DESC 描述;优先使用 EXPORT_SYMBOL_GPL 保护知识产权;在模块卸载时清理参数相关的动态资源。