【ARM Cache 入门及渐进五--内存屏障ISB/DSB/DMB】

文章目录

  • Cache 之内存屏障指令
    • 1.1 内存屏障基本规则
    • 1.2 DMB(数据存储屏障)
      • 1.2.1 DMB 使用场景
    • 1.3 DSB(数据同步屏障)
      • 1.3.1 DSB 使用背景
      • 1.3.2 LDREX/STREX 机制
      • 1.3.3 DSB互斥锁使用场景
    • 1.4 ISB(指令同步隔离)
      • 1.4.1 DMB/DSB/ISB 关系
      • 1.4.2 ISB 使用场景

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重新从高速缓存/内存中预取指令。

参考:
https://blog.csdn.net/qq_42174306/article/details/124716782
https://blog.csdn.net/leekay123/article/details/110678403
https://www.bbsmax.com/A/GBJrVxoRJ0/

你可能感兴趣的:(#,ARM,CPU,Cache,linux,arm,缓存)