Linux内核与驱动:3.驱动模块传参,内核模块符号导出

内核模块传参允许我们在加载驱动(insmod)时,动态地改变驱动的行为,而不需要重新编译代码。这在调试硬件(比如修改 I2C 地址、串口波特率或调试等级)时非常高效。

为什么需要传参?

在开发 RK3568 或其他嵌入式驱动时,我们经常遇到这种场景:

  • 动态调试:通过参数开启或关闭printk日志打印。

  • 硬件适配:同一个驱动,通过参数指定它控制的是哪一个 GPIO 引脚。

  • 灵活性:无需修改代码、无需重新编译内核,只需在加载时传入不同的值。

1.内核模块传参基本知识

1.1核心宏定义

Linux内核提供了三种内核传参的宏定义,分别是module_param(name, type, perm)、module_param_array(name, type, nu mp, perm)宏和module_param_string(name, string, len, perm)宏,分别进行基本类型、数组和字 符串参数的传递。它们定义在"内核源码/include/linux/moduleparam.h"文件中。

module_param(name, type, perm)

  • name: 变量名。

  • type: 参数类型(如int,bool,char等)。

  • perm: 访问权限(一般设为444)。

访问权限表如下所示

宏定义 八进制值 权限描述 实际用途
S_IRUGO 0444 用户、组、其他用户均只读 最常用的权限,允许任何人查看参数值。
S_IWUSR 0200 文件所有者(root)可写 允许在运行时动态修改参数。
S_IRUSR 0400 文件所有者(root)只读 仅限 root 查看,安全性更高。
**`S_IRUGO S_IWUSR`** 0644 root 可读写,组和其他用户只读
0 0000 无文件对应 参数不会在 /sys 下显示,仅能通过加载时传参。

1.2内核模块传参基本原理

Linux 内核提供了专门的模块参数机制。开发者只需要:

  1. 定义模块参数变量
  2. 使用module_param或module_param_arry等宏导出参数
  3. 指定参数类型和访问权限
  4. 在加载模块时通过insmod传入参数值

模块加载时,内核会自动解析这些参数,并在驱动初始化函数执行之前完成赋值。因此,在 hello_init() 中就可以直接读取这些参数。

2.内核模块传参实验

编写完成的parameter.c代码如下:

cpp 复制代码
#include <linux/init.h>
#include <linux/module.h>
#include <linux/kernel.h>

static int num = 10;
static char *name = "default";
static int values[3] = {1, 2, 3};
static int count = 0;

module_param(num, int, 0644);
MODULE_PARM_DESC(num, "An integer parameter");

module_param(name, charp, 0644);
MODULE_PARM_DESC(name, "A string parameter");

module_param_array(values, int, &count, 0644);
MODULE_PARM_DESC(values, "An integer array parameter");
static int __init hello_init(void)
{
    int i;

    printk(KERN_INFO "HelloWorld driver init");
    printk(KERN_INFO "num = %d", num);
    printk(KERN_INFO "name = %s", name);
    printk(KERN_INFO "values count = %d", count);

    for (i = 0; i < count; i++) {
        printk(KERN_INFO "values[%d] = %d", i, values[i]);
    }

    return 0;
}

static void __exit hello_exit(void)
{
    printk(KERN_INFO "HelloWorld driver exit");
}

module_init(hello_init);
module_exit(hello_exit);
MODULE_LICENSE("GPL");
MODULE_AUTHOR("your_name");
MODULE_DESCRIPTION("HelloWorld driver with module parameters");
MODULE_VERSION("1.0");

后续常规创建Makefile文件,将其编译成.ko文件。

然后insmod加载到开发板中(传入参数):

bash 复制代码
insmod hello.ko num=100 name=board values=7,8,9

注:数组传参是用逗号隔开。

3.内核模块符号导出

在 Linux 内核中,每个模块都是独立编译的。默认情况下,模块内的函数和变量仅对自身可见(类似 C 语言中的static)。 符号导出机制允许我们将特定的函数或变量放入"全局内核符号表 "(或叫公共内核符号表)中,使得其他内核模块可以发现并调用它们。

内核模块符号导出(Symbol Exporting)是实现驱动模块化和层叠驱动(Stacked Drivers)的核心技术。在开发复杂的项目时,如果你希望一个模块调用另一个模块中定义的函数,就必须使用符号导出。

3.1核心宏介绍

内核提供了两个主要的宏来导出符号:

宏名称 权限说明
EXPORT_SYMBOL(res) 对所有内核模块可见,没有任何版本限制。
EXPORT_SYMBOL_GPL(res) 仅对声明了 MODULE_LICENSE("GPL") 的模块可见。这是内核开发者推荐的做法。

3.2内核模块符号导出实验

假设我们需要实现两个模块:模块 A (提供者) 定义一个加法函数,模块 B (使用者) 调用这个函数。

模块A(module_a.c):

cpp 复制代码
#include <linux/init.h>
#include <linux/module.h>

// 定义一个要导出的函数
int my_add_function(int a, int b) {
    return a + b;
}

// 关键步骤:导出符号
EXPORT_SYMBOL(my_add_function);

static int module_a_init(void) {
    printk("Module A: Exported my_add_function\n");
    return 0;
}

static void module_a_exit(void) {
    printk("Module A: Exit\n");
}

module_init(module_a_init);
module_exit(module_a_exit);
MODULE_LICENSE("GPL");

模块B(module_b.c):

cpp 复制代码
#include <linux/init.h>
#include <linux/module.h>

// 1. 声明外部符号
extern int my_add_function(int a, int b);

static int module_b_init(void) {
    int result = my_add_function(10, 20); // 2. 调用函数
    printk("Module B: Result from Module A is %d\n", result);
    return 0;
}

static void module_b_exit(void) {
    printk("Module B: Exit\n");
}

module_init(module_b_init);
module_exit(module_b_exit);
MODULE_LICENSE("GPL");

--------一定要 extern 声明外部符号---------

由于我们的模块B是依赖于模块A 的函数的,所以在编译的时候我们只能先编译模块A,编译模块A后会出现一个Module.symvers文件,这就相当于是我们之前提到的公共内核符号表,我们需要将Module.symvers文件复制到模块B源码目录下,然后编译模块B。

运行与验证

在 RK3568 开发板上加载模块时,顺序非常重要:

  1. 先加载提供者insmod module_a.ko

  2. 后加载使用者insmod module_b.ko

卸载顺序相反,先卸载模块B,再卸载模块A。

Q&A

  • Q: 如果模块 A 没加载,直接加载模块 B 会怎样?

    • A: 会报错 Unknown symbol in module,加载失败。这就是模块依赖。
  • Q: 为什么推荐使用 EXPORT_SYMBOL_GPL

    • A: 为了强制使用者遵循 GPL 协议,保护开源社区的成果,同时避免一些闭源驱动绕过内核限制。
相关推荐
程序猿编码2 小时前
网络数据包环形缓存捕获技术:原理、设计与实现(C/C++代码实现)
linux·c语言·网络·tcp/ip·缓存
默|笙2 小时前
【Linux】进程信号(4)_信号捕捉_内核态与用户态
linux·运维·服务器
supersolon2 小时前
PVE9安装32位爱快路由(ikuai)
linux·运维·网络
123过去2 小时前
mfterm使用教程
linux·网络·测试工具·安全
深圳市恒讯科技2 小时前
OpenClaw 2026安全指南
运维·服务器·安全
海兰2 小时前
使用 TypeScript 创建 Elasticsearch MCP 服务器
服务器·elasticsearch·typescript·mcp
学编程的小程2 小时前
我的极空间 NAS 进阶玩法:开启 SSH,解锁私有云服务器新体验
运维·服务器·ssh
123过去2 小时前
nfc-mfclassic使用教程
linux·网络·测试工具·安全
深念Y2 小时前
飞牛OS部署MCSM搭建MC服务器完整教程
运维·服务器·jdk·端口·nas·mc·飞牛os