请阅读【ARM Cache 及 MMU/MPU 系列文章专栏导读】
及【嵌入式开发学习必备专栏】
文章目录
上篇文章:ARM Cache 系列文章 4 – Cache 与 CPU 乱序执行
Cache 之内存屏障指令
1.1 内存屏障基本规则
- 所有内存屏障指令之前的数据访问必须在该指令之前完成。
- 所有内存屏障指令之后的数据必须等待该指令之后执行。
- 如果有多条内存屏障指令,它们是按照顺序执行的。
1.2 DMB(数据存储屏障)
DMB指令保证DMB指令前后的内存访问指令的执行次序,内存访问包括load、store、data cache维护指令, 像add指令DMB是管不到的。
1.2.1 DMB 使用场景
DMB不能保证它前面的指令在它后面的指令之前完成,只能保证后面的指令能观察到前面的指令执行了(即在LSU的执行顺序是能得到保证的),可能你会问这样的屏障有什么实际的作用呢?
举个例子,如果dmb前后的两次访存指令访问的都是同一个终点(比如都是ddr,或者都是spi的fifo),它在能保证两次访问放射顺序的同时,其实就能保证实际的完成顺序了。因为我们访问的是同一个终点,访问路径是一致的,在这种背景下谁先执行谁就先完成。在这种场景下使用DMB相对于DSB就能提升性能。
1.3 DSB(数据同步屏障)
- DSB指令比DMB指令严格很多;
- 在DSB之后的任何指令,必须等到如下完成了才能开始执行:
- 在DSB指令前面的所有数据访问必须执行完成。
- 在DSB之前的cache、branch predictor、TLB等指令必须执行完
1.3.1 DSB 使用背景
为了实现线程间同步,一般都要在执行关键代码段之前加互斥(Mutex)锁,且在执行完关键代码段之后解锁。为了实现所谓的互斥锁的概念,需要引入称作Load-Link(LL)和Store-Conditional(SC)的操作,通常简称为LL/SC。LL操作返回一个内存地址上当前存储的值,后面的SC操作,会向这个内存地址写入一个新值,但是只有在这个内存地址上存储的值,从上个LL操作开始直到现在都没有发生改变的情况下,写入操作才能成功,否则都会失败。这个操作非常重要,是很多平台实现基本原子操作的基础。 对于ARM平台来说,也在硬件层面上提供了对LL/SC的支持,LL操作用的是LDREX指令,SC操作用的是STREX指令。
1.3.2 LDREX/STREX 机制
LDREX 和STREX指令,是将单纯的更新内存的原子操作分成了两个独立的步骤。
1)LDREX用来读取内存中的值,并标记对该段内存的独占访问:
LDREX Rx, [Ry]
上面的指令意味着,读取寄存器 Ry 指向的4字节内存值,将其保存到 Rx 寄存器中,同时标记对 Ry 指向内存区域的独占访问。
如果执行 LDREX 指令的时候发现已经被标记为独占访问了,并不会对指令的执行产生影响。
2)而 STREX 在更新内存数值时,会检查该段内存是否已经被标记为独占访问,并以此来决定是否更新内存中的值:
STREX Rx, Ry, [Rz]
如果执行这条指令的时候发现已经被标记为独占访问了,则将寄存器 Ry 中的值更新到寄存器Rz指向的内存,并将寄存器Rx 设置成 0。指令执行成功后,会将独占访问标记位清除。
执行处理器如果在执行这条指令的时候发现没有设置独占标记(可能别的处理器已经成功更新了该段独占访问内存值),则不会再更新这段内存,且将寄存器Rx的值设置成1。在mutex中可以理解为抢锁失败。
1.3.3 DSB互斥锁使用场景
...
LOCKED EQU 1
UNLOCKED EQU 0
lock_mutex
; 互斥量是否锁定?
LDREX r1, [r0] ; 检查是否锁定
CMP r1, #LOCKED ; 和 "LOCKED" 比较
WFEEQ ; 如果上面一条比较指令的结果是相等,则表示互斥量已经锁定,此时进入休眠
BEQ lock_mutex ; 被唤醒,重新检查互斥量是否锁定
; 尝试锁定互斥量
MOV r1, #LOCKED
STREX r2, r1, [r0] ; 尝试锁定
CMP r2, #0x0 ; 检查STREX指令是否成功完成,为0则拿锁成功
BNE lock_mutex ; STREX执行结果为1,表示拿锁失败(可能被别的线程抢先一步),重试
DMB ; 进入被保护的资源前需要隔离,保证互斥量已经被更新
BX lr
unlock_mutex
DMB ; 保证资源的访问已经结束
MOV r1, #UNLOCKED ; 向锁定域写 "UNLOCKED"
STR r1, [r0]
DSB ; 保证在CPU唤醒前完成互斥量状态更新
SEV ; 向其他CPU发送事件,唤醒任何等待事件的CPU
BX lr
1.4 ISB(指令同步隔离)
确保所有在ISB指令之后的指令都从指令高速缓存或内存中重新预取。它刷新流水线(flush pipeline)和预取缓冲区后才会从指令高速缓存或者内存中预取ISB指令之后的指令。
ISB指令保证:
- isb后面的指令都从cache或者内存中重新获取。为什么需要重新获取呢?
- isb指令之前的更改上下文的操作都已经完成, 包括Cache,TLB 和 Branch Predictor等操作。改变系统的寄存器等。
1.4.1 DMB/DSB/ISB 关系
- DMB 与 DSB 的区别在于DMB可以继续执行之后的指令,只要这条指令不是内存访问指令;
- DSB不管它后面的什么指令,都会强迫CPU等待它之前的指令执行完毕;
- ISB不仅做了DSB所做的事情,还将流水线清空。
1.4.2 ISB 使用场景
ISB会冲刷流水线,然后从指令高速缓存或者内存中重新预取指令。因为编程中较少使用该指令,简单举一例如下:
/** Enable the MPU.
* \param MPU_Control Default access permissions for unconfigured regions.
*/
__STATIC_INLINE void ARM_MPU_Enable(uint32_t MPU_Control)
{
MPU->CTRL = MPU_Control | MPU_CTRL_ENABLE_Msk;
#ifdef SCB_SHCSR_MEMFAULTENA_Msk
SCB->SHCSR |= SCB_SHCSR_MEMFAULTENA_Msk;
#endif
__DSB();
__ISB();
}
DSB是保证此前的指令都已执行完,且其他观察者都已经收到广播且回了ack;
紧接着的ISB再让当前CPU重新从高速缓存/内存中预取指令。
上篇文章:ARM Cache 系列文章 4 – Cache 与 CPU 乱序执行
参考:
https://blog.csdn.net/qq_42174306/article/details/124716782
https://blog.csdn.net/leekay123/article/details/110678403
https://www.bbsmax.com/A/GBJrVxoRJ0/