一、目的
Linux驱动开发中有一个特别重要的知识点必须掌握,即并发、竞态以及同步。
什么是并发?
多个执行单元(进程、线程、中断)同时对一个共享资源的进行访问;此处的共享资源可以是外设、内存或者软件层面的全局变量静态变量等。
什么是同步?
多个执行单元对同一个共享资源访问会引发竞态问题,导致程序运行异常;为了保证某个时刻只能有一个执行单元对共享资源进行操作,就需要进行同步(即独占访问,即A在访问资源时,B只能忙等待或或者休眠;只有A释放其对共享资源的占用后,B才能访问)。
情景分析
上图中每条连线都代表并发可能发生的情景。
进程可能由于其时间片段用完发生调度,也有可能直接被更高优先级的进程抢占执行;中断也可以打断进程的执行。
在SMP多核系统中多个CPU都可以对外设或者内存进行访问,所以并发的情景更加频繁。
在单核支持抢占的系统中,进程A的执行流程可能被进程B打断;进程A的执行流程也可能被中断本身打断,故在单核支持抢占的系统中,并发也是现实存在的问题。
针对并发问题,Linux内核中提供了多种同步手段来协调资源的访问,例如关中断(单核简单系统中可用)、原子操作、自旋锁、信号量、互斥锁、完成量等。
但是我们细看其代码时,我们会发现在ARM平台中原子操作或者其他同步机制都需要LDREX/STREX指令的参与(还有更重要的一个知识点--屏障指令)。
本篇的目的就是帮助大家深入理解这两个指令的作用、实现原理以及应用。
二、介绍
参考资料
https://developer.arm.com/documentation/dht0008/a/arm-synchronization-primitives/exclusive-accesses/ldrex-and-strex?lang=en
LDREX/STREX是ARM架构上的同步原语,属于硬件层面的同步机制。同步发生在当共享资源某个时刻只能被一个执行单元访问时;共享资源可以是内存、外设设备;执行单元可以是处理器、进程或者线程;
一般是通过以原子方式(原子是最小的不可分割的)修改代表资源状态的一个变量来实现(同步);修改操作只会有两个结果,要么成功,要么失败;并且对所有的同时访问这个变量的执行单元都可见。
在简单系统中可以通过开关中断的方式实现;在多任务和多核系统中开关中断可能未必是个有效的方法,频繁的开关中断会影响系统的实时处理和调度,甚至有可能就是一个BUG所在。
LDREX/STREX这两个指令配合独占监控器(独占监控器会跟踪独占内存访问)可以实现原子地更新内存数据。
LDREX指令说明
LDREX指令从内存中加载一个字(word),并且初始化独占监控器的状态用来跟踪同步操作。
LDREX R1, [R0]
上面的代码片段从R0寄存器表示的地址中读取一个字,存放在R1寄存器中,并且更新独占监控器。
STREX指令说明
STREX指令将存储一个字到内存中,但是这个存储指令是有条件的;如果独占监控器允许这个存储操作,那么对应的内存地址就会更新,并且将返回值0保存在目标寄存器中,代表此次操作成功;如果独占监控器不允许,那么就不会更新独占监控器,并且将返回值1保存在目标寄存器中,代表此次操作失败。
基于上述逻辑,我们就可以实现条件执行语句,根据STREX不同的结果进行不同的操作。
独占监控器
在上面的描述中我们提到独占监控器,独占监控器是一种简单的状态机,其存在两种状态:打开或者独占。为了实现多个处理器间的同步,一般会存在两类独占监控器:本地监控器和全局监控器。
对非共享内存的独占访问只检查本地监控器;对共享内存的独占访问会同时检查本地和全局监控器
如果我们访问共享资源,例如上图中的Memory B,那么当CPU0访问B时,CPU0的本地独占监控器会标记为已被独占,同时全局独占监控器也会标记为已被独占(全局监控器会监控多个CPU对共享资源的访问)
上图中Memory A只会被CPU0访问,但是CPU0可能内部多个进程都会访问Memory A。
独占监控器情景分析
CPU0访问Memory A的情形
时间 | 进程1 | 进程2 |
T1 | LDREX | |
T2 | ... | LDREX |
T3 | STREX | ... |
T4 | STREX |
T1时刻进程1调用LDREX,此时本地监控器标记为已独占;
T2时刻进程2也调用LDREX,此时也会标记本地监控器为已独占;
T3时刻进程1调用STREX,此时由于本地监控器是独占状态,所以进程1的STREX操作成功同时清除本地独占器的独占状态;
T4时刻进程2调用STREX,但是此时本地独占器为Open状态,故此处存储操作不成功;所以进程2必须重新通过LDREX指令去获取内存值去判断。
CPU0和CPU1访问Memory B的情形和上述基本类似,此处不再赘述,只要特别注意的是,对于共享内存的访问,会更新全局监控器,STREX执行完毕后本地和全局独占监控器都会复位为Open状态。
互斥锁实现
基于LDREX/STREX这样的硬件特性,我们可以实现互斥锁或者信号量
注意lock_mutex/unlock_mutex函数中的DMB指令的使用
实现信号量
我们在实现互斥锁或者信号量时可以根据业务需要,可以永久等待或者超时等待,或者完全不等待仅查询是否可以获取到锁或者信号量。
至此,本篇的知识点就介绍完毕,记得点赞+收藏。