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/ 下创建对应的文件。用户可以通过读写这些文件动态修改参数值(需满足权限要求)。

模块参数与 sysfs 交互流程 用户空间 insmod / modprobe sysfs 文件系统 内核模块 模块参数变量 说明: 1. 加载时通过 insmod 传递参数直接写入模块变量。 2. 运行时通过 sysfs 文件读写动态调整参数值(需 perm 允许)。

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 时会自动解析这些符号的地址。

模块间符号调用架构 内核符号表 (kallsyms) 模块 A (导出者) EXPORT_SYMBOL(func_a) EXPORT_SYMBOL(var_x) 模块 B (调用者) extern int func_a(); extern int var_x; 模块 C (GPL 调用者) 调用流程: 1. 模块 A 加载时将符号注册到内核符号表。 2. 模块 B 加载时解析未定义符号,从符号表查找。 3. 模块 B 直接调用模块 A 的函数/变量。

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 控制符号的可见性与使用限制。
  • 模块间符号调用依赖内核符号表,加载顺序和版本一致性至关重要。
  • 结合参数与导出符号,可以构建高度可配置、可扩展的内核模块体系。