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

在嵌入式Linux驱动开发中,模块化设计不仅要求代码能够动态加载和卸载,还要求模块之间能够协同工作、传递配置参数。本章聚焦于驱动模块的“对外接口”:模块参数允许用户在加载时动态调整驱动行为;符号导出使得一个模块的函数或变量可以被其他模块调用;而模块依赖关系则确保内核在加载和卸载时能正确处理模块间的引用链。

学习目标:掌握 module_param 系列宏的用法,理解 EXPORT_SYMBOL 的作用域与导出规则,能够通过符号依赖分析模块间的加载顺序。

3.1 模块参数:module_param 宏的使用

模块参数允许驱动开发者在 insmod 或 modprobe 时向模块内部传递配置值,而无需重新编译内核或驱动。这对于调试开关、缓冲区大小、设备模式等运行时可调参数非常有用。

3.1.1 基本宏与数据类型

内核提供了 module_param 和 module_param_named 等宏,定义在 <linux/moduleparam.h> 中。其基本语法为:

module_param(name, type, perm);

其中 name 是变量名,type 是参数类型,perm 是 sysfs 文件权限(0 表示不创建 sysfs 条目)。

类型标识 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, y/n, Y/N)
invbool bool 反转布尔值

3.1.2 数组参数与描述

对于数组类型参数,使用 module_param_array:

module_param_array(name, type, nump, perm);

其中 nump 是一个指向 unsigned int 的指针,用于接收实际传入的元素个数。

内核还支持 MODULE_PARM_DESC 宏为参数添加描述信息,这些信息可通过 modinfo 命令查看:

static int debug_level = 0;
module_param(debug_level, int, 0644);
MODULE_PARM_DESC(debug_level, "Debug level (0=off, 1=info, 2=verbose)");

static char *mode = "normal";
module_param(mode, charp, 0);
MODULE_PARM_DESC(mode, "Operation mode: normal|fast|safe");

3.1.3 加载时传参示例

用户空间加载模块时传递参数:

# insmod my_driver.ko debug_level=2 mode="fast"

通过 modprobe 加载时,参数可以写在 /etc/modprobe.d/ 配置文件中:

options my_driver debug_level=1 mode=normal
权限位说明: perm 参数使用标准的 Linux 文件权限八进制表示。设置为 0 表示不在 /sys/module/ 下创建对应的参数文件;设置为 0444 表示只读;0644 表示 root 可写、其他人只读。运行时可通过 sysfs 修改参数值(如果权限允许)。
模块参数传递流程 用户空间 insmod / modprobe 内核参数解析层 parse_args() 模块内部变量 debug_level / mode 参数串 赋值 sysfs 接口 /sys/module/.../parameters/ perm != 0 时创建 运行时修改 echo 2 > .../debug_level 说明: 模块参数支持在加载时通过命令行传递,也可在运行时通过 sysfs 接口动态调整(需 perm 非零且具有写权限)。

3.2 EXPORT_SYMBOL 机制

内核模块之间需要共享函数或全局变量时,必须显式导出符号。Linux 内核提供了 EXPORT_SYMBOL 和 EXPORT_SYMBOL_GPL 两个宏,用于将符号(函数名或变量名)加入到内核符号表中。

3.2.1 导出宏的对比

宏 导出范围 使用限制 典型场景
EXPORT_SYMBOL(name) 所有模块(包括非GPL模块) 无 通用API、硬件抽象层
EXPORT_SYMBOL_GPL(name) 仅GPL兼容模块 非GPL模块无法引用 核心子系统、专有接口保护

3.2.2 导出与引用示例

导出模块(exporter.c):

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

int my_add(int a, int b)
{
    return a + b;
}
EXPORT_SYMBOL(my_add);

int my_mul(int a, int b)
{
    return a * b;
}
EXPORT_SYMBOL_GPL(my_mul);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Driver Expert");

引用模块(importer.c):

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

extern int my_add(int a, int b);
extern int my_mul(int a, int b);

static int __init importer_init(void)
{
    pr_info("my_add(3, 4) = %d\n", my_add(3, 4));
    pr_info("my_mul(3, 4) = %d\n", my_mul(3, 4));
    return 0;
}

static void __exit importer_exit(void)
{
    pr_info("importer module unloaded\n");
}

module_init(importer_init);
module_exit(importer_exit);

MODULE_LICENSE("GPL");
注意事项:
  • 导出符号的模块必须先于引用模块加载,否则引用模块在加载时会出现 Unknown symbol 错误。
  • 使用 EXPORT_SYMBOL_GPL 导出的符号,如果引用模块未声明 MODULE_LICENSE("GPL"),内核将拒绝加载该引用模块。
  • 符号名在编译时会被内核附加 __ksymtab_ 前缀,可通过 /proc/kallsyms 查看所有导出的符号。

3.2.3 查看导出符号

加载模块后,可以通过以下方式验证符号是否成功导出:

# cat /proc/kallsyms | grep my_add
# nm exporter.ko | grep my_add

在编译引用模块时,需要确保能够找到导出符号的声明。通常通过头文件或 extern 声明来实现。

EXPORT_SYMBOL 机制:符号导出与引用关系 导出模块 (exporter.ko) int my_add(int a, int b) EXPORT_SYMBOL(my_add); int my_mul(int a, int b) EXPORT_SYMBOL_GPL(my_mul); 内核符号表 __ksymtab_my_add [GPL-only: No] __ksymtab_my_mul [GPL-only: Yes] 引用模块 (importer.ko) extern int my_add(...); extern int my_mul(...); MODULE_LICENSE("GPL"); // 可引用 my_add 和 my_mul 导出 引用 加载顺序约束: 必须先加载 exporter.ko,再加载 importer.ko。卸载时顺序相反:先卸载 importer.ko,再卸载 exporter.ko。 验证命令: # cat /proc/kallsyms | grep -E "my_add|my_mul" # nm exporter.ko | grep my_

3.3 模块依赖关系

当模块 A 导出的符号被模块 B 引用时,模块 B 就依赖于模块 A。内核通过 模块引用计数 和 符号依赖链 来管理这种关系。

3.3.1 依赖关系的自动生成

使用 modprobe 加载模块时,它会自动解析 /lib/modules/$(uname -r)/modules.dep 文件,按依赖顺序加载所有需要的模块。该文件由 depmod 命令生成:

# depmod -a
# cat /lib/modules/5.10.0/modules.dep | grep importer
kernel/drivers/misc/importer.ko: kernel/drivers/misc/exporter.ko

3.3.2 手动管理依赖

如果不使用 modprobe,手动加载时必须遵循依赖顺序:

# insmod exporter.ko
# insmod importer.ko

卸载时顺序相反:

# rmmod importer
# rmmod exporter

如果尝试先卸载 exporter,内核会提示 Module is in use:

# rmmod exporter
rmmod: ERROR: Module exporter is in use by: importer

3.3.3 查看模块依赖

使用 lsmod 可以查看当前加载模块的引用计数和依赖模块列表:

# lsmod
Module                  Size  Used by
importer               16384  0
exporter               16384  1 importer

其中 Used by 列显示引用该模块的其他模块名称。数字表示引用计数。

命令/文件 作用 示例输出
lsmod 查看当前加载的模块及引用计数 exporter 16384 1 importer
modinfo -F depends exporter.ko 查看模块的依赖列表 depends: (空表示无依赖)
cat /proc/kallsyms 查看所有内核符号(包括导出的) ffffffffc0001234 T my_add
nm exporter.ko 查看模块目标文件中的符号 0000000000000000 T my_add
关键概念: 模块依赖关系本质上是一个有向无环图(DAG)。内核在加载模块时会进行符号解析,如果发现未解析的符号,加载会失败。使用 modprobe 可以自动处理依赖链,推荐在开发和生产环境中使用。
模块依赖关系:有向无环图(DAG) core_driver.ko 基础硬件操作 mid_layer.ko 协议封装 platform_if.ko 平台接口适配 app_module.ko test_module.ko 依赖 依赖 依赖 依赖 加载顺序: core → mid_layer → app_module

3.4 综合实践:构建带参数和符号导出的驱动模块

下面是一个完整的示例,展示如何在一个模块中同时使用模块参数和符号导出,并在另一个模块中引用这些符号。

文件:calc_ops.c(导出模块)

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

static int op_debug = 0;
module_param(op_debug, int, 0644);
MODULE_PARM_DESC(op_debug, "Enable debug output (0/1)");

int calc_add(int x, int y)
{
    if (op_debug)
        pr_info("calc_add: %d + %d = %d\n", x, y, x + y);
    return x + y;
}
EXPORT_SYMBOL(calc_add);

int calc_sub(int x, int y)
{
    if (op_debug)
        pr_info("calc_sub: %d - %d = %d\n", x, y, x - y);
    return x - y;
}
EXPORT_SYMBOL_GPL(calc_sub);

static int __init calc_init(void)
{
    pr_info("calc_ops module loaded (debug=%d)\n", op_debug);
    return 0;
}

static void __exit calc_exit(void)
{
    pr_info("calc_ops module unloaded\n");
}

module_init(calc_init);
module_exit(calc_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Expert");
MODULE_DESCRIPTION("Basic arithmetic operations with module parameters");

文件:calc_user.c(引用模块)

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

extern int calc_add(int x, int y);
extern int calc_sub(int x, int y);

static int __init user_init(void)
{
    int a = 10, b = 5;

    pr_info("calc_user module loaded\n");
    pr_info("calc_add(%d, %d) = %d\n", a, b, calc_add(a, b));
    pr_info("calc_sub(%d, %d) = %d\n", a, b, calc_sub(a, b));

    return 0;
}

static void __exit user_exit(void)
{
    pr_info("calc_user module unloaded\n");
}

module_init(user_init);
module_exit(user_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("Embedded Linux Expert");
MODULE_DESCRIPTION("User of calc_ops module");

Makefile 片段:

obj-m += calc_ops.o
obj-m += calc_user.o

all:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) modules

clean:
    make -C /lib/modules/$(shell uname -r)/build M=$(PWD) clean

加载与测试:

# insmod calc_ops.ko op_debug=1
# insmod calc_user.ko
# dmesg | tail -5
[ 1234.5678] calc_ops module loaded (debug=1)
[ 1234.5679] calc_user module loaded
[ 1234.5680] calc_add(10, 5) = 15
[ 1234.5681] calc_sub(10, 5) = 5

# rmmod calc_user
# rmmod calc_ops
开发建议:
  • 在开发阶段,将 op_debug 参数默认设为 1,方便调试;发布时再改为 0。
  • 对于核心API,优先使用 EXPORT_SYMBOL_GPL 以保护知识产权。
  • 使用 modprobe -r 可以自动处理依赖模块的卸载顺序。
  • 在模块的 MODULE_DESCRIPTION 中清晰描述其导出的符号和参数,方便其他开发者使用。

3.5 本章小结

知识点 核心要点 常用命令/宏
模块参数 允许加载时或运行时传递配置值 module_param, module_param_array, MODULE_PARM_DESC
符号导出 使模块的函数/变量可被其他模块引用 EXPORT_SYMBOL, EXPORT_SYMBOL_GPL
模块依赖 引用导出符号的模块自动形成依赖关系 lsmod, depmod, modprobe
加载顺序 被依赖模块必须先加载,后卸载 insmod, rmmod