4. 模块参数与导出符号:module_param宏、参数类型、EXPORT_SYMBOL、模块间符号调用
内核模块在加载时或运行过程中,经常需要接收外部传入的参数来调整行为,例如指定设备号、缓冲区大小或调试级别。同时,模块之间可以相互调用导出的函数或变量,实现功能复用。本章深入讲解 module_param 宏的使用、参数类型定义、EXPORT_SYMBOL 机制以及模块间符号调用的完整流程。
核心要点: 模块参数提供了一种灵活的用户态与内核态交互方式;导出符号则构建了内核模块间的依赖与协作基础。理解这两者,是编写可配置、可扩展内核模块的关键。
4.1 module_param 宏与参数类型
module_param 是 Linux 内核提供的一个宏,用于在模块中声明一个可以被用户空间在加载模块时(通过 insmod 或 modprobe)或通过 sysfs 文件系统动态修改的参数。其基本语法如下:
module_param(name, type, perm);
- name:模块内部定义的全局变量名。
- type:参数的数据类型,内核预定义了多种类型支持。
- perm:参数的访问权限(
sysfs文件权限位),0 表示不创建sysfs条目。
4.1.1 支持的参数类型
| 类型标识 | 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) |
invbool |
bool |
反转布尔值 |
权限说明:
perm 参数使用标准的 Linux 文件权限宏,如 S_IRUGO(所有人可读)、S_IWUSR(所有者可写)。设置为 0 时,参数仅在模块加载时可用,不会出现在 /sys/module/<模块名>/parameters/ 目录下。
4.1.2 完整示例:带参数的内核模块
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
static int debug_level = 0;
static char *device_name = "default_dev";
static int buffer_size = 1024;
module_param(debug_level, int, S_IRUGO | S_IWUSR);
module_param(device_name, charp, S_IRUGO);
module_param(buffer_size, uint, S_IRUGO);
MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
MODULE_PARM_DESC(device_name, "Device name string");
MODULE_PARM_DESC(buffer_size, "Buffer size in bytes");
static int __init param_demo_init(void)
{
printk(KERN_INFO "param_demo: debug_level = %d\n", debug_level);
printk(KERN_INFO "param_demo: device_name = %s\n", device_name);
printk(KERN_INFO "param_demo: buffer_size = %d\n", buffer_size);
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");
MODULE_AUTHOR("Kernel Expert");
MODULE_DESCRIPTION("Module parameter demonstration");
加载时传递参数:
sudo insmod param_demo.ko debug_level=2 device_name="mydev" buffer_size=4096
注意: 字符串参数
charp 指向的内存在模块加载时由内核分配,模块内部不应尝试修改该指针指向的内容。同时,MODULE_PARM_DESC 用于提供参数描述,可通过 modinfo 命令查看。
4.2 模块参数与 sysfs 交互
当 perm 不为 0 时,参数会在 /sys/module/<模块名>/parameters/ 下创建对应的文件。用户可以通过读写这些文件动态修改参数值(需满足权限要求)。
4.3 EXPORT_SYMBOL 与符号导出
内核模块可以通过 EXPORT_SYMBOL 或 EXPORT_SYMBOL_GPL 宏将函数或变量导出,供其他模块使用。导出的符号会出现在内核符号表 /proc/kallsyms 中。
4.3.1 导出宏对比
| 宏 | 使用限制 | 适用场景 |
|---|---|---|
EXPORT_SYMBOL |
无限制,所有模块均可调用 | 通用接口,如核心库函数 |
EXPORT_SYMBOL_GPL |
仅 GPL 许可的模块可调用 | 保护知识产权,强制 GPL 兼容 |
4.3.2 导出符号示例
导出模块(exporter.c):
#include <linux/module.h>
#include <linux/init.h>
int exported_var = 100;
EXPORT_SYMBOL(exported_var);
int exported_add(int a, int b)
{
return a + b;
}
EXPORT_SYMBOL(exported_add);
static int __init exporter_init(void)
{
printk(KERN_INFO "exporter: module loaded, exported_var = %d\n", exported_var);
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):
#include <linux/module.h>
#include <linux/init.h>
/* 声明外部符号 */
extern int exported_var;
extern int exported_add(int a, int b);
static int __init importer_init(void)
{
int result;
printk(KERN_INFO "importer: exported_var = %d\n", exported_var);
result = exported_add(10, 20);
printk(KERN_INFO "importer: exported_add(10, 20) = %d\n", result);
/* 修改导出变量 */
exported_var = 200;
printk(KERN_INFO "importer: modified exported_var to %d\n", exported_var);
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");
加载顺序: 必须先加载导出模块(exporter),再加载调用模块(importer)。卸载时顺序相反。可以使用
modprobe 自动处理依赖关系。
4.4 模块间符号调用机制
模块间符号调用的核心依赖于内核的符号表解析机制。当模块 B 引用模块 A 导出的符号时,内核在加载模块 B 时会自动解析这些符号的地址。
4.4.1 依赖关系管理
内核通过 Module.symvers 文件记录模块导出的符号及其 CRC 校验值。当编译调用模块时,编译器会检查符号的 CRC 是否匹配,确保接口一致性。
- 自动依赖: 使用
modprobe加载模块时,会自动加载其依赖的模块。 - 手动管理: 可以通过
depmod生成模块依赖文件modules.dep。 - 版本检查: 如果导出模块更新导致符号 CRC 变化,调用模块需要重新编译。
常见错误:
- Unknown symbol: 调用模块引用了未导出的符号,或导出模块未加载。
- 版本不匹配: 符号的 CRC 校验失败,通常需要重新编译调用模块。
- 循环依赖: 模块 A 导出符号给 B,B 又导出符号给 A,导致加载死锁。
4.5 实践:构建可配置的模块参数与符号导出
结合本章知识点,设计一个具有参数配置和符号导出的完整模块示例:
/* calculator.c - 可配置的计算器模块 */
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>
static int operation_mode = 0; /* 0: 加法, 1: 乘法 */
static int debug = 0;
module_param(operation_mode, int, S_IRUGO | S_IWUSR);
module_param(debug, bool, S_IRUGO);
MODULE_PARM_DESC(operation_mode, "0=add, 1=multiply");
MODULE_PARM_DESC(debug, "Enable debug output");
/* 导出函数 */
int calculate(int a, int b)
{
int result;
if (operation_mode == 0) {
result = a + b;
} else {
result = a * b;
}
if (debug)
printk(KERN_DEBUG "calculator: %d %s %d = %d\n",
a, operation_mode == 0 ? "+" : "*", b, result);
return result;
}
EXPORT_SYMBOL(calculate);
int get_operation_mode(void)
{
return operation_mode;
}
EXPORT_SYMBOL(get_operation_mode);
static int __init calculator_init(void)
{
printk(KERN_INFO "calculator: loaded, mode=%s\n",
operation_mode == 0 ? "add" : "multiply");
return 0;
}
static void __exit calculator_exit(void)
{
printk(KERN_INFO "calculator: unloaded\n");
}
module_init(calculator_init);
module_exit(calculator_exit);
MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Configurable calculator module with exported symbols");
关键总结:
module_param支持多种数据类型,通过perm控制运行时修改能力。EXPORT_SYMBOL和EXPORT_SYMBOL_GPL控制符号的可见性与使用限制。- 模块间符号调用依赖内核符号表,加载顺序和版本一致性至关重要。
- 结合参数与导出符号,可以构建高度可配置、可扩展的内核模块体系。