ARM 内存屏障:DMB、DSB、ISB详解

1. 什么是内存屏障

1.1. 内存屏障的定义

内存屏障(Memory Barrier)是一类特殊指令,用来约束屏障前后指令的顺序、完成状态或生效时机。

1.2. 为什么需要内存屏障

ARM 采用弱内存模型(Weakly-ordered memory model),程序里的访存顺序和其他观察者(其他核、DMA、外设)看到的顺序可能不一致,原因有三层:

  • 编译器重排:编译器在不改变单线程语义的前提下调整指令顺序。
  • CPU 乱序执行:流水线、Store Buffer、推测执行、Write Combining 等都会改变访存的实际顺序。
  • 多核与缓存:写操作通常先进入缓存,不会立刻到达内存,不经过该缓存的观察者可能从内存中读到旧值。另外,多核时每个核有自己的 L1 缓存,写操作对其他核的可见时间和顺序也会存在不确定性。

观察者(Observer):指任何能发起内存访问、能"看到"内存内容的硬件单元,例如 CPU 核、DMA 控制器、GPU 和其他总线主设备(如网卡、存储控制器等)。
通常我们都会使用术语“观察者”来描述内存屏障的作用对象;内存屏障约束的正是这些观察者看到各次访存的先后顺序。


2. DMB、DSB、ISB 是什么

DMB、DSB、ISB 是 ARM 架构下的三条内存屏障指令,用来约束访存和指令执行的顺序与完成状态。

指令全称说明
DMBData Memory Barrier约束内存访问的观察顺序:保证按程序顺序位于 DMB 之前的显式内存访问,先于 DMB 之后的显式内存访问被指定共享域内的观察者观察到。
DSBData Synchronization Barrier保证相关内存操作完成后再继续执行:DSB 之后的指令必须等待 DSB 之前符合要求的显式内存访问,以及相关的 Cache、TLB、分支预测器等维护操作达到架构规定的完成条件后,才能执行。
ISBInstruction Synchronization Barrier冲刷流水线,使后续指令重新取指。

3. ARM 下内存屏障的具体实现

3.1. 指令格式

三条屏障指令的汇编语法:

DMB  <option>
DSB  <option>
ISB  {<option>}      // 只有一个有效选项 SY,可省略
3.1.1. option 取值

<option> 由共享域 + 访问类型组合而成。

option共享域访问类型说明
SY全系统load + store → load + store最强屏障,缺省值。用于需要对所有观察者(含非一致性设备)生效的场合
ST全系统store → store保证之前的 store 先于之后的 store 被系统内所有观察者看到
LD全系统load → load/store保证之前的 load 先于之后的任何访问
ISH同 OS 的所有核(Inner Shareable)load + store → load + store多核同步的通用选择
ISHST同 OS 的所有核(Inner Shareable)store → store多核间发布数据的写端屏障,开销小于 ISH
ISHLD同 OS 的所有核(Inner Shareable)load → load/store多核间读取数据的读端屏障
NSH仅本核(Non Shareable)load + store → load + store只保证本核观察顺序,不对其他观察者生效。
NSHST仅本核(Non Shareable)store → store仅本核的 store 顺序
NSHLD仅本核(Non Shareable)load → load/store仅本核的 load 顺序
OSHISH域 + 外部设备(Outer Shareable)load + store → load + storeCPU 与 DMA/GPU 交互的全序屏障
OSHSTISH域 + 外部设备(Outer Shareable)store → store保证 CPU 写入的数据/描述符先于后续写被设备(DMA/GPU)看到
OSHLDISH域 + 外部设备(Outer Shareable)load → load/store保证读完设备(DMA/GPU) 相关数据后再进行后续访问

注意: 表格中列出的 option 取值为 ARM 架构规范所定义的完整集合,但具体芯片未必全部支持。

load(加载): 从内存读数据到寄存器。ARM 中对应 LDR、LDAR 等指令。
store(存储): 把寄存器中的数据写到内存。ARM 中对应 STR、STLR 等指令。
屏障约束的正是这两类访存之间的先后顺序,所以访问类型里才会出现 LD(load)和 ST(store)。

  • 共享域
    "共享"指一块内存被哪些处理器或设备共同访问, "域"即这些访问者所构成的范围。屏障中的共享域用来限定顺序保证的作用范围: 只需要在真正需要的范围内保证访存顺序。因为,范围越小, 硬件需要等待和同步的对象越少, 屏障开销越低, 性能越好。
    在这里插入图片描述

  • 访问类型
    描述的是屏障前的哪类访问与屏障后的哪类访问之间保持顺序。例如 option 为 LD 时,表示屏障之前的 load,先于屏障之后的 load 和 store 被观察到。

  • 其他说明

    上述屏障指令适用于 ARMv7 和 ARMv8 架构。在 ARMv6 及更早的架构中, 没有专用的屏障指令, 需要通过协处理器 CP15 的操作来实现,具体如下所示:

    mcr p15, 0, r0, c7, c10, 5   @ DMB
    mcr p15, 0, r0, c7, c10, 4   @ DSB
    mcr p15, 0, r0, c7, c5, 4    @ ISB
    

3.2 内联汇编实现

为在C代码中便捷地插入ARM架构屏障指令,采用如下内联汇编宏封装。

#define dmb(opt) __asm__ volatile("dmb " #opt : : : "memory")
#define dsb(opt) __asm__ volatile("dsb " #opt : : : "memory")
#define isb()    __asm__ volatile("isb" : : : "memory")

3.3. CMSIS 实现

以 GCC 工具链 + Cortex-M 为例。

3.3.1 接口
__ISB();   // Instruction Synchronization Barrier
__DSB();   // Data Synchronization Barrier
__DMB();   // Data Memory Barrier

说明:Cortex-M 对应的 Armv6-M / Armv7-M / Armv8-M 架构只支持 SY,没有 ISH、ST、LD 等细分选项,所以接口不带参数。

3.3.2 CMSIS 底层实现

CMSIS 的 ARM 屏障底层实现本质上是内联汇编封装。典型实现形式如下:

//option 固定为 0xF,即 SY(全系统)

__STATIC_FORCEINLINE void __DMB(void)
{
    __ASM volatile ("dmb 0xF" ::: "memory");
}

__STATIC_FORCEINLINE void __DSB(void)
{
    __ASM volatile ("dsb 0xF" ::: "memory");
}

__STATIC_FORCEINLINE void __ISB(void)
{
    __ASM volatile ("isb 0xF" ::: "memory");
}

3.4. Linux 实现

以arch/arm64/include/asm/barrier.h为例。

#define sev()       asm volatile("sev" : : : "memory")
#define wfe()       asm volatile("wfe" : : : "memory")
#define wfi()       asm volatile("wfi" : : : "memory")

#define isb()       asm volatile("isb" : : : "memory")
#define dmb(opt)    asm volatile("dmb " #opt : : : "memory")
#define dsb(opt)    asm volatile("dsb " #opt : : : "memory")

/* 强制屏障:含设备访问 */
#define mb()        dsb(sy)
#define rmb()       dsb(ld)
#define wmb()       dsb(st)

/* DMA 屏障:普通内存 <-> 设备 */
#define dma_mb()    dmb(osh)
#define dma_rmb()   dmb(oshld)
#define dma_wmb()   dmb(oshst)

/* SMP 屏障:核间同步 */
#define smp_mb()    dmb(ish)
#define smp_rmb()   dmb(ishld)
#define smp_wmb()   dmb(ishst)

4. DMB:数据内存屏障

4.1. 作用

保证屏障前后的访存按序被观察到,不等待访存完成,不影响屏障后的非访存指令(比如算术运算)继续执行。

  1. 保证内存访问顺序:屏障之前的内存访问,在屏障之后的内存访问之前,对其他观察者可见。
  2. 限制乱序执行:阻止处理器、内存系统等将屏障前后的内存访问以违反屏障语义的顺序对外呈现。
  3. 不保证访问完成:DMB 不保证屏障之前的所有内存访问已经完成,只保证它们满足规定的顺序关系。
  4. 不刷新 Cache:DMB 不会主动清理 Cache,也不会将 Cache 中的数据强制写回 DDR。

4.2. 使用举例

场景1:生产者与消费者之间的内存访问同步

// 生产者:先写共享数据,再更新状态标志
share_buffer[0] = 0x12345678;
dmb(sy)  // 保证数据写入先于 ready 标志更新
ready = 1;

// 消费者:检测到 ready 后,再读取共享数据
if (ready == 1) {
    dmb(sy);  // 保证读取 ready 之后,再读取共享数据
    data = share_buffer[0];
}

//没有这两个 DMB,消费者可能看到 `ready=1` 但读到旧的 `data`。

场景2:多核 CPU 之间共享内存,进行完整的读写顺序同步

操作示例同场景1,区别在于屏障的共享域:多核之间只需约束同一 Inner Shareable 域内的观察者,使用dmb(ish) 即可。

场景 3:系统级外设访问顺序控制

例如先配置外设的 DMA 地址,再启动 DMA。此处的观察者包含片上外设,需要全系统范围的屏障,使用 sy:

DMA->ADDR = buffer_addr;

dmb(sy); //保证 DMA 地址配置操作先于 DMA 启动操作对外呈现

DMA->CTRL = DMA_START;

5. DSB:数据同步屏障

5.1. 作用

保证屏障之前的显式内存访问以及相关维护操作完成后,才允许屏障之后的指令继续执行。

  1. 保证内存访问完成:屏障之前的内存访问必须达到架构定义的完成状态。
  2. 保证后续指令执行:在屏障之前要求完成的操作完成之前,处理器不能继续执行屏障之后的指令。
  3. 同步 Cache 和 TLB 维护操作:保证之前发起的相关维护操作完成。
  4. 不等于 Cache 刷新:DSB 本身不执行 Cache Clean 或 Invalidate 操作。

5.2. 使用举例

场景 1:Cache 维护后启动 DMA

CleanInvalidateDCache();  // 执行 Cache Clean / Invalidate 等维护操作

dsb(sy);  // 等待 Cache 维护操作在全系统范围内完成 

/* 后续可以启动 DMA 读取该内存 */

场景 2:多核 CPU,TLB 维护后

页表修改的标准序列是:修改页表项 → dsb(ishst) → tlbi → dsb(ish) → isb()。

page_table[index] = new_descriptor; // 修改页表描述符

dsb(ishst);  // 仅需保证之前的 store(页表写入)先对其他核可见

__set_TLB_invalidate(); // 执行 TLB Invalidate

dsb(ish);    // 等待 TLB 维护操作在 Inner Shareable 域内全部完成

isb();       // 冲刷流水线,使后续指令使用新的地址翻译

场景 3:系统进入低功耗

writel_relaxed(PWR_REQ_SUSPEND, pmu_base + PMU_CTRL);  // 请求电源控制器掉电

dsb(sy);   // 确保之前所有访存(含设备写)完成 

wfi();     // 进入低功耗,等待唤醒事件

6. ISB:指令同步屏障

6.1. 作用

冲刷流水线,丢弃已预取和已译码的指令,使屏障之后的指令在上下文变更生效后重新取指和执行。

  1. 刷新指令流水线:使屏障之后的指令重新取指。
  2. 同步处理器执行上下文:确保后续指令基于屏障之前已经完成的系统控制状态变化执行。
  3. 确保控制寄存器修改生效于后续指令执行上下文:例如修改 SCTLR、TTBR 等系统控制寄存器之后。
  4. 不负责数据同步:ISB 不保证之前的内存访问完成,也不负责 Cache 数据清理。

6.2. 使用举例

场景 1:修改系统控制寄存器后

在修改系统寄存器系统接口实现函数内部都带有 isb()。

SCB->SCTLR |= SCTLR_Z_Msk; // 修改处理器系统控制寄存器

isb();  // 冲刷流水线,使后续指令在更新后的控制状态下执行

场景 2:TLB 维护后

见上面 DSB 场景 2,其中最后的 isb() 用于丢弃已经按旧地址翻译预取的指令,让后续指令重新取指。

场景 3:切换异常向量表后

extern char vectors[];

write_sysreg((unsigned long)vectors, vbar_el1);  // 设置新的异常向量表基址

isb();  // 保证此后发生的异常,一定使用新的向量表

7. 总结

  • ARM 采用弱内存模型:程序中的访问顺序不一定等同于其他观察者看到的顺序,因此需要通过内存屏障指令,在必要时约束内存访问和指令执行顺序。
  • DMB 管顺序,DSB 管完成,ISB 管指令同步:三者分别解决“内存访问顺序如何保证”,“屏障前的操作是否达到架构规定的完成条件”,“后续指令是否基于更新后的执行上下文执行”三个问题,不能互相替代。
  • 屏障指令不等于 Cache 操作:Cache 维护需要显式的维护指令,屏障指令(DSB) 只负责等待其完成。
  • 按需选择屏障选项:ARM 屏障指令可以通过 <option> 指定共享域(NSH / ISH / OSH / SY)和访问类型(无后缀 / ST / LD)。应根据观察者范围和访问类型选择合适的选项,避免不必要的同步开销。
Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐