3. 模块参数与导出符号:向内核模块传递参数与符号共享
在 Linux 内核模块开发中,模块参数与导出符号是模块间通信与配置的核心机制。模块参数允许在加载模块时动态调整行为,而导出符号则使一个模块的函数或变量能被其他模块调用。本章将深入讲解这两种机制的实现原理与实战用法。
3.1 模块参数:动态配置内核模块
模块参数(module parameter)是 Linux 内核提供的一种机制,允许在 insmod 或 modprobe 加载模块时,通过命令行传递参数值,从而改变模块的运行行为。参数定义在模块源码中,使用 module_param() 宏声明。
3.1.1 模块参数声明宏
| 宏 | 参数类型 | 说明 |
|---|---|---|
module_param(name, type, perm) |
基本类型 | 声明一个模块参数,name 为变量名,type 为数据类型,perm 为 sysfs 文件权限 |
module_param_array(name, type, nump, perm) |
数组类型 | 声明一个数组参数,nump 指向记录数组元素个数的变量 |
module_param_string(name, string, len, perm) |
字符串类型 | 声明一个字符串参数,len 为缓冲区长度 |
MODULE_PARM_DESC(name, desc) |
描述 | 为参数添加描述信息,可通过 modinfo 查看 |
支持的基本类型:byte、short、ushort、int、uint、long、ulong、charp(字符串指针)、bool、invbool(反转布尔值)。
3.1.2 模块参数实战示例
以下代码演示如何定义并使用模块参数:
#include <linux/module.h>
#include <linux/kernel.h>
#include <linux/init.h>
/* 定义模块参数变量 */
static int debug_level = 0;
static char *device_name = "default_dev";
static int array_param[5];
static int array_count;
/* 声明模块参数 */
module_param(debug_level, int, 0644);
module_param(device_name, charp, 0644);
module_param_array(array_param, int, &array_count, 0644);
/* 添加参数描述 */
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
MODULE_PARM_DESC(device_name, "Device name string");
MODULE_PARM_DESC(array_param, "Array of integers");
static int __init param_demo_init(void)
{
int i;
printk(KERN_INFO "Module loaded with:\n");
printk(KERN_INFO " debug_level = %d\n", debug_level);
printk(KERN_INFO " device_name = %s\n", device_name);
printk(KERN_INFO " array_count = %d\n", array_count);
for (i = 0; i < array_count; i++)
printk(KERN_INFO " array_param[%d] = %d\n", i, array_param[i]);
return 0;
}
static void __exit param_demo_exit(void)
{
printk(KERN_INFO "Module unloaded\n");
}
module_init(param_demo_init);
module_exit(param_demo_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("Kernel Expert");
MODULE_DESCRIPTION("Module parameter demo");
insmod param_demo.ko debug_level=2 device_name="mydev" array_param=10,20,30 传递参数。若未传递参数,则使用代码中的默认值。
3.1.3 参数权限与 sysfs 接口
perm 参数控制模块参数在 /sys/module/<module_name>/parameters/ 目录下的文件权限:
- 0:不在 sysfs 中创建文件,参数仅加载时可用。
- S_IRUGO(0444):只读,用户空间可查看但不能修改。
- S_IWUSR | S_IRUGO(0644):所有者可写,其他用户只读,可在运行时修改。
3.2 导出符号:模块间函数与变量共享
导出符号(exported symbol)是 Linux 内核实现模块间依赖与功能复用的基础。通过 EXPORT_SYMBOL 或 EXPORT_SYMBOL_GPL 宏,一个模块可以将自己的函数或变量公开,供其他模块调用。
3.2.1 导出符号宏
| 宏 | 许可限制 | 说明 |
|---|---|---|
EXPORT_SYMBOL(symbol) |
无限制 | 导出符号,任何模块均可使用 |
EXPORT_SYMBOL_GPL(symbol) |
仅 GPL 许可 | 导出符号,仅 GPL 兼容模块可使用 |
3.2.2 导出符号实战示例
导出模块(exporter.c):
#include <linux/module.h>
#include <linux/kernel.h>
/* 定义要导出的函数 */
int add_numbers(int a, int b)
{
return a + b;
}
void print_message(const char *msg)
{
printk(KERN_INFO "Exported: %s\n", msg);
}
/* 导出符号 */
EXPORT_SYMBOL(add_numbers);
EXPORT_SYMBOL_GPL(print_message);
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");
MODULE_DESCRIPTION("Symbol exporter demo");
使用模块(importer.c):
#include <linux/module.h>
#include <linux/kernel.h>
/* 声明外部符号 */
extern int add_numbers(int a, int b);
extern void print_message(const char *msg);
static int __init importer_init(void)
{
int result;
result = add_numbers(10, 20);
printk(KERN_INFO "Importer: add_numbers(10, 20) = %d\n", result);
print_message("Hello from importer module!");
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");
MODULE_DESCRIPTION("Symbol importer demo");
modprobe 时,内核会自动处理依赖关系。
3.3 模块参数与导出符号的协同架构
以下 SVG 图展示了模块参数与导出符号在 Linux 内核模块系统中的完整交互流程:
3.4 模块依赖与加载顺序管理
当模块 B 导出符号、模块 C 使用这些符号时,内核会自动记录模块间的依赖关系。使用 lsmod 可查看模块依赖:
$ lsmod
Module Size Used by
importer 16384 0
exporter 16384 1 importer
依赖管理要点:
- 加载顺序:导出模块必须先于使用模块加载,否则使用模块加载时会因符号未定义而失败。
- 卸载顺序:使用模块必须先于导出模块卸载,否则导出模块卸载时内核会因仍有模块引用其符号而拒绝卸载。
- 自动处理:使用
modprobe加载模块时,会自动解析依赖并按照正确顺序加载所有需要的模块。 - 查看依赖:
modinfo -F depends <module_name>可查看模块的依赖列表。
3.5 模块参数与导出符号的对比
| 特性 | 模块参数 | 导出符号 |
|---|---|---|
| 目的 | 向模块传递配置数据 | 模块间共享函数/变量 |
| 传递方向 | 用户空间 → 内核模块 | 内核模块 → 内核模块 |
| 数据类型 | 基本类型、数组、字符串 | 函数、全局变量 |
| 运行时修改 | 支持(取决于权限设置) | 不支持(符号地址固定) |
| sysfs 接口 | 自动生成(权限非0时) | 不生成 |
| 许可限制 | 无 | EXPORT_SYMBOL_GPL 限制非GPL模块使用 |
| 依赖关系 | 无依赖 | 产生模块间依赖 |
3.6 实战技巧与避坑指南
my_drv_debug 而非 debug。
grep <symbol_name> /proc/kallsyms 可检查符号是否已被占用。若冲突,模块加载时会报 "Unknown symbol" 错误。
调试检查命令:
# 查看模块参数
ls /sys/module/<module_name>/parameters/
cat /sys/module/<module_name>/parameters/<param_name>
# 查看模块导出符号
cat /proc/kallsyms | grep <module_name>
# 查看模块依赖
lsmod | grep <module_name>
modinfo -F depends <module_name>
3.7 本章小结
模块参数与导出符号是 Linux 内核模块开发中不可或缺的基础设施。模块参数提供了灵活的动态配置能力,使同一模块代码可适应不同场景;导出符号则构建了模块间的协作生态,实现了代码复用与功能分层。掌握这两种机制,是编写可配置、可扩展、可维护内核模块的第一步。