3. 内核模块参数与符号导出:module_param宏的使用、EXPORT_SYMBOL机制、模块依赖关系。

在Linux内核模块开发中,模块参数与符号导出是实现模块间协作与运行时配置的核心机制。本章将深入剖析module_param宏的使用方法、EXPORT_SYMBOL机制的底层原理,以及由此衍生的模块依赖关系管理。掌握这些技术,是构建可复用、可配置、可维护的内核模块的基础。

3.1 内核模块参数:module_param宏

内核模块参数允许用户在加载模块时动态传递配置值,而无需重新编译内核或模块。这一机制通过module_param宏族实现,它本质上是将模块内的全局变量暴露给insmod或modprobe工具。

3.1.1 module_param宏族

标准module_param宏定义在<linux/moduleparam.h>中,其基本形式如下:

module_param(name, type, perm);
  • name:模块内已定义的全局变量名。
  • type:参数的数据类型,如int、charp(字符串指针)、bool、long等。
  • perm:参数在/sys/module/<module_name>/parameters/下的文件权限,通常为0644(可读可写)或0444(只读)。0表示不在sysfs中暴露。
关键点:参数变量必须在模块全局作用域中声明,且不能是局部变量或动态分配的内存指针(除非使用module_param_string等变体)。

3.1.2 参数类型与变体宏

宏 数据类型 说明
module_param(name, int, perm) int 标准整型参数
module_param(name, long, perm) long 长整型参数
module_param(name, charp, perm) char * 字符串指针参数,内核会复制传入的字符串
module_param(name, bool, perm) bool 布尔型参数,接受0/1、y/n、Y/N
module_param_array(name, type, num, perm) 数组 传递数组参数,num为指向数组长度的指针
module_param_string(name, string, len, perm) 固定长度字符串 将参数复制到预分配的字符数组中

3.1.3 完整示例:带参数的可配置模块

以下代码展示了一个使用多种参数类型的字符设备模块框架:

#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>

static int debug_level = 0;
static char *device_name = "my_device";
static int buffer_size = 1024;
static int irq_numbers[4];
static int irq_count = 0;

module_param(debug_level, int, 0644);
module_param(device_name, charp, 0444);
module_param(buffer_size, int, 0644);
module_param_array(irq_numbers, int, &irq_count, 0444);

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");
MODULE_PARM_DESC(irq_numbers, "Array of IRQ numbers");

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 "  buffer_size = %d\n", buffer_size);
    printk(KERN_INFO "  irq_count = %d\n", irq_count);
    for (i = 0; i < irq_count; i++)
        printk(KERN_INFO "  irq[%d] = %d\n", i, irq_numbers[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 demonstration");
加载示例:
sudo insmod param_demo.ko debug_level=2 device_name="test_dev" buffer_size=4096 irq_numbers=10,20,30

3.1.4 参数加载流程SVG

下图展示了从用户空间insmod命令到内核模块参数赋值的完整流程:

模块参数加载流程 模块参数加载流程 用户空间 insmod 系统调用层 内核模块加载器 解析 module_param 创建 sysfs 文件 全局变量赋值 调用 module_init() 初始化函数 说明:参数解析在模块加载器阶段完成,初始化函数执行时参数已就绪 sysfs 文件允许运行时通过 echo 命令修改参数(需 perm 包含写权限)

3.2 EXPORT_SYMBOL机制:符号导出与共享

内核模块之间需要共享函数和变量时,必须显式导出符号。EXPORT_SYMBOL及其变体EXPORT_SYMBOL_GPL是内核提供的符号导出宏,它们将符号放入内核符号表,供其他模块或内核核心使用。

3.2.1 导出宏对比

宏 许可证限制 使用场景
EXPORT_SYMBOL(symbol) 无限制 导出给所有模块使用,包括非GPL模块
EXPORT_SYMBOL_GPL(symbol) 仅GPL模块 强制使用者必须声明GPL许可证,保护知识产权
EXPORT_SYMBOL_NS(symbol, ns) 无限制 带命名空间的导出,需使用者显式导入命名空间
EXPORT_SYMBOL_NS_GPL(symbol, ns) 仅GPL模块 带命名空间的GPL限制导出

3.2.2 导出与导入实战

导出模块(exporter.c):

#include <linux/module.h>
#include <linux/init.h>

/* 导出一个简单的加法函数 */
int add_numbers(int a, int b)
{
    return a + b;
}
EXPORT_SYMBOL(add_numbers);

/* 导出一个带GPL限制的函数 */
int multiply_numbers(int a, int b)
{
    return a * b;
}
EXPORT_SYMBOL_GPL(multiply_numbers);

/* 导出一个全局变量 */
int shared_counter = 0;
EXPORT_SYMBOL_GPL(shared_counter);

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 module");

导入模块(importer.c):

#include <linux/module.h>
#include <linux/init.h>

/* 声明外部符号 */
extern int add_numbers(int a, int b);
extern int multiply_numbers(int a, int b);
extern int shared_counter;

static int __init importer_init(void)
{
    int sum, product;

    sum = add_numbers(10, 20);
    product = multiply_numbers(5, 6);

    printk(KERN_INFO "Importer: sum = %d, product = %d\n", sum, product);
    printk(KERN_INFO "Importer: shared_counter = %d\n", shared_counter);

    /* 修改导出的全局变量 */
    shared_counter += 100;
    printk(KERN_INFO "Importer: shared_counter after increment = %d\n", shared_counter);

    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 module");
注意事项:
  • 导入模块必须在导出模块之后加载,否则符号解析失败。
  • 使用EXPORT_SYMBOL_GPL导出的符号,导入模块必须声明MODULE_LICENSE("GPL"),否则加载时内核会拒绝解析。
  • 符号导出后,其他模块可以直接调用,但需注意同步问题(如全局变量的并发访问)。

3.2.3 符号导出与解析流程SVG

下图展示了符号从导出到被其他模块解析使用的完整生命周期:

符号导出与解析流程 符号导出与解析流程 导出模块 (exporter.ko) 定义函数/变量 EXPORT_SYMBOL 写入内核符号表 内核符号表 /proc/kallsyms 导入模块 (importer.ko) extern 声明符号 模块加载器解析 查找符号表匹配 许可证兼容性检查 GPL vs 非GPL 成功:符号地址填入导入模块 失败:模块加载被拒绝,dmesg 显示错误 提示:使用 cat /proc/kallsyms | grep symbol_name 查看符号是否成功导出 导出模块必须先加载,导入模块后加载;依赖关系可通过 depmod 自动管理

3.3 模块依赖关系:构建可复用的模块体系

当模块A导出的符号被模块B使用时,模块B就依赖于模块A。内核提供了完整的依赖管理机制,确保模块按正确顺序加载和卸载。

3.3.1 依赖关系的产生与声明

依赖关系通过以下方式产生和声明:

  • 符号依赖:模块B使用EXPORT_SYMBOL导出的符号,自动产生对导出模块的依赖。
  • 显式依赖:使用MODULE_SOFTDEP或MODULE_DEPEND宏声明软依赖或硬依赖。
  • 设备依赖:通过设备树或ACPI表建立的设备级依赖。

3.3.2 依赖管理工具与文件

工具/文件 作用 使用方式
depmod 生成模块依赖关系文件 sudo depmod -a
/lib/modules/$(uname -r)/modules.dep 存储模块依赖关系 由depmod自动生成
modprobe 智能加载模块及其依赖 sudo modprobe importer
lsmod 查看当前加载的模块及依赖 lsmod 查看 Used by 列
modinfo 查看模块信息,包括依赖 modinfo importer.ko

3.3.3 依赖关系示例与加载顺序

假设有以下三个模块:

  • core.ko:导出基础函数core_init()和core_cleanup()
  • mid.ko:依赖core.ko,导出mid_process()
  • app.ko:依赖mid.ko(间接依赖core.ko)

正确的加载顺序为:core.ko → mid.ko → app.ko
卸载顺序相反:app.ko → mid.ko → core.ko

使用modprobe简化操作:
sudo modprobe app 会自动加载 core、mid 和 app 三个模块,并按依赖顺序加载。

3.3.4 模块依赖关系图SVG

下图展示了一个典型的多层模块依赖关系:

模块依赖关系图 模块依赖关系图 app.ko (应用模块) 使用 mid_process() mid.ko (中间层模块) 导出 mid_process(),依赖 core_init() core.ko (核心模块) 导出 core_init() 和 core_cleanup() 加载顺序:③ 加载顺序:② 加载顺序:① 箭头方向表示“被依赖”,即箭头指向的模块必须先加载

3.4 综合实战:构建带参数和符号导出的模块对

本节将前面所有知识点整合为一个完整的实战案例:一个可配置的日志模块(log_core)导出日志接口,另一个业务模块(business)使用这些接口。

3.4.1 log_core模块:参数配置 + 符号导出

/* log_core.c */
#include <linux/module.h>
#include <linux/moduleparam.h>
#include <linux/init.h>

static int log_level = 3;
static char *log_prefix = "LOG";

module_param(log_level, int, 0644);
module_param(log_prefix, charp, 0444);

MODULE_PARM_DESC(log_level, "Log level (0-5)");
MODULE_PARM_DESC(log_prefix, "Log prefix string");

void log_message(const char *module_name, const char *msg)
{
    if (log_level >= 3)
        printk(KERN_INFO "[%s][%s] %s\n", log_prefix, module_name, msg);
}
EXPORT_SYMBOL_GPL(log_message);

int get_log_level(void)
{
    return log_level;
}
EXPORT_SYMBOL_GPL(get_log_level);

static int __init log_core_init(void)
{
    printk(KERN_INFO "Log core module loaded (level=%d, prefix=%s)\n",
           log_level, log_prefix);
    return 0;
}

static void __exit log_core_exit(void)
{
    printk(KERN_INFO "Log core module unloaded.\n");
}

module_init(log_core_init);
module_exit(log_core_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Core logging module with parameters");

3.4.2 business模块:使用导出符号

/* business.c */
#include <linux/module.h>
#include <linux/init.h>

extern void log_message(const char *module_name, const char *msg);
extern int get_log_level(void);

static int __init business_init(void)
{
    int level;

    level = get_log_level();
    log_message("business", "Module initializing...");
    log_message("business", "Current log level is %d", level);

    if (level >= 4)
        log_message("business", "Verbose logging enabled");

    return 0;
}

static void __exit business_exit(void)
{
    log_message("business", "Module exiting...");
}

module_init(business_init);
module_exit(business_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Business module using exported symbols");
MODULE_SOFTDEP("pre: log_core");  /* 声明软依赖 */
加载与测试:
sudo insmod log_core.ko log_level=4 log_prefix="MYLOG"
sudo insmod business.ko
dmesg | tail -10 查看输出
lsmod | grep -E "log_core|business" 查看模块状态

3.5 本章小结

知识点 核心要点 实践建议
module_param 运行时配置模块行为,支持多种数据类型 为每个参数添加MODULE_PARM_DESC描述
EXPORT_SYMBOL 将符号暴露给其他模块,GPL变体保护许可证 优先使用EXPORT_SYMBOL_GPL保护知识产权 ← 上一章 📖 返回目录 下一章 →