LV06-06-并发与竞争-01-并发与竞争简介

要是现在同时有两个应用程序访问我们的字符设备怎么办?什么是并发?什么是竞争?若笔记中有错误或者不合适的地方,欢迎批评指正😃。

点击查看使用工具及版本
PC端开发环境 Windows Windows11
Ubuntu Ubuntu20.04.2的64位版本
VMware® Workstation 17 Pro 17.6.0 build-24238078
终端软件 MobaXterm(Professional Edition v23.0 Build 5042 (license))
Win32DiskImager Win32DiskImager v1.0
Linux开发板环境 Linux开发板 正点原子 i.MX6ULL Linux 阿尔法开发板
uboot NXP官方提供的uboot,使用的uboot版本为U-Boot 2019.04
linux内核 linux-4.19.71(NXP官方提供)
点击查看本文参考资料
分类 网址 说明
官方网站 https://www.arm.com/ ARM官方网站,在这里我们可以找到Cotex-Mx以及ARMVx的一些文档
https://www.nxp.com.cn/ NXP官方网站
https://www.nxpic.org.cn/NXP 官方社区
https://u-boot.readthedocs.io/en/latest/u-boot官网
https://www.kernel.org/linux内核官网
点击查看相关文件下载
分类 网址 说明
NXP https://github.com/nxp-imx NXP imx开发资源GitHub组织,里边会有u-boot和linux内核的仓库
nxp-imx/linux-imx/releases/tag/v4.19.71 NXP linux内核仓库tags中的v4.19.71
nxp-imx/uboot-imx/releases/tag/rel_imx_4.19.35_1.1.0 NXP u-boot仓库tags中的rel_imx_4.19.35_1.1.0
I.MX6ULL i.MX 6ULL Applications Processors for Industrial Products I.MX6ULL 芯片手册(datasheet,可以在线查看)
i.MX 6ULL Applications ProcessorReference Manual I.MX6ULL 参考手册(下载后才能查看,需要登录NXP官网)
Source Code https://elixir.bootlin.com/linux/latest/source linux kernel源码
kernel/git/stable/linux.git - Linux kernel stable tree linux kernel源码(官网,tag 4.19.71)
https://elixir.bootlin.com/u-boot/latest/source uboot源码

一、举个例子

有一台打印机,大家都可以使用。现在 A 和 B 要同时使用这一台打印机,都要打印一份文件。A 要打印的文件内容如下:

1
2
3
姓名: A
电话: 15111111111
工号: 001

B要打印的信息如下:

1
2
3
姓名: B
电话: 18100000000
工号: 002

这两份文档肯定是各自打印出来的,不能相互影响。当两个人同时打印的话如果打印机不做处理的话可能会出现 A 的文档打印了一行,然后开始打印 B 的文档,这样打印出来的文档就错乱了,可能会出现如下的错误文档内容:

1
2
3
姓名: A
电话: 18100000000
工号: 001

可以看出,A 打印出来的文档中电话号码错误了,变成 B 的了,这是绝对不允许的。如果有多人同时向打印机发送了多份文档,打印机必须保证一次只能打印一份文档,只有打印完成以后才能打印其他的文档。

二、并发与竞争

1. 并发的概念

早期计算机大多只有一个 CPU 核心, 一个 CPU 在同一时间只能执行一个任务, 当系统中有多个任务等待执行时, CPU 只能执行完一个再执行下一个。 而计算机的很多指令会涉及 I/O操作, 执行速度远远低于 CPU 内高速存储器的存取速度, 这就导致 CPU 经常处于空闲状态,只能等待 I/O 操作完成后才能继续执行后面的指令。 为了提高 CPU 利用率, 减少等待时间,提出了 CPU 并发工作理论。

所谓并发, 就是通过算法将 CPU 资源合理地分配给多个任务, 当一个任务执行 I/O 操作时, CPU 可以转而执行其它的任务, 等到 I/O 操作完成以后, 或者新的任务遇到 I/O 操作时, CPU 再回到原来的任务继续执行。

下图展示了两个任务并发执行的过程(为了容易理解, 这里以两个任务并发执行为例, 当然一个 CPU 核心并不仅仅只能两个任务并发) :

image-20250121092703236

虽然 CPU 在同一时刻只能执行一个任务, 但是通过将 CPU 的使用权在恰当的时机分配给不同的任务, 使得多个任务看起来是一起执行的(CPU 的执行速度极快, 多任务切换的时间也极短) 。

2. 并行的概念

并发是针对单核 CPU 提出的, 而并行则是针对多核 CPU 提出的。 和单核 CPU 不同, 多核 CPU 真正实现了“同时执行多个任务”。 多核 CPU 的每个核心都可以独立地执行一个任务,而且多个核心之间不会相互干扰。 在不同核心上执行的多个任务, 是真正地同时运行, 这种状态就叫做并行。 双核 CPU 的工作状态如下图所示:

image-20250121092812568

双核 CPU 执行两个任务时, 每个核心各自执行一个任务, 和单核 CPU 在两个任务之间不断切换相比, 它的执行效率更高。

3. 并发+并行

在并行的工作状态中, 两个 CPU 分别执行两个任务, 是一种理想状态。 但是在实际场景中, 处于运行状态的任务是非常多的, 以实际办公电脑为例, windows 系统在开机之后会运行几十个任务, 而 CPU 往往只有 4 核、 8 核等, 远远低于任务的数量, 这个时候就会同时存在并发和并行两种情况, 即所有核心在并行工作的同时, 每个核心还要并发工作。

例如一个双核 CPU 要执行四个任务, 它的工作状态可能如下图所示:

image-20250121092940224

为了容易理解, 这里是以两个任务并发执行为例, 当然一个 CPU 核心并不仅仅只能两个任务并发, 并发任务的数量和操作系统的分配方式、 以及每个任务的工作状态有关系。

Tips:并发可以看作是并行的理想状态, 为了便于学习和避免产生歧义, 之后的笔记无论是并发还是并行, 都会统称为并发。

3. 竞争

3.1 共享资源

什么是共享资源?以实际生活中的共享资源为例, 可以是公共电话, 也可以是共享单车、 共享充电宝等公共物品, 以上都属于共享资源的范畴, 以公共电话为例, 每个人都可以对它进行使用, 但在同一时间内只能由一个人进行使用, 如果两个人都要对电话进行使用, 就会出现矛盾,这个其实就是后面说的竞争。

在linux系统中可被多个线程访问的内容都可被称为共享资源,比如一个文件、一块内存、全局变量。 linux系统中一切皆文件,我们编写的驱动在系统中对应一个设备节点文件,从应用程序角度看,驱动也是共享资源。 竞争访问对于单核linux系统主要表现为抢占式内核以及中断。

Tips:linux2.6及更高版本引入了抢占式内核, 高优先级的任务可以打断低优先级的任务。在线程访问共享资源的时候,另一个线程打断了现在正在访问共享资源的线程同时也对共享 资源进行操作,从宏观角度上来看两个线程对共享资源的访问是“同时”的,存在着竞争关系, 同样也存在线程和中断的“同时”访问共享资源的情况。

3.2 竞争的概念

并发可能会造成多个程序同时访问一个共享资源, 这时候由并发同时访问一个共享资源产生的问题就叫做竞争。 就比如上面的打印机,A在用,B现在也要用,但是资源只有一个,这个时候A和B就会产生竞争。

3.3 竞争产生的原因

Linux 系统是个多任务操作系统,会存在多个任务同时访问同一片内存区域,这些任务可能会相互覆盖这段内存中的数据,造成内存数据混乱。针对这个问题必须要做处理,严重的话可能会导致系统崩溃。Linux 系统并发产生的原因很复杂,总结一下有下面几个主要原因

(1)多线程并发访问, Linux 是多任务(线程)的系统,所以多线程访问是最基本的原因。

(2)抢占式并发访问,linux2.6 及更高版本引入了抢占式内核,也就是说调度程序可以在任意时刻抢占正在运行的线程,从而运行其他的线程。高优先级的任务可以打断低优先级的任务。 在线程访问共享资源的时候, 另一个线程打断了现在正在访问共享资源的线程同时也对共享资源进行操作, 从而造成了竞争。

(3)中断程序并发访问,中断任务产生后, CPU 会立刻停止当前工作, 从而去执行中断中的任务, 如果中断任务对共享资源进行了修改, 就会产生竞争。

(4)SMP(多核)核间并发访问,多核处理器之间存在核间并发访问,现在 ARM 架构的多核 SOC 很常见。

三、共享资源的保护

1. 保护什么?

前面一直说要防止并发访问共享资源,换句话说就是要保护共享资源,防止进行并发访问。那保护的内容是什么?

需要知道的是,我们保护的不是代码,而是数据!某个线程的局部变量不需要保护,我们要保护的是多个线程都会访问的共享数据。一个整形的全局变量 a 是数据,一份要打印的文档也是数据。

虽然我们知道了要对共享数据进行保护,那么怎么判断哪些共享数据要保护呢?找到要保护的数据才是重点,而这个也是难点,因为驱动程序各不相同,那么数据也千变万化,一般像全局变量,设备结构体这些肯定是要保护的,至于其他的数据就要根据实际的驱动程序而定了。当我们发现驱动程序中存在并发和竞争的时候一定要处理掉。

Tips:

不处理竞争问题的话,可能会出现一些莫名其妙的错误,这些错误发生的时候,我们加打印信息去确定的时候,又会改变线程运行的时序,可能就无法再出现了,之前工作中就出现过一个竞争导致的问题,加打印就怎么都无法复现。

2. 怎么保护?

解决竞争的主要路径是当有一个执行单元在访问共享资源时,其他的执行单元禁止访问, 根据共享资源的种类以及实际应用场景我们有多种解决方法可选,常用的几种方式如下。

2.1 原子操作

“原子”的定义是“化学反应不可再分的基本微粒”,“原子操作”可以理解为“不可拆分的操作”。

原子操作保证了对于一个整型数据的修改是不可分割的,它的实现与CPU的架构息息相关,对于ARM处理器而言,底层使用LDREX和 STREX指令,而Linux内核提供了一系列函数来实现内核中的原子操作,我们只需要调用相对应的函数即可进行原子操作。

原子操作进一步细分为“整型原子操作”和“位原子操作”,两者都只能保护“整型”数据。

2.2 自旋锁

自旋锁是实现互斥访问的常用手段,在一些实时操作系统中也经常用到。自旋锁主要操作分为定义自旋锁、初始化自旋锁、 获取自旋锁、释放自旋锁。 “自旋锁”的作用是给一段代码“加锁”。获取自旋锁成功后才能运行被保护的代码,运行结束后释放自旋锁。 “自旋”是指获取自旋锁失败后会一直轮询检测锁的状态,直到自旋锁被释放。CPU在等待自旋锁时不做任何有用的工作,仅仅是等待。

自旋锁的缺点是自旋锁失败后会循环检测自旋锁的状态,占用系统资源。 同时应该避免自旋锁导致系统死锁,既在递归调用时,递归使用自旋锁。如果一个已经拥有自旋锁的线程想第二次获取这个自旋锁, 那么将导致死锁的情况。自旋锁在锁定期间不能调用可能引起进程调度的函数,不然可能导致内核崩溃。 优点是实现简单、不需要休眠、可以在中断中使用。

2.3 信号量

信号量是操作系统中最典型的用于同步和互斥的手段,信号量的值可以是0、1或者n; 信号量的使用一般分为定义信号量、初始化信号量、尝试获取信号量、获取信号量和释放信号量。 对比“自旋锁”使用方法差异不大,两者差异主要表现在申请失败后的处理方式,自旋锁申请失败后会轮询检测锁的状态, 而信号量申请失败后进程可引入休眠,当信号量可用时由系统通知进程退出休眠。

由于信号量无需轮询检测信号量状态所以它不会造成系统资源的浪费,缺点是会引起进程的休眠所以不能在中断中使用。 信号量的“量值”可以是多个所以它可以同时保护多个资源。例如有5个缓冲区,使用信号量时将“量值”设置为5每获取一次“量值”减一, 释放一次“量值”加一。当信号量的值为0时,该线程将进入等待状态,直到信号量释放唤醒。

2.4 互斥体

信号量已经实现了互斥的功能了,互斥体可以说是“量值”为1的信号量,也可以叫互斥锁。把信号量的“量值”设置为1就行了那为什么还要引入互斥体呢? 很简单,虽然两者功能相同但是具体实现不同,互斥体效率更高。在使用信号量时,如果“量值”为1,考虑到效率, 我们一般将其改为使用互斥体实现。

四、竞争问题demo

1. 实例1

1.1 demo源码

07_concurrency/02_concurrency_competition · 苏木/imx6ull-driver-demo - 码云 - 开源中国

1.2 开发板验证

我们执行make命令编译驱动,将得到的sdriver_demo.ko、app_demo.out拷贝到开发板,然后加载驱动:

1
insmod sdriver_demo.ko
image-20250122094859559

然后我们执行测试程序:

1
2
./app_demo.out /dev/sdevchr 2 0 sumu1 &
./app_demo.out /dev/sdevchr 2 0 sumu2 &

然后会有以下打印信息:

image-20250122095303316

为了避免打印错乱,我把dmesg放到了后台,敲dmesg就可以看到驱动的打印信息了。可以看到最后我们的scdev_ops_write()函数打印的两次数据都是sumu2,但是我们第一次写入的是sumu1,那么来分析一下:

(1)执行写入sumu1的测试命令,驱动要休眠4秒才打印出写入的数据;

(2)执行写入sumu2的测试命令,驱动休眠2秒后打印数据

两个测试命令最后都会向缓冲区写入数据,但是由于(1)还未打印数据的时候,(2)执行了,它修改了缓冲区中的数据。这个缓冲区是两个app_demo.out所共享的,这就出现了竞争关系,导致最后数据出错。