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 协议,保护开源社区的成果,同时避免一些闭源驱动绕过内核限制。
相关推荐
三品吉他手会点灯42 分钟前
STM32 VSCode 开发-C/C++的环境配置中,找不到C/C++: Edit Configurations选项
c语言·c++·vscode·stm32·单片机·嵌入式硬件·编辑器
一生了无挂1 小时前
自己编译RustDesk,并将自建ID服务器和key信息写入客户端
运维·服务器
6Hzlia2 小时前
【Hot 100 刷题计划】 LeetCode 287. 寻找重复数 | C++ 数组判环 (快慢指针终极解法)
c++·算法·leetcode
feng_you_ying_li2 小时前
linux之运行状态(2),内核链表与进程状态
linux
yngsqq2 小时前
编译的dll自动复制到指定目录并重命名
java·服务器·前端
聊点儿技术2 小时前
IP风险等级评估在保险承保中的三个核心应用场景——从投保核验到持续监控
服务器·金融·ip·保险·ip风险评估·ip风险等级·风险评估api
图图玩ai3 小时前
SSH 命令管理工具怎么选?从命令收藏到批量执行一次讲清
linux·nginx·docker·ai·程序员·ssh·可视化·gmssh·批量命令执行
Robot_Nav3 小时前
DPMPC-Planner:复杂静态环境与动态障碍物下的无人机实时轨迹规划框架
c++·无人机·mpc
似水এ᭄往昔3 小时前
【Linux】--基础IO
linux·服务器
桌面运维家3 小时前
IDV云桌面vDisk机房课表联动部署方案
大数据·服务器·数据库