【系列文章】Linux中的并发与竞争[01]-并发与竞争的概念
该文章为系列文章:Linux中的并发与竞争中的第1篇
该系列的导航页连接:
【系列文章】Linux中的并发与竞争-导航页
文章目录
一、并发与竞争
1.1并发
早期计算机大多只有一个 CPU 核心,一个 CPU 在同一时间只能执行一个任务,当系统中有多个任务等待执行时,CPU 只能执行完一个再执行下一个。而计算机的很多指令会涉及 I/O操作,执行速度远远低于 CPU 内高速存储器的存取速度,这就导致 CPU 经常处于空闲状态,只能等待 I/O 操作完成后才能继续执行后面的指令。为了提高 CPU 利用率,减少等待时间,提出了 CPU 并发工作理论。
所谓并发,就是通过算法将 CPU 资源合理地分配给多个任务,当一个任务执行 I/O 操作时,CPU 可以转而执行其它的任务,等到 I/O 操作完成以后,或者新的任务遇到 I/O 操作时,CPU 再回到原来的任务继续执行。
下图展示了两个任务并发执行的过程(为了容易理解,这里以两个任务并发执行为例,当然一个 CPU 核心并不仅仅只能两个任务并发):

虽然 CPU 在同一时刻只能执行一个任务,但是通过将 CPU 的使用权在恰当的时机分配给不同的任务,使得多个任务看起来是一起执行的(CPU 的执行速度极快,多任务切换的时间也极短)。
1.2并行
并发是针对单核 CPU 提出的,而并行则是针对多核 CPU 提出的。和单核 CPU 不同,多核 CPU 真正实现了“同时执行多个任务”。多核 CPU 的每个核心都可以独立地执行一个任务,而且多个核心之间不会相互干扰。在不同核心上执行的多个任务,是真正地同时运行,这种状态就叫做并行。双核 CPU 的工作状态如下图所示:

双核 CPU 执行两个任务时,每个核心各自执行一个任务,和单核 CPU 在两个任务之间不断切换相比,它的执行效率更高。
1.3并发+并行
在并行的工作状态中,两个 CPU 分别执行两个任务,是一种理想状态。但是在实际场景中,处于运行状态的任务是非常多的,以实际办公电脑为例,windows 系统在开机之后会运行几十个任务,而 CPU 往往只有 4 核、8 核等,远远低于任务的数量,这个时候就会同时存在并发和并行两种情况,即所有核心在并行工作的同时,每个核心还要并发工作。例如一个双核 CPU 要执行四个任务,它的工作状态如下图所示:

为了容易理解,这里是以两个任务并发执行为例,当然一个 CPU 核心并不仅仅只能两个任务并发,并发任务的数量和操作系统的分配方式、以及每个任务的工作状态有关系。
并发可以看作是并行的理想状态,为了便于讲解和避免产生歧义,之后的章节无论是并发还是并行,都会统称为并发。
1.4竞争
并发可能会造成多个程序同时访问一个共享资源,这时候由并发同时访问一个共享资源产生的问题就叫做竞争。
竞争产生的原因如下所示:
(1)多线程的并发访问。由于 Linux 是多任务操作系统,所以多线程访问是竞争产生的基本原因。
(2)中断程序的并发访问。中断任务产生后,CPU 会立刻停止当前工作,从而去执行中断中的任务,如果中断任务对共享资源进行了修改,就会产生竞争。
(3)抢占式并发访问。linux2.6 及更高版本引入了抢占式内核,高优先级的任务可以打断低优先级的任务。在线程访问共享资源的时候,另一个线程打断了现在正在访问共享资源的线程同时也对共享资源进行操作,从而造成了竞争。
(4)多处理器(SMP)并发访问。多核处理器之间存在核间并发访问。
1.5共享资源的保护
竞争是由并发访问同一个共享资源产生的。为了防止“竞争”的产生就要对共享资源进行保护,这里提到的共享资源又是什么呢?
以实际生活中的共享资源为例,可以是公共电话,也可以是共享单车、共享充电宝等公共物品,以上都属于共享资源的范畴,以公共电话为例,每个人都可以对它进行使用,但在同一时间内只能由一个人进行使用,如果两个人都要对电话进行使用,则产生了竞争。而在实际的驱动的代码中,共享资源可以是全局变量,也可以是驱动中的设备结构体等,需要根据具体的驱动程序来进行分析。在下一小节的实验中,会以全局变量为例,进行并发与竞争实验。
二、实验程序的编写
2.1驱动程序编写
本实验将编写并发与竞争的驱动代码,首先完善字符设备驱动框架,然后通过copy_from_user(…)函数接收用户空间传递到内核空间的数据并进行判断,如果接收到的字符串数据为“topeet”会在睡眠 4 秒钟后打印接收到的数据,如果接收到的字符串数据为“itop”会在睡眠 2 秒钟后打印接收到的数据。
编写完成的 example.c 代码如下所示
#include <linux/init.h>
#include <linux/module.h>
#include <linux/fs.h>
#include <linux/cdev.h>
#include <linux/kdev_t.h>
#include <linux/uaccess.h>
#include <linux/delay.h>
static int open_test(struct inode *inode,struct file *file)
{
printk("\nthis is open_test \n");
return 0;
}
static ssize_t read_test(struct file *file,char __user *ubuf,size_t len,loff_t *off)
{
int ret;
char kbuf[10] = "topeet";//定义 char 类型字符串变量 kbuf
printk("\nthis is read_test \n");
ret = copy_to_user(ubuf,kbuf,strlen(kbuf));//使用 copy_to_user 接收用户空间传递的数据
if (ret != 0)
{
printk("copy_to_user is error \n");
}
printk("copy_to_user is ok \n");
return 0;
}
static char kbuf[10] = {0};//定义 char 类型字符串全局变量 kbuf
static ssize_t write_test(struct file *file,const char __user *ubuf,size_t len,loff_t *off)
{
int ret;
ret = copy_from_user(kbuf,ubuf,len);//使用 copy_from_user 接收用户空间传递的数据
if (ret != 0)
{
printk("copy_from_user is error\n");
}
if(strcmp(kbuf,"topeet") == 0 )//如果传递的 kbuf 是 topeet 就睡眠四秒钟
{
ssleep(4);
}
else if(strcmp(kbuf,"itop") == 0)//如果传递的 kbuf 是 itop 就睡眠两秒钟
{
ssleep(2);
}
printk("copy_from_user buf is %s \n",kbuf);
return 0;
}
static int release_test(struct inode *inode,struct file *file)
{
//printk("\nthis is release_test \n");
return 0;
}
struct chrdev_test {
dev_t dev_num;//定义 dev_t 类型变量 dev_num 来表示设备号
int major,minor;//定义 int 类型的主设备号 major 和次设备号 minor
struct cdev cdev_test;//定义 struct cdev 类型结构体变量 cdev_test,表示要注册的字符设备
struct class *class_test;//定于 struct class *类型结构体变量 class_test,表示要创建的类
};
struct chrdev_test dev1;//创建 chrdev_test 类型的
struct file_operations fops_test = {
.owner = THIS_MODULE,//将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
.open = open_test,//将 open 字段指向 open_test(...)函数
.read = read_test,//将 read 字段指向 read_test(...)函数
.write = write_test,//将 write 字段指向 write_test(...)函数
.release = release_test,//将 release 字段指向 release_test(...)函数
};
static int __init atomic_init(void)
{
if(alloc_chrdev_region(&dev1.dev_num,0,1,"chrdev_name") < 0 )//自动获取设备号,设备名
{
chrdev_name
printk("alloc_chrdev_region is error \n");
}
printk("alloc_chrdev_region is ok \n");
dev1.major = MAJOR(dev1.dev_num);//使用 MAJOR()函数获取主设备号
dev1.minor = MINOR(dev1.dev_num);//使用 MINOR()函数获取次设备号
printk("major is %d,minor is %d\n",dev1.major,dev1.minor);
//使用 cdev_init()函数初始化 cdev_test 结构体,并链接到fops_test 结构体
cdev_init(&dev1.cdev_test,&fops_test);
//将 owner 字段指向本模块,可以避免在模块的操作正在被使用时卸载该模块
dev1.cdev_test.owner = THIS_MODULE;
cdev_add(&dev1.cdev_test,dev1.dev_num,1);//使用 cdev_add()函数进行字符设备的添加
//使用 class_create 进行类的创建,类名称为 class_test
dev1.class_test = class_create(THIS_MODULE,"class_test");
//使用 device_create 进行设备的创建,设备名称为 device_test
device_create(dev1.class_test,0,dev1.dev_num,0,"device_test");
return 0;
}
static void __exit atomic_exit(void)
{
device_destroy(dev1.class_test,dev1.dev_num);//删除创建的设备
class_destroy(dev1.class_test);//删除创建的类
cdev_del(&dev1.cdev_test);//删除添加的字符设备 cdev_test
unregister_chrdev_region(dev1.dev_num,1);//释放字符设备所申请的设备号
printk("module exit \n");
}
module_init(atomic_init);
module_exit(atomic_exit)
MODULE_LICENSE("GPL v2");
MODULE_AUTHOR("XXX");
2.2编写测试 APP
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <string.h>
#include <unistd.h>
int main(int argc, char *argv[])
{
int fd;//定义 int 类型的文件描述符
char str1[10] = {0};//定义读取缓冲区 str1
fd = open(argv[1],O_RDWR);//调用 open 函数,打开输入的第一个参数文件,权限为可读可写
if(fd < 0 )
{
printf("file open failed \n");
return -1;
}
/*如果第二个参数为 topeet,条件成立,调用 write 函数,写入 topeet*/
if (strcmp(argv[2],"topeet") == 0 )
{
write(fd,"topeet",10);
}
/*如果第二个参数为 itop,条件成立,调用 write 函数,写入 itop*/
else if (strcmp(argv[2],"itop") == 0 )
{
write(fd,"itop",10);
}
close(fd);
return 0;
}
2.3运行测试
以下命令运行测试 app,运行结果如下图所示:
./app /dev/device_test topeet

可以看到传递的 buf 值为 topeet,然后输入以下命令在后台运行两个 app,来进行竞争测试,运行结果如下图所示:
./app /dev/device_test topeet &
./app /dev/device_test itop &

在不存在竞争的情况下,传递的两个字符串数据应该是 topeet 和 itop,而在上图中的打印信息为两个 itop,原因是第二个 app 应用程序运行之后对共享资源进行了修改,两个 app 应用程序就产生了竞争关系,会在之后的章节中使用不同的方法对上述驱动程序进行改进,从而避免竞争的产生。

被折叠的 条评论
为什么被折叠?



