【linux驱动】讲解linux驱动开发中的并发与并行,并且给出解决驱动开发中资源竞争的解决方案(上)

发布时间:2024年01月17日


开发环境:迅为3568开发板 + ubuntu18.04

并发与竞争相关概念

  • 共享资源:多个线程进程等能都访问的变量或内存等。比如共享单车。

  • 临界区:操作临界资源的那一段程序称之为临界区。

  • 共享资源与临界资源的关系:临界资源一定是共享资源,但是共享资源不一定是临界资源。当共享资源在一段时间内能否允许被多个进程访问时,就可称之为临界资源。

  • 线程:进程中的一个执行任务的控制单元,负责当前进程中程序的执行。在一个进程至少有一个线程,一个进程可以运行多个线程,同时多个线程也可共享数据。

  • 进程:一个在内存中运行的应用程序。每个进程都有自己独立的一块内存空间,一个进程可以有多个线程,比如在ubuntu中,可使用ps命令来查看当前的进程信息。

  • 并发:是指在单核CPU中的某一时间间隔内,多个任务分时运行。如下图:
    在这里插入图片描述

  • 并行:是指在多核CPU中的某个时刻,多个任务同时运行。如下图:
    在这里插入图片描述

  • 竞争:此处的竞争,主要是指资源竞争。如并发可能会造成多个程序同时访问一个共享资源,这时候由并发同时访问一个共享资源产生的问题就叫做竞争。引起竞争的原因有以下几点:

    • 多线程并发访问:多个线程同时访问同一共享资源,会造成竞争;
    • 中断程序并发访问:中断发生后,会暂停CPU当前任务转而去执行中断服务程序,若中断服务中操作了共享资源,那么就会产生竞争;
    • 抢占式并发访问:在线程1访问共享资源时,出现优先级更高的线程2打断线程1,同时线程2先对共享资源进行更改,这也会产生竞争;
    • 多处理器并发访问:多处理器并发访问产生竞争主要是指处理器内核间同时访问同一共享资源而引起的竞争。

注意在单核CPU中,并行无法实现;同理,在多核CPU内,一般不会有并发。

自制一个用于并发的驱动

制作一个用于并发的驱动简单来说,就是需要处理驱动中的write函数,因为用户在应用程序中调用write最终还是会调用驱动中的write
这里可以使第一个调用驱动暂停5秒,第二次调用驱动就暂停10秒,这里需要注意的是驱动开发中需要使用ssleep函数来进行休眠,其中参数是休眠时间,单位秒。

	if(strcmp(kbuff,"test1") == 0)
	{
		ssleep(5);
	}
	else if(strcmp(kbuff,"test2") == 0)
	{
		ssleep(10);
	}

为了编写驱动简单,此处直接使用杂项设备驱动而不使用字符驱动,整个驱动文件如下:

#include <linux/kernel.h>
#include <linux/init.h>              //初始化头文件
#include <linux/module.h>            //最基本的文件,支持动态添加和卸载模块。
#include <linux/miscdevice.h>        //注册杂项设备头文件
#include <linux/fs.h>                //注册设备节点的文件结构体
#include <linux/uaccess.h>
#include <linux/delay.h>


/*	进程并发与并行的驱动 */ 

// 驱动名
#define DEVICE_NAME "threadtest"

// 数据buffer
static char kbuff[32] ;

// 打开设备
int misc_open(struct inode *inode,struct file*file)
{
	printk("misc_open is ok.\r\n");
	return 0;
}

// 关闭设备
int misc_close(struct inode * inode, struct file *file)
{
	printk("misc_close\r\n");
	return 0;
}

// 读取设备中信息
ssize_t misc_read (struct file *file, char __user *buff, size_t size, loff_t *loff)
{
	int len = strlen(kbuff);
	if(copy_to_user(buff,kbuff,len) != 0) // 将内核中的数据给应用
	{
		printk("copy_to_user error\r\n");
		return -1;
	}	
	printk("copy_to_user is successful\r\n");

	return len;
}

// 写入设备信息
ssize_t misc_write (struct file *file, const char __user *buff, size_t size, loff_t *loff)
{
	int ret = copy_from_user(kbuff,buff,size);
	if( ret != 0) // 从应用那儿获取数据
	{
		printk("ret of error is %d.\r\n",ret);
		return ret;
	}	
	if(strcmp(kbuff,"test1") == 0)
	{
		ssleep(2);
	}
	else if(strcmp(kbuff,"test2") == 0)
	{
		ssleep(4);
	}

	printk("copy_from_user data:%s.\r\n",kbuff);
	
	return size;
}

// 设备文件操作集
struct file_operations misc_fops ={
	.owner = THIS_MODULE,
	.open = misc_open,
	.release = misc_close,
	.read = misc_read,
	.write = misc_write
};

// 设备文件信息描述集
struct miscdevice misc_dev = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = DEVICE_NAME,
	.fops = &misc_fops
};

// 驱动的入口函数
static int __init misc_init(void)
{
	// 申请杂项设备
	int ret = 0;
	ret = misc_register(&misc_dev);
	printk("--------------------------------------------\r\n");
	if(ret < 0)
	{
		printk("misc register is error.\r\n");
	}
	else
		printk("misc register is seccussful.\r\n");
	
	
	return ret;
}

//驱动的出口函数
static void __exit misc_exit(void)
{
	// 注销杂项设备
	misc_deregister(&misc_dev);
	
	printk("misc_exit\r\n");
}

module_init(misc_init);
module_exit(misc_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("zxj");

在开发板上使用insmod + 驱动名.ko后就可编写测试程序,并编译运行。

创建一个并发程序

测试程序的逻辑为,打开文件后先写入数据,然后读取刚才写入的数据并且打印在控制台上,详细的测试程序见下:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <string.h>
#include <stdlib.h>

int main(int argc,char *argv[])
{
	if(argc != 2)
	{
		printf("the number of your input is eror\n");
		return -1;
	}

	char filePath[] = "/dev/threadtest";
	int fd = open(filePath,O_RDWR);
	if(fd < 0)
	{
		printf("opening is failed.\n");
		return -1;
	}
	else
		printf("opening is successful.\n");

	// 写入数据
	write(fd,argv[1],strlen(argv[1]));
	
	char buff[10];
	// 读出数据
	read(fd,buff,sizeof(buff));
	printf("buff:%s   argv:%s\r\n",buff,argv[1]);

	close(fd);

	return 0;
}

将测试程序传送到开发板后,可先试用cp命令拷贝一份文件,命令格式为cp 源文件 目的文件
然后再后台运行可执行文件,后台运行只需要在运行命令后加上一个&符号即可。执行结果如下:
在这里插入图片描述

在这里插入图片描述

解决资源竞争的方法

原子操作

原子操作,是借用化学中元素组成最小单位为原子来命名的。顾名思义,原子操作是指最小的操作,该操作是不可再分的,也是不能够被中断打断的。

原子操作函数
在32位操作系统中定义一个原子操作变量可使用atomic_t结构体,该结构体的具体定义见下方:

typedef struct {
int counter;
} atomic_t;

在64位操作系统中定义一个原子操作变量可使用atomic64_t结构体,该结构体的具体定义见下方:

typedef struct {
long counter;
} atomic64_t;

在32位机器中,常见的原子操作函数如下:

函数描述
ATOMIC_INIT(int i)定义原子变量的时候初始化
int atomic_read(atomic_t *v)读取原子变量 v 的值,并且返回
void atomic_set(atomic_t *v, int i)向 v 写入 i 值
void atomic_add(int i, atomic_t *v)给 v 加上 i 值
void atomic_sub(int i, atomic_t *v)给 v 减去 i 值
void atomic_inc(atomic_t *v)自增
void atomic_dec(atomic_t *v)自减
int atomic_dec_return(atomic_t *v)自减,并返回 v 的值
int atomic_inc_return(atomic_t *v)自增,并返回 v 的值
int atomic_sub_and_test(int i, atomic_t *v)从 v 减 i,如果结果为 0 就返回真,否则返回假
int atomic_dec_and_test(atomic_t *v)从 v 减 1,如果结果为 0 就返回真,否则返回假
int atomic_inc_and_test(atomic_t *v)给 v 加 1,如果结果为 0 就返回真,否则返回假
int atomic_inc_and_test(atomic_t *v)给 v 加 1,如果结果为 0 就返回真,否则返回假
int atomic_add_negative(int i, atomic_t *v)给 v 加 i,如果结果为负就返回真,否则返回假

64位机器中,原子操作函数的作用与32位机器一样,不过函数名有些许差别,64位机器上操作需要加上一个数字64,如32位机器中的读取函数为atomic_read,而64位机器中为atomic64_read

编写一个驱动,完成该驱动仅允许一个应用程序打开。

  • 首先,定义一个原子变量,并完成初始化:
    atomic64_t V = ATOMIC64_INIT(1)
  • 然后,在打开驱动open中,借助原子变量判断该驱动是否已经打开。如果已经打开就先恢复原子变量的值,再返回打开设备失败。
	// 判断是否已经有设备打开
	if(!(atomic64_sub_and_test(1,&V)))
	{
		// 已经打开一个设备 本次打开失败
		atomic64_add(1,&V); // 因为if判断中已经减1 因此此处的值就小于1 因此需要恢复
		return -1;
	}
  • 最后需要注意,每次设备关闭后,需要恢复原子操作至初始值,否则下次设备就无法打开。
	// 恢复原子锁
	atomic64_add(1,&V);

整个驱动的源码如下:

#include <linux/kernel.h>
#include <linux/init.h>              //初始化头文件
#include <linux/module.h>            //最基本的文件,支持动态添加和卸载模块。
#include <linux/miscdevice.h>        //注册杂项设备头文件
#include <linux/fs.h>                //注册设备节点的文件结构体
#include <linux/uaccess.h>

#include <linux/atomic.h>
#include <asm/atomic.h>
 
#include <asm/bitops.h>  
#include <linux/sched.h>  

/*	含原子操作驱动 */ 
atomic64_t V = ATOMIC64_INIT(1);

// 驱动名
#define DEVICE_NAME "atomictest"

// 数据buffer
static char kbuff[32] ;

// 打开设备
int misc_open(struct inode *inode,struct file*file)
{
	// 判断是否已经有设备打开
	if(!(atomic64_sub_and_test(1,&V)))
	{
		// 已经打开一个设备 本次打开失败
		atomic64_add(1,&V); // 因为if判断中已经减1 因此此处的值就小于1 因此需要恢复
		return -1;
	}

	printk("misc_open is ok.\r\n");
	return 0;
}

// 关闭设备
int misc_close(struct inode * inode, struct file *file)
{
	// 恢复原子锁
	atomic64_add(1,&V);

	printk("misc_close\r\n");
	return 0;
}

// 读取设备中信息
ssize_t misc_read (struct file *file, char __user *buff, size_t size, loff_t *loff)
{
	int len = strlen(kbuff);
	if(copy_to_user(buff,kbuff,len) != 0) // 将内核中的数据给应用
	{
		printk("copy_to_user error\r\n");
		return -1;
	}	
	printk("copy_to_user is successful\r\n");

	return len;
}

// 写入设备信息
ssize_t misc_write (struct file *file, const char __user *buff, size_t size, loff_t *loff)
{
	int ret = copy_from_user(kbuff,buff,size);
	if( ret != 0) // 从应用那儿获取数据
	{
		printk("ret of error is %d.\r\n",ret);
		return ret;
	}	

	printk("copy_from_user data:%s.\r\n",kbuff);

	return size;
}

// 设备文件操作集
struct file_operations misc_fops ={
	.owner = THIS_MODULE,
	.open = misc_open,
	.release = misc_close,
	.read = misc_read,
	.write = misc_write
};

// 设备文件信息描述集
struct miscdevice misc_dev = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = DEVICE_NAME,
	.fops = &misc_fops
};

// 驱动的入口函数
static int __init misc_init(void)
{
	// 申请杂项设备
	int ret = 0;
	ret = misc_register(&misc_dev);
	printk("--------------------------------------------\r\n");
	if(ret < 0)
	{
		printk("misc register is error.\r\n");
	}
	else
		printk("misc register is seccussful.\r\n");
	
	return ret;
}

//驱动的出口函数
static void __exit misc_exit(void)
{
	// 注销杂项设备
	misc_deregister(&misc_dev);
	
	printk("misc_exit\r\n");
}

module_init(misc_init);
module_exit(misc_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("zxj");

使用insmod加载驱动后,直接写一个打开驱动的测试函数即可,由于测试函数过于简单,此处就不再编写。如果大家想要源码参考,可点击杂项设备的详细说明

测试结果如下(说明第一个failed与第二个successful相隔的时间远远大于驱动暂停的时间):
在这里插入图片描述

位原子操作函数
与原子整形变量不同,原子位操作没有 atomic_t 的数据结构,原子位操作是直接对内存进行操作,原子位操作相关 API 函数如下:

函数描述
void set_bit(int nr, void *p)将 p 地址的第 nr 位置 1
void clear_bit(int nr,void *p)将 p 地址的第 nr 位清零
void change_bit(int nr, void *p)将 p 地址的第 nr 位进行翻转
int test_bit(int nr, void *p)获取 p 地址的第 nr 位的值
int test_and_set_bit(int nr, void *p)将 p 地址的第 nr 位置 1,并且返回 nr 位原来的值
int test_and_clear_bit(int nr, void *p)将 p 地址的第 nr 位清零,并且返回 nr 位原来的值
int test_and_change_bit(int nr, void *p)将 p 地址的第 nr 位翻转,并且返回 nr 位原来的值

具体使用较为简单,可设置一个变量保存需要的数据,然后再定义一个指针指向该变量,后续就可直接使用位原子操作函数间接操作该变量了。例如:

编写一个驱动,首先将位原子变量的第 1位置1,并打印数据;然后再将位原子操作的第 2位翻转,并打印数据;最后将位原子操作的第2位置0,并打印数据。

  • 先定义一个unsigned long类型变量,并且定义一个同型指针,指向该地址。
	// 保存数据
	unsigned long data = 5;
	// 定义指针 指向数据保存位置
	unsigned long *p = &data;
  • 然后就可使用上述位原子操作API操作了。
	//将p地址的第1个位置数据置1
	set_bit(1,p); 
	printk("data1:%ld  %ld\r\n",*p,data);

	// 将p地址的第2为位置数据翻转
	change_bit(2,p); 
	printk("data2:%ld  %ld\r\n",*p,data);

	//将 p 地址的第 nr 位置 0,并且返回 nr 位原来的值
	ret = test_and_clear_bit(2,p); 
	printk("data3:%d  %ld   %ld\r\n",ret,*p,data);

完成驱动源码如下:

#include <linux/kernel.h>
#include <linux/init.h>              //初始化头文件
#include <linux/module.h>            //最基本的文件,支持动态添加和卸载模块。
#include <linux/miscdevice.h>        //注册杂项设备头文件
#include <linux/fs.h>                //注册设备节点的文件结构体
#include <linux/uaccess.h>

#include <linux/atomic.h>
#include <asm/atomic.h>
 
#include <asm/bitops.h>  
#include <linux/sched.h>  

// 驱动名
#define DEVICE_NAME "atomictest"

unsigned long data = 5;
unsigned long *p = &data;

// 设备文件操作集
struct file_operations misc_fops ={
	.owner = THIS_MODULE,
};

// 设备文件信息描述集
struct miscdevice misc_dev = {
	.minor = MISC_DYNAMIC_MINOR,
	.name = DEVICE_NAME,
	.fops = &misc_fops
};

// 驱动的入口函数
static int __init misc_init(void)
{
	// 申请杂项设备
	int ret = 0;
	ret = misc_register(&misc_dev);
	printk("--------------------------------------------\r\n");
	if(ret < 0)
	{
		printk("misc register is error.\r\n");
	}
	else
		printk("misc register is seccussful.\r\n");
	
	//将p地址的第1个位置数据置1
	set_bit(1,p); 
	printk("data1:%ld  %ld\r\n",*p,data);

	// 将p地址的第2为位置数据翻转
	change_bit(2,p); 
	printk("data2:%ld  %ld\r\n",*p,data);

	//将 p 地址的第 nr 位置 0,并且返回 nr 位原来的值
	ret = test_and_clear_bit(2,p); 
	printk("data3:%d  %ld   %ld\r\n",ret,*p,data);

	return ret;
}

//驱动的出口函数
static void __exit misc_exit(void)
{
	// 注销杂项设备
	misc_deregister(&misc_dev);
	
	printk("misc_exit\r\n");
}

module_init(misc_init);
module_exit(misc_exit);
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("zxj");

使用insmod加载驱动后,就可直接查看执行结果:

在这里插入图片描述
咱还可以手动分析来校验位原子操作的正确性,检验过程如下:
在这里插入图片描述

文章来源:https://blog.csdn.net/qq_53960242/article/details/135574402
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。