深入理解 Linux 驱动开发:驱动传参与符号导出

95 阅读11分钟

在 Linux 驱动开发的领域中,驱动传参和驱动符号导出是两个至关重要的机制,它们为驱动程序的灵活性、可扩展性和模块间协作提供了强大的支持。接下来,我们将深入且详细地剖析这两个概念。

一、驱动传参

(一)驱动传参的作用

驱动传参是一种在加载驱动模块时向其传递参数的机制,这一特性极大地增强了驱动程序的适应性和可配置性。想象一下,在实际的嵌入式项目中,可能存在多种不同型号的传感器设备,它们的工作参数各不相同。以串口驱动为例,不同的串口设备可能需要不同的波特率(如 9600、115200 等)、数据位(5 位、6 位、7 位或 8 位)、校验位(奇校验、偶校验、无校验)和停止位(1 位、2 位)来进行数据通信 。通过驱动传参,我们无需为每一种配置单独编写驱动代码,而是可以在加载驱动模块时传递相应的参数,使同一驱动程序能够适配多种硬件配置。 在调试阶段,驱动传参同样发挥着重要作用。开发人员可以通过改变参数值来观察驱动的运行状态,而无需重新编译驱动代码。这大大缩短了调试周期,提高了开发效率。例如,在调试网络驱动时,可以通过传递不同的缓冲区大小参数,来测试驱动在不同数据流量下的性能表现,快速定位问题所在。

(二)驱动传参的实现方式

Linux 内核提供了丰富的宏定义来实现驱动传参功能,这些宏定义在/include/linux/moduleparam.h文件中。下面我们详细介绍几种常用的宏及其使用方法: module_param(name, type, perm) module_param宏用于定义单个参数。其中: name:是参数的名称,在加载驱动模块时,我们通过这个名称来指定参数的值。例如,我们定义一个名为baud_rate的参数,用于设置串口的波特率。 type:指定参数的数据类型,支持int、charp(字符指针)、bool等多种基本数据类型。例如,如果要传递一个整数值作为波特率,就可以将type设置为int。 perm:用于设置该参数在sysfs文件系统中对应文件节点的属性权限。权限的定义参考/include/linux/stat.h文件。常见的权限设置如0666,表示所有者、所属组和其他用户都具有读写权限;0644表示所有者具有读写权限,所属组和其他用户只有读权限。 // 定义一个名为baud_rate的整数型参数,权限设置为0666

static int baud_rate;
module_param(baud_rate, int, 0666);

在加载驱动模块时,可以使用以下命令传递参数:

insmod my_driver.ko baud_rate=115200

这样,驱动模块中的baud_rate变量就会被赋值为115200。

module_param_array(name, type, nump, perm)

当需要传递一组参数时,可以使用module_param_array宏。该宏的参数含义如下: name:参数数组的名称。 type:数组元素的数据类型。 nump:是一个指针,指向一个整数变量,用于记录实际传递的参数个数。 perm:同样是设置文件节点的权限。 // 定义一个整数型数组参数my_array,用于存储多个数据

static int my_array[10];

// 用于记录实际传入参数个数的变量

static int array_size;
module_param_array(my_array, int, &array_size, 0666);

加载驱动时传递参数:

insmod my_driver.ko my_array=1,2,3,4

此时,驱动模块中的my_array数组将依次存储1、2、3、4这四个值,array_size变量会被设置为4。

module_param_string(name, string, len, perm)

如果需要传递字符串类型的参数,module_param_string宏就能派上用场。各参数含义如下: name:外部传入的参数名。 string:指向一个字符数组的指针,用于接收传递进来的字符串。 len:字符数组的长度,用于确保不会发生缓冲区溢出。 perm:文件节点权限。

// 定义一个字符数组用于接收传入的字符串
static char my_string[50];
module_param_string(my_string, my_string, sizeof(my_string), 0666);

加载驱动:

insmod my_driver.ko my_string="Hello, Linux Driver!"

驱动模块中的my_string数组就会存储字符串"Hello, Linux Driver!"。

参考代码

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


static int baud_rate = 0;
module_param(baud_rate, int, 0664);

static int my_array[10];
static int array_size = 0;
module_param_array(my_array, int, &array_size, 0664);

static int hello_init(void)
{
	printk("hello world init baud_rate = %d, array_size = %d \n", baud_rate, array_size);
	int i;
	for(i = 0; i < array_size; i++)
	{
		printk("hello world init array[%d] = %d\n", i, my_array[i]);
	}

	return 0;
}

static void hello_exit(void)
{
	printk("hello world exit 1!!!");
}

module_init(hello_init);
module_exit(hello_exit);

MODULE_LICENSE("GPL");
MODULE_AUTHOR("cmy");
MODULE_VERSION("v1.0");

在这里插入图片描述

(三)驱动传参的注意事项 参数类型匹配:在加载驱动模块时,传递的参数类型必须与驱动中定义的参数类型严格一致。如果类型不匹配,可能会导致驱动加载失败,或者在驱动运行过程中出现内存错误、程序崩溃等严重问题。例如,如果驱动中定义的参数类型为int,而我们在加载时传递了一个字符串,就会引发错误。 权限设置:合理设置参数的权限至关重要。如果权限设置过高,可能会导致未经授权的用户随意修改参数,从而影响系统的稳定性和安全性;权限设置过低,则可能导致合法用户无法正常配置驱动。因此,需要根据实际需求,谨慎设置参数的权限。 参数默认值:在驱动代码中,通常会为参数设置默认值。当加载驱动模块时传递了参数值,该值会覆盖默认值。所以,在设置默认值时,要充分考虑大多数应用场景的需求,确保默认配置能够使驱动正常工作。例如,对于串口驱动的波特率参数,将默认值设置为常用的9600,可以保证在未传递参数时,驱动也能以一个合理的配置运行。

二、驱动符号导出

(一)驱动符号导出的概念

在 Linux 内核庞大的驱动体系中,各个驱动模块通常是相互独立开发和维护的。然而,在复杂的系统中,一个驱动模块可能需要调用另一个驱动模块中定义的函数或访问其变量。驱动符号导出机制应运而生,它允许一个驱动模块将自身内部的函数或变量暴露出来,供其他模块使用。通过这种方式,不同的驱动模块之间可以实现更紧密的协作,共同完成复杂的系统功能。 例如,在一个包含网络驱动和存储驱动的嵌入式系统中,网络驱动可能需要调用存储驱动提供的函数来读取或写入配置文件;存储驱动也可能需要使用网络驱动的某些变量来获取网络状态信息。驱动符号导出使得这种跨模块的交互成为可能,大大增强了系统的扩展性和灵活性。

(二)驱动符号导出的实现方式

Linux 内核提供了EXPORT_SYMBOL和EXPORT_SYMBOL_GPL两个宏来实现驱动符号导出功能,它们定义在/include/linux/export.h文件中。 EXPORT_SYMBOL(sym) EXPORT_SYMBOL宏用于将指定的函数或变量导出到公共内核符号表中,以便其他模块可以使用该符号。其中,sym是要导出的函数或变量的名称。 // 定义一个用于加法运算的函数

int my_exported_function(int a, int b) {
    return a + b;
}

// 将my_exported_function函数导出

EXPORT_SYMBOL(my_exported_function);

在其他驱动模块中,如果要使用my_exported_function函数,就可以通过包含相应的头文件(如果函数声明在头文件中),然后直接调用该函数。 EXPORT_SYMBOL_GPL(sym) EXPORT_SYMBOL_GPL宏的功能与EXPORT_SYMBOL类似,但存在一个重要区别:使用EXPORT_SYMBOL_GPL导出的符号只能被遵循 GPL(General Public License)许可协议的模块使用。在实际开发中,如果没有特殊的许可证要求,大多数情况下使用EXPORT_SYMBOL即可。 // 定义一个全局变量

int my_gpl_exported_variable = 10;

// 将my_gpl_exported_variable变量以GPL许可导出

EXPORT_SYMBOL_GPL(my_gpl_exported_variable);

(三)驱动符号导出的使用步骤

导出符号的模块编译:首先对包含要导出符号的驱动模块进行编译。编译完成后,在该模块的目录下会生成一个名为Module.symvers的文件。这个文件记录了模块中所有导出符号的信息,包括符号名称、版本号等。它是其他模块使用这些导出符号的关键依据。 使用符号的模块编译:将Module.symvers文件拷贝到需要使用导出符号的驱动模块目录下,然后对该模块进行编译。在编译过程中,编译器会根据Module.symvers文件中的信息,找到对应的导出符号。同时,要确保正确包含了导出符号模块的相关头文件(如果有函数声明在头文件中),以便编译器能够识别和解析要使用的符号。 模块加载顺序:在加载驱动模块时,必须严格遵循先加载导出符号的模块,后加载使用这些符号的模块的顺序。这是因为使用符号的模块在加载过程中,需要从内核符号表中查找并解析所依赖的符号。如果先加载使用符号的模块,由于此时导出符号的模块尚未加载,内核符号表中没有相应的符号信息,就会导致模块加载失败,并报错提示找不到所需符号。例如,有模块 A 导出符号,模块 B 使用这些符号,正确的加载顺序为:

insmod moduleA.ko
insmod moduleB.ko

模块卸载顺序:卸载驱动模块时,顺序与加载顺序相反,即先卸载使用符号的模块,再卸载导出符号的模块。这样可以确保系统的稳定性,避免因符号依赖关系混乱而导致系统异常。例如:

rmmod moduleB.ko
rmmod moduleA.ko

参考代码 在这里插入图片描述

symbol_provider.c

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

// 导出的全局变量
int global_counter = 0;
EXPORT_SYMBOL_GPL(global_counter);

// 导出的函数 - 执行两数相加
int add_numbers(int a, int b) {
    printk(KERN_INFO "Adding %d and %d\n", a, b);
    return a + b;
}
EXPORT_SYMBOL_GPL(add_numbers);

static int __init provider_init(void) {
    printk(KERN_INFO "Symbol provider module loaded\n");
    return 0;
}

static void __exit provider_exit(void) {
    printk(KERN_INFO "Symbol provider module unloaded\n");
}

module_init(provider_init);
module_exit(provider_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Symbol Provider Module");
MODULE_AUTHOR("cmy");
    

在这里插入图片描述

symbol_consumer.c

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

// 声明要使用的外部符号
extern int global_counter;
extern int add_numbers(int a, int b);

static int __init consumer_init(void) {
    int result;
    
    printk(KERN_INFO "Symbol consumer module loaded\n");
    
    // 使用导出的函数
    result = add_numbers(5, 3);
    printk(KERN_INFO "Result of add_numbers: %d\n", result);
    
    // 使用导出的变量
    global_counter++;
    printk(KERN_INFO "Global counter value: %d\n", global_counter);
    
    return 0;
}

static void __exit consumer_exit(void) {
    printk(KERN_INFO "Symbol consumer module unloaded\n");
}

module_init(consumer_init);
module_exit(consumer_exit);

MODULE_LICENSE("GPL");
MODULE_DESCRIPTION("Symbol Consumer Module");
MODULE_AUTHOR("cmy");
    

在这里插入图片描述

(四)驱动符号导出的注意事项

符号命名冲突:在多个驱动模块中,要特别注意避免导出符号的名称冲突。如果不同模块导出了相同名称的符号,会导致内核符号表中的信息混乱,使得模块加载失败,或者在运行时出现难以调试的错误。为了防止命名冲突,可以采用命名空间的方式,为符号名称添加特定的前缀或后缀,使其具有唯一性。 模块依赖关系:使用符号的模块完全依赖于导出符号的模块。在进行模块升级、维护或替换时,必须确保相关模块之间的兼容性。例如,如果导出符号的模块进行了功能修改或接口变更,使用该符号的模块也需要相应地进行调整,以适应新的变化。否则,可能会出现模块无法正常加载或运行错误的情况。 导出符号的安全性:在导出符号时,要充分考虑安全性问题。避免导出包含敏感信息的变量,如系统密码、密钥等;同时,对于导出的函数,要确保其不会被恶意利用,导致系统安全漏洞。在设计和实现导出符号时,应该进行严格的安全审查和测试,确保系统的安全性和稳定性。