Linux 并发与竞争

本文详细探讨了Linux环境中的并发与竞争问题,介绍了原子操作、自旋锁、信号量和互斥体等并发控制机制,并提供了相关实验以加深理解。
摘要由CSDN通过智能技术生成
Linux是一个多任务操作系统,肯定会存在多个任务共同操作同一段内存或者设备的情况,
多个任务甚至中断都能访问的资源叫做共享资源,就和共享单车一样。在驱动开发中要注意对
共享资源的保护,也就是要处理对共享资源的并发访问。比如共享单车,大家按照谁扫谁骑走
的原则来共用这个单车,如果没有这个并发访问共享单车的原则存在,只怕到时候为了一辆单
车要打起来了。在 Linux 驱动编写过程中对于并发控制的管理非常重要,本章我们就来学习一
下如何在 Linux 驱动中处理并发。

47.1 并发与竞争

1、并发与竞争简介
    并发就是多个“用户”同时访问同一个共享资源,比如你们公司有一台打印机,你们公司
的所有人都可以使用。现在小李和小王要同时使用这一台打印机,都要打印一份文件。
两个人同时打印的话如果打印机不做处理的话可能会出现小李的文档打印了一行,
然后开始打印小王的文档,这样打印出来的文档就错乱了。
如果有多人同时向打印机发送了多份文档,打印机必须保证一次只能打印一份文档,
只有打印完成以后才能打印其他的文档。

    Linux 系统是个多任务操作系统,会存在多个任务同时访问同一片内存区域,这些任务可
能会相互覆盖这段内存中的数据,造成内存数据混乱。针对这个问题必须要做处理,严重的话
可能会导致系统崩溃。现在的 Linux 系统并发产生的原因很复杂,总结一下有下面几个主要原
因:
①、多线程并发访问, Linux 是多任务(线程)的系统,所以多线程访问是最基本的原因。
②、抢占式并发访问,从 2.6 版本内核开始, Linux 内核支持抢占,也就是说调度程序可以
在任意时刻抢占正在运行的线程,从而运行其他的线程。
③、中断程序并发访问,这个无需多说,学过 STM32 的同学应该知道,硬件中断的权利可是很大的。
④、 SMP(多核)核间并发访问,现在 ARM 架构的多核 SOC 很常见,多核 CPU 存在核间并发访问。
    并发访问带来的问题就是竞争,学过 FreeRTOS和 UCOS的同学应该知道临界区这个概念,
所谓的临界区就是共享数据段,对于临界区必须保证一次只有一个线程访问,也就是要保证临
界区是原子访问的,注意这里的“原子”不是正点原子的“原子”。我们都知道,原子化学反应
不可再分的基本微粒,这里的原子访问就表示这一个访问是一个步骤,不能再进行拆分。如果
多个线程同时操作临界区就表示存在竞争,我们在编写驱动的时候一定要注意避免并发和防止
竞争访问。很多 Linux 驱动初学者往往不注意这一点,在驱动程序中埋下了隐患,这类问题往
往又很不容易查找,导致驱动调试难度加大、费时费力。所以我们一般在编写驱动的时候就要
考虑到并发与竞争,而不是驱动都编写完了然后再处理并发与竞争

2、保护内容是什么
前面一直说要防止并发访问共享资源,换句话说就是要保护共享资源,防止进行并发访问。
我们要保护的是多个线程都会访问的共享数据。
一个整形的全局变量 a 是数据,一份要打印的文档也是数据,虽然我们知道了要对共享数据进
行保护,那么怎么判断哪些共享数据要保护呢?找到要保护的数据才是重点,而这个也是难点,
因为驱动程序各不相同,那么数据也千变万化,一般像全局变量,设备结构体这些肯定是要保
护的,至于其他的数据就要根据实际的驱动程序而定了。
    当我们发现驱动程序中存在并发和竞争的时候一定要处理掉,接下来我们依次来学习一下
Linux 内核提供的几种并发和竞争的处理方法。

47.2 原子操作

47.2.1 原子操作简介
首先看一下原子操作,原子操作就是指不能在进一步分割的操作,一般原子操作用于变量
或者位操作。假如现在要对无符号整形变量 a 赋值,值为 3,对于 C 语言来讲很简单,直接就
是:
a = 3
但是 C 语言要先编译为成汇编指令, ARM 架构不支持直接对寄存器进行读写操作,比如
要借助寄存器 R0、 R1 等来完成赋值操作。假设变量 a 的地址为 0X3000000,“ a=3”这一行 C
语言可能会被编译为如下所示的汇编代码:
示例代码 47.2.1.1 汇编示例代码
1 ldr r0, =0X30000000 /* 变量 a 地址 */
2 ldr r1, = 5 /* 要写入的值 */
3 str r1, [r0] /* 将 5 写入到 a 变量中 */
示例代码 47.2.1.1 只是一个简单的举例说明,实际的结果要比示例代码复杂的多。从上述
代码可以看出, C 语言里面简简单单的一句“ a=3”,编译成汇编文件以后变成了 3 句,那么程
序在执行的时候肯定是按照示例代码 47.2.1.1 中的汇编语句一条一条的执行。
要解决这个问题就要保证示例代码 47.2.1.1 中的三行汇编指令作为一个整体运行,也就是作为一个原子存在。 
Linux 内核提供了一组原子操作 API 函数来完成此功能, Linux 内核提供了两组原子操作 API 函数,一组是对整形变量进行操作的,一组是对位进行操作的,我们接下来看一下这些 API 函数。

47.2.2 原子整形操作 API 函数
Linux 内核定义了叫做 atomic_t 的结构体来完成整形数据的原子操作,在使用中用原子变
量来代替整形变量,此结构体定义在 include/linux/types.h 文件中,定义如下:
示例代码 47.2.2.1 atomic_t 结构体
175 typedef struct {
   
    176 int counter;
177 } atomic_t;
如果要使用原子操作 API 函数,首先要先定义一个 atomic_t 的变量,如下所示:
atomic_t a; //定义 a
也可以在定义原子变量的时候给原子变量赋初值,如下所示:
atomic_t b = ATOMIC_INIT(0); //定义原子变量 b 并赋初值为 0
可以通过宏 ATOMIC_INIT 向原子变量赋初值。
原子变量有了,接下来就是对原子变量进行操作,比如读、写、增加、减少等等, Linux 内
核提供了大量的原子操作 API 函数,如表 47.2.2.1 所示:
函数 描述
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) 给 v 加 1,也就是自增。
void atomic_dec(atomic_t *v) 从 v 减 1,也就是自减
int atomic_dec_return(atomic_t *v) 从 v 减 1,并且返回 v 的值。
int atomic_inc_return(atomic_t *v) 给 v 加 1,并且返回 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_add_negative(int i, atomic_t *v) 给 v 加 i,如果结果为负就返回真,否则返回假
表 47.2.2.1 原子整形操作 API 函数表
如果使用 64 位的 SOC 的话,就要用到 64 位的原子变量, Linux 内核也定义了 64 位原子
结构体,如下所示:
示例代码 47.2.2.2 atomic64_t 结构体
typedef struct {
   
    long long counter;
} atomic64_t;
相应的也提供了 64 位原子变量的操作 API 函数,这里我们就不详细讲解了,和表 47.2.1.1
中的 API 函数有用法一样,只是将“ atomic_”前缀换为“ atomic64_”,将 int 换为 long long。如
果使用的是 64 位的 SOC,那么就要使用 64 位的原子操作函数。 Cortex-A7 是 32 位的架构,所
以本书中只使用表 47.2.2.1 中的 32 位原子操作函数。原子变量和相应的 API 函数使用起来很简
单,参考如下示例:
示例代码 47.2.2.2 原子变量和 API 函数使用
atomic_t v = ATOMIC_INIT(0); /* 定义并初始化原子变零 v=0 */
atomic_set(10); /* 设置 v=10 */
atomic_read(&v); /* 读取 v 的值,肯定是 10 */
atomic_inc(&v); /* v 的值加 1, v=11 */

47.2.3 原子位操作 API 函数
位操作也是很常用的操作, Linux 内核也提供了一系列的原子位操作 API 函数,只不过原
子位操作不像原子整形变量那样有个 atomic_t 的数据结构,原子位操作是直接对内存进行操作,
API 函数如表 47.2.3.1 所示:
函数 描述
void set_bit(int nr, void *p) 将 p 地址的第 nr 位置 1void 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 位原来的值。
表 47.2.3.1 原子位操作函数表

47.3 自旋锁

47.3.1 自旋锁简介
    原子操作只能对整形变量或者位进行保护,但是,在实际的使用环境中怎么可能只有整形
变量或位这么简单的临界区。举个最简单的例子,设备结构体变量就不是整型变量,我们对于
结构体中成员变量的操作也要保证原子性,在线程 A 对结构体变量使用期间,应该禁止其他的
线程来访问此结构体变量,这些工作原子操作都不能胜任,需要本节要讲的锁机制,在 Linux
内核中就是自旋锁。
    当一个线程要访问某个共享资源的时候首先要先获取相应的锁, 锁只能被一个线程持有,
只要此线程不释放持有的锁,那么其他的线程就不能获取此锁。对于自旋锁而言,如果自旋锁
正在
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值