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

模块写好了,怎么跟用户空间交互?模块之间怎么互相调用?这两个问题,说白了就是本章要讲的 模块参数 和 导出符号。

我刚开始做驱动开发时,经常为了调试一个参数反复编译模块,后来才发现有 module_param 这种好东西。至于模块间依赖,嗯,踩过的坑也不少,咱们一个一个说。

4.1 模块参数:让用户空间控制内核模块

模块参数允许你在加载模块时传递参数,或者在 /sys/module/<name>/parameters/ 下动态修改。说白了,就是给内核模块开了一个可控的“后门”。

4.1.1 基本用法

先看一个最简单的例子:

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

static int debug_level = 0;
static char *device_name = "default_dev";
static int array_param[4] = {0, 1, 2, 3};

module_param(debug_level, int, S_IRUGO | S_IWUSR);
module_param(device_name, charp, S_IRUGO);
module_param_array(array_param, int, NULL, S_IRUGO);

MODULE_PARM_DESC(debug_level, "Debug level (0-3)");
MODULE_PARM_DESC(device_name, "Device name string");
MODULE_PARM_DESC(array_param, "Sample array parameter");

加载时这样传参:

sudo insmod my_module.ko debug_level=2 device_name="mydev"

我个人习惯把调试开关都做成模块参数,这样在开发阶段不用反复编译。你想想看,每次改个打印级别都要重新编译,多浪费时间。

4.1.2 参数权限与类型

类型宏 C类型 说明
module_param(name, byte, perm) unsigned char 字节类型
module_param(name, int, perm) int 整型,最常用
module_param(name, uint, perm) unsigned int 无符号整型
module_param(name, long, perm) long 长整型
module_param(name, charp, perm) char * 字符串指针
module_param(name, bool, perm) bool 布尔值,支持0/1或y/n
module_param_array(name, type, num, perm) 数组 数组参数,num返回元素个数

权限位 perm 使用 <linux/stat.h> 中的宏:

  • S_IRUGO — 所有人可读
  • S_IWUSR — 所有者可写
  • S_IRUSR | S_IWUSR — 所有者读写
  • 0 — 不在sysfs中显示
注意: 我曾经在生产环境遇到过一个问题——把调试参数权限设成了 S_IWUGO,结果用户空间随便一个进程就能改掉内核模块的行为。权限一定要收窄,生产环境建议只给 S_IRUGO 甚至 0。

4.2 EXPORT_SYMBOL:模块间的“API接口”

模块A想调用模块B里的函数,怎么办?这就用到了 EXPORT_SYMBOL。它把内核符号导出到全局符号表,其他模块就能通过 extern 声明来调用。

4.2.1 导出与导入

先看导出方(模块B):

// module_b.c
#include <linux/module.h>

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

int my_sub(int a, int b)
{
    return a - b;
}
EXPORT_SYMBOL_GPL(my_sub);  // 只允许GPL模块使用

再看导入方(模块A):

// module_a.c
#include <linux/module.h>

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

static int __init a_init(void)
{
    printk("3 + 5 = %d\n", my_add(3, 5));
    printk("10 - 4 = %d\n", my_sub(10, 4));
    return 0;
}

4.2.2 EXPORT_SYMBOL vs EXPORT_SYMBOL_GPL

宏 许可证要求 使用场景
EXPORT_SYMBOL 无限制 通用接口,商业模块也可调用
EXPORT_SYMBOL_GPL 必须GPL 核心接口,保护GPL代码
EXPORT_SYMBOL_GPL_FUTURE 当前无限制,未来可能变GPL 过渡期使用,很少见
我的建议: 如果你写的是开源驱动,尽量用 EXPORT_SYMBOL_GPL。这不仅是许可证合规问题,更是对开源社区的一种尊重。我在给某个商业公司做BSP时,他们要求所有导出符号都用 EXPORT_SYMBOL,结果后来被社区吐槽了很久。

4.3 模块间依赖关系

模块A依赖模块B,意味着加载A之前必须先加载B,卸载时则相反。这种依赖关系由内核自动维护,但你需要正确声明。

4.3.1 依赖声明

在模块A中,使用 MODULE_SOFTDEP 或 MODULE_DEPEND 声明依赖:

// module_a.c
MODULE_SOFTDEP("pre: module_b");  // 软依赖,建议先加载module_b
MODULE_LICENSE("GPL");

更常见的做法是,只要模块A调用了模块B导出的符号,内核在加载A时会自动检查B是否已加载。如果B没加载,insmod 会失败并提示 Unknown symbol。

4.3.2 查看依赖关系

用 lsmod 可以查看模块间的依赖:

$ lsmod
Module                  Size  Used by
module_a               16384  0
module_b               20480  1 module_a

注意 Used by 列,module_b 显示 1 module_a,说明有一个模块在使用它。

用 modinfo 也能看到依赖信息:

$ modinfo module_a.ko
filename:       module_a.ko
depends:        module_b
...

4.3.3 依赖链与循环依赖

内核不允许循环依赖。比如模块A依赖B,B依赖C,C又依赖A——这种链在加载时会直接报错。

避坑指南: 我曾经设计过一个电源管理框架,模块A负责核心调度,模块B和C分别管理不同外设。结果B和C都调用了A的函数,A又调用了B的某个回调——形成了隐式循环依赖。查了两天才发现,最后通过把回调注册改成通知链才解决。记住:模块依赖必须是DAG(有向无环图)。

4.4 知识结构总览

下面这张图把本章的核心逻辑串起来了:

模块参数与导出符号知识结构 模块参数 (module_param) 加载时传参 sysfs动态修改 权限控制 类型检查 导出符号 (EXPORT_SYMBOL) EXPORT_SYMBOL EXPORT_SYMBOL_GPL 符号表注册 许可证检查 模块间依赖关系 自动依赖检测 MODULE_SOFTDEP 循环依赖检测 核心要点 模块参数 = 用户空间控制内核模块的接口 导出符号 = 模块间函数调用的桥梁,依赖关系自动维护

4.5 实战经验总结

最后分享几个我在项目中积累的经验:

  • 参数命名规范: 我习惯用 模块名_参数名 的格式,比如 usb_debug_level,避免全局命名冲突。
  • 导出符号最小化: 只导出必要的接口,不要图省事把所有函数都 EXPORT_SYMBOL。内核符号表越臃肿,维护成本越高。
  • 依赖关系可视化: 用 lsmod 和 modinfo 检查依赖链,复杂项目建议画个依赖图。
  • 调试技巧: 如果遇到 Unknown symbol 错误,先 cat /proc/kallsyms | grep 符号名 看看符号是否真的被导出了。

一句话总结: module_param 让模块可配置,EXPORT_SYMBOL 让模块可协作,依赖关系让模块可组合。这三者构成了内核模块生态的基础设施。