一、目的
Linux驱动开发中有一个特别重要的知识点必须掌握,即并发访问、竞态以及同步。
什么是并发?
多个执行单元(进程、线程、中断)同时对一个共享资源进行访问;此处的共享资源可以是外设、内存或者软件层面的全局变量静态变量等;只要并发的多个执行单元存在对共享资源的访问,竞态就有可能发生。
什么是竞态?
多个执行单元访问(修改)共享单元势必会造成逻辑上的不一致,导致程序异常或者崩溃(Crash)。
什么是同步?
为了保证某个时刻只能有一个执行单元对共享资源进行操作,就需要进行同步(即独占访问,即A在访问资源时,B只能忙等待或者休眠;只有A释放其对共享资源的占用后,B才能进行访问)。
同步的基本原理就是将并发的访问变成顺序访问。
情景分析
多核并发
上图中每条连线都代表并发可能发生的情景。
进程可能由于自身用完时间片发生调度,也有可能直接被更高优先级的进程抢占执行;同时中断也可以打断进程的执行。
在SMP多核系统中多个CPU都可以对外设或者内存进行访问,所以并发的情景更加频繁。
在单核支持抢占的系统中,进程A的执行流程可能被进程B打断;进程A的执行流程也可能被中断本身打断,故在单核支持抢占的系统中,并发也是现实存在的问题。
针对并发问题,Linux内核中提供了多种同步手段来协调资源的访问,例如关中断(单核简单系统中可用)、原子操作、自旋锁、信号量、互斥锁、完成量等。
但是我们细看其代码时,我们会发现在ARM平台中原子操作或者其他同步机制都需要LDREX/STREX指令的参与(还有更重要的一个知识点--屏障指令)。
本篇的目的就是帮助大家深入理解LDREX/STREX这两个指令的作用、实现原理以及应用。
二、介绍
在正式介绍LDREX/STREX这两个指令之前,我们先介绍一下LDR/STR指令。
LDR指令说明
LDR指令用于从内存中加载一个字(word)到寄存器
LDR R1, [R0]
上面的代码片段从R0寄存器存储的内存地址处读取一个字到R1寄存器
STR指令说明
STR指令用于存储一个字到内存
STR R1,[R0]
上面的代码片段将R1寄存器中的值存储到R0寄存器存储的内存地址处
LDREX/STREX详解
按照惯例我们这边贴出ARM官网的资料(英语比较好的同学可以直接去看原版)
参考资料
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不同的结果进行不同的操作。
独占监控器
在上面的描述中我们提到独占监控器,独占监控器是一种简单的状态机,其存在两种状态:打开(open)或者独占(exclusive)。为了实现多个处理器间的同步,一般会存在两类独占监控器:本地监控器和全局监控器。
对非共享内存的独占访问只检查本地监控器;对共享内存的独占访问会同时检查本地和全局监控器
如果我们访问共享资源,例如上图中的Memory B,那么当CPU0访问B时,CPU0的本地独占监控器会标记为已独占,同时全局独占监控器也会标记为已被独占(全局监控器会监控多个CPU对共享资源的访问)
上图中Memory A只会被CPU0访问,但是CPU0可能内部多个进程都会访问Memory A;同样Memory C只会被CPU1访问,但是CPU1可能内部多个进程都会访问Memory C。
独占监控器情景分析
CPU0访问Memory A的情形
时间 |
进程1 |
进程2 |
T1 |
LDREX |
|
T2 |
... |
LDREX |
T3 |
STREX |
... |
T4 |
STREX |
T1时刻进程1调用LDREX访问Memory A,此时本地监控器标记为已独占;
T2时刻进程2也调用LDREX访问Memory A,此时也会标记本地监控器为已独占;
T3时刻进程1调用STREX,此时由于本地监控器是独占状态,所以进程1的STREX操作成功同时清除本地独占器的独占状态;
T4时刻进程2调用STREX,但是此时本地独占器为Open状态,故此处存储操作不成功;所以进程2必须重新通过LDREX指令去获取内存值去判断。
CPU0和CPU1同时访问Memory B的情景
时间 |
CPU0上的进程X |
CPU1上的进程Y |
T1 |
LDREX |
|
T2 |
... |
LDREX |
T3 |
STREX |
... |
T4 |
STREX |
T1时刻CPU0进程X调用LDREX访问Memory B,此时本地监控器和全局监控器标记为已独占;
T2时刻CPU1进程Y也调用LDREX访问Memory B,此时也会标记本地监控器和全局监控器为已独占;
T3时刻CPU0进程X调用STREX对Memory B进行存储操作,此时由于本地监控器和全局监控器是独占状态,所以CPU0进程X的STREX操作成功同时清除本地独占器和全解监控器的独占状态;
T4时刻CPU1进程Y调用STREX,此时本地独占器为独占状态但是全局监控器为open状态,故此处存储操作不成功;所以进程Y必须重新通过LDREX指令去获取内存值去判断。
互斥锁实现
基于LDREX/STREX这样的硬件特性,我们可以实现互斥锁或者信号量
注意lock_mutex/unlock_mutex函数中的DMB指令的使用
实现信号量
我们在实现互斥锁或者信号量时可以根据业务需要,可以永久等待或者超时等待,或者完全不等待仅查询是否可以获取到锁或者信号量。文章来源:https://www.toymoban.com/news/detail-698201.html
至此,本篇的知识点就介绍完毕,记得点赞+收藏。文章来源地址https://www.toymoban.com/news/detail-698201.html
到了这里,关于ARM LDREX/STREX指令以及独占监控器详解的文章就介绍完了。如果您还想了解更多内容,请在右上角搜索TOY模板网以前的文章或继续浏览下面的相关文章,希望大家以后多多支持TOY模板网!