内核模块传参允许我们在加载驱动(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 内核提供了专门的模块参数机制。开发者只需要:
- 定义模块参数变量
- 使用module_param或module_param_arry等宏导出参数
- 指定参数类型和访问权限
- 在加载模块时通过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 开发板上加载模块时,顺序非常重要:
-
先加载提供者 :
insmod module_a.ko -
后加载使用者 :
insmod module_b.ko
卸载顺序相反,先卸载模块B,再卸载模块A。
Q&A
-
Q: 如果模块 A 没加载,直接加载模块 B 会怎样?
- A: 会报错
Unknown symbol in module,加载失败。这就是模块依赖。
- A: 会报错
-
Q: 为什么推荐使用
EXPORT_SYMBOL_GPL?- A: 为了强制使用者遵循 GPL 协议,保护开源社区的成果,同时避免一些闭源驱动绕过内核限制。