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");
加载顺序: 必须先加载导出模块(exporter),再加载使用模块(importer)。使用 modprobe 时,内核会自动处理依赖关系。

3.3 模块参数与导出符号的协同架构

以下 SVG 图展示了模块参数与导出符号在 Linux 内核模块系统中的完整交互流程:

模块参数与导出符号协同架构 用户空间 insmod / modprobe 命令 传递参数:debug_level=2 device_name="mydev" 内核空间 模块 A(参数接收者) module_param(debug_level, int, 0644) module_param(device_name, charp, 0644) module_param_array(array_param, int, &count, 0644) 参数值存储在模块变量中 sysfs: /sys/module/module_a/parameters/ 模块 B(符号导出者) EXPORT_SYMBOL(add_numbers) EXPORT_SYMBOL_GPL(print_message) 函数实现: int add_numbers(int a, int b) void print_message(const char *msg) 模块 C(符号使用者) extern int add_numbers(int a, int b); extern void print_message(const char *msg); 调用导出函数实现功能 依赖关系:模块 C → 模块 B 参数传递 符号引用 内核符号表 add_numbers print_message __this_module (模块B的符号) 符号注册

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 内核模块开发中不可或缺的基础设施。模块参数提供了灵活的动态配置能力,使同一模块代码可适应不同场景;导出符号则构建了模块间的协作生态,实现了代码复用与功能分层。掌握这两种机制,是编写可配置、可扩展、可维护内核模块的第一步。