内存屏障的硬件原理: 从 Store Buffer 到 ARM DMB/DSB/ISB

3 阅读18分钟

配套代码: DeguiLiu/newosp -- header-only C++17 嵌入式基础设施库

相关文章:

CSDN 原文: C++多线程编程中的内存屏障/内存栅栏

1. 为什么 CPU 会重排序

多数内存屏障教程直接从 std::memory_order 六个枚举值讲起,告诉你 acquire 防止后面的读写提前、release 防止前面的读写延后。但为什么 CPU 要重排序?这个问题的答案藏在两个硬件组件里: Store BufferInvalidation Queue

1.1 Store Buffer: 写操作的隐藏队列

现代 CPU 的 L1 Cache 访问延迟约 1-4 个时钟周期,但当写操作 cache miss 时,需要等待 MESI 协议完成 (获取 cache line 的独占权),延迟可达 几十到上百个周期。如果 CPU 每次写操作都阻塞等待缓存一致性完成,流水线将频繁停顿。

Store Buffer 的作用是让写操作立即完成: CPU 将写入值暂存到 Store Buffer,然后继续执行后续指令,无需等待缓存一致性协议完成。

CPU Core 0                          CPU Core 1
┌──────────┐                        ┌──────────┐
│ Pipeline  │                        │ Pipeline  │
│  执行 str │                        │  执行 ldr │
└────┬─────┘                        └────┬─────┘
     │ (1) 写入值暂存                     │ (4) 从 L1 Cache 读取
┌────▼─────┐                        ┌────▼─────┐
│  Store    │                        │ Invalidation│
│  Buffer   │ (2) 异步刷新 ──────→   │  Queue      │ (3) 延迟处理失效
└────┬─────┘                        └────┬─────┘
     │                                   │
┌────▼─────────────────────────────────────▼──────┐
│              L1 Cache / L2 Cache (MESI 协议)     │
└─────────────────────────────────────────────────┘

关键问题: Core 0 的写入暂存在 Store Buffer 中,对 Core 1 不可见。Core 1 从自己的 L1 Cache 中读到的仍然是旧值。这不是 bug,而是 CPU 为了性能做出的设计决策。

Store Buffer 还引入了一个微妙的行为 -- Store Forwarding: 当 Core 0 读取自己刚写过的地址时,会直接从 Store Buffer 中获取最新值,绕过 L1 Cache。这意味着同一个 CPU 核心看到的写入顺序与其他核心看到的不同

1.2 Invalidation Queue: 读操作的延迟

MESI 协议中,当一个核心要写入某条 cache line 时,需要向所有持有该 line 的其他核心发送 Invalidate 消息。接收方收到 Invalidate 后应该立即将对应 cache line 标记为 Invalid。

但如果接收方正在忙于其他操作 (流水线满载),立即处理 Invalidate 会导致停顿。因此硬件引入了 Invalidation Queue: 将收到的 Invalidate 消息排队,先回复 Acknowledge (让发送方继续),稍后再实际处理失效。

Core 1 收到 Invalidate(addr=0x1000):
  ┌──────────────────────────────┐
  │ 1. 将 Invalidate 消息入队     │
  │ 2. 立即回复 Ack (让 Core 0    │
  │    认为失效已完成)             │
  │ 3. 稍后处理: 将 cache line    │
  │    标记为 Invalid              │
  └──────────────────────────────┘

  在步骤 2 和 3 之间,Core 1 仍然
  可以从自己的 L1 Cache 读到旧值!

关键问题: Core 1 已经回复了 Ack,Core 0 认为其他核心都已经看到了自己的写入,但 Core 1 实际上还在用旧的 cache line。这就是过期读 (stale read) 的硬件根源。

1.3 两个队列,两种乱序

Store Buffer 和 Invalidation Queue 分别导致了两种可见性问题:

硬件组件导致的问题影响
Store Buffer写操作延迟对外可见其他核心看不到最新写入
Invalidation Queue读操作使用过期数据本核心看到的是失效前的旧值

这两个机制的组合使得多核系统中,内存操作的执行顺序可能与程序顺序不同。这不是编译器优化,而是硬件行为 -- 即使你用 volatile 禁止编译器优化,CPU 仍然可能重排序。

2. MESI 协议: 缓存一致性不等于内存一致性

2.1 四种状态

MESI 协议是多核 CPU 维护缓存一致性的标准协议。每条 cache line 有四种状态:

状态含义可读可写其他核心状态
M (Modified)已修改,与内存不一致无 (独占)
E (Exclusive)独占,与内存一致是 (→M)无 (独占)
S (Shared)共享,与内存一致否 (需先 Invalidate)多核共享
I (Invalid)无效-

2.2 一致性 vs 一致性

MESI 保证的是 Cache Coherence (缓存一致性): 对同一地址的所有写入,所有核心最终看到相同的值和相同的顺序。但它不保证 Memory Consistency (内存一致性): 不同地址的写入顺序在不同核心上的可见顺序可能不同。

// 初始: x = 0, y = 0

// Core 0                    // Core 1
x = 1;  // (1)               y = 1;  // (3)
r1 = y; // (2)               r2 = x; // (4)

// 可能的结果: r1 == 0 && r2 == 0

这个经典的 Store Buffer Litmus Test 在 ARM 上可以真实发生:

  • Core 0 执行 (1): x=1 进入 Core 0 的 Store Buffer (对 Core 1 不可见)
  • Core 1 执行 (3): y=1 进入 Core 1 的 Store Buffer (对 Core 0 不可见)
  • Core 0 执行 (2): 读 y,从 L1 Cache 读到旧值 0
  • Core 1 执行 (4): 读 x,从 L1 Cache 读到旧值 0

MESI 保证 x 最终为 1、y 最终为 1,但不保证 Core 0 在写 x 后立即看到 Core 1 写的 y。

这就是内存屏障的存在意义: 强制 Store Buffer 刷新或 Invalidation Queue 处理,使特定的内存操作按程序顺序对其他核心可见。

3. 四种屏障类型

根据 Store Buffer 和 Invalidation Queue 的组合,内存屏障被分为四种基本类型:

3.1 StoreStore Barrier

Store A
--- StoreStore ---
Store B

保证: Store A 在 Store B 之前刷出 Store Buffer,即其他核心先看到 A 的写入,再看到 B 的写入。

硬件机制: 标记 Store Buffer 中的当前条目,后续的 Store 必须等待这些条目刷新到缓存后才能继续。

典型场景: 生产者写数据,然后写 flag 通知消费者。StoreStore 保证消费者看到 flag=1 时,数据已经可见。

// 生产者 (Core 0)
data = 42;            // Store A
// --- StoreStore ---
flag = 1;             // Store B

// 消费者 (Core 1)
while (flag != 1);    // 看到 flag=1 时
use(data);            // data 保证是 42

3.2 LoadLoad Barrier

Load A
--- LoadLoad ---
Load B

保证: Load A 完成后再执行 Load B,Load B 不会使用比 Load A 更旧的数据。

硬件机制: 处理 Invalidation Queue 中的所有待处理失效消息,确保后续 Load 读到最新值。

典型场景: 消费者先读 flag,再读 data。LoadLoad 保证读到 flag=1 后,读 data 不会命中过期的 cache line。

3.3 LoadStore Barrier

Load A
--- LoadStore ---
Store B

保证: Load A 完成后再执行 Store B。防止 Store 被提前到 Load 之前执行。

使用较少: x86 TSO 天然禁止 LoadStore 重排,ARM 弱序需要。

3.4 StoreLoad Barrier (Full Fence)

Store A
--- StoreLoad ---
Load B

保证: Store A 对所有核心可见后再执行 Load B。这是最强也是最昂贵的屏障。

硬件机制: 刷新 Store Buffer 中的所有条目 + 处理 Invalidation Queue 中的所有消息。相当于 StoreStore + LoadLoad + LoadStore 的组合。

为什么最贵: Store Buffer 刷新需要等待 MESI 协议完成 (可能需要等待远端核心的 Ack),这个延迟通常是几十到上百个时钟周期。

3.5 四种屏障与硬件机制的映射

屏障类型作用于硬件效果
StoreStoreStore Buffer刷新已有条目后才允许新 Store
LoadLoadInvalidation Queue处理待失效消息后才允许新 Load
LoadStore两者Load 完成后才允许 Store 提交
StoreLoad两者完全刷新 Store Buffer + Invalidation Queue

4. 三个层次: 编译器屏障、硬件屏障、C++ memory_order

代码到硬件之间有三层可能的重排序,需要三个层次的屏障:

         源代码
           │
     ┌─────▼─────┐
     │ 编译器优化  │ ← 编译器屏障 (阻止编译器重排)
     └─────┬─────┘
           │
     ┌─────▼─────┐
     │ CPU 乱序   │ ← 硬件屏障 (阻止 CPU 重排)
     │ 执行引擎   │
     └─────┬─────┘
           │
     ┌─────▼─────┐
     │ Store Buffer│ ← 内存屏障 (强制刷新/排空)
     │ Inv. Queue  │
     └─────┬─────┘
           │
         内存

4.1 编译器屏障

编译器屏障只阻止编译器重排序指令,不生成任何硬件屏障指令。

// GCC/Clang 内联汇编编译器屏障
asm volatile("" ::: "memory");

// C++11 标准方式
std::atomic_signal_fence(std::memory_order_acq_rel);

asm volatile("" ::: "memory") 的含义:

  • "": 空指令 (不生成机器码)
  • volatile: 不被编译器消除
  • "memory": 告诉编译器内存已被修改,不要跨越此点重排内存操作

适用场景: 单核 MCU (无 CPU 乱序执行问题),或仅需要阻止编译器优化时。atomic_signal_fence 用于同一线程中信号处理函数与主线程的同步。

// newosp SPSC 在 FakeTSO 单核模式下:
// 只用编译器屏障替代硬件屏障 (零硬件开销)
#ifdef OSP_FAKE_TSO
  std::atomic_signal_fence(std::memory_order_release);  // 编译器屏障
#else
  std::atomic_thread_fence(std::memory_order_release);  // 硬件屏障
#endif

详见 SPSC 无锁环形缓冲区设计剖析 中的 FakeTSO 机制。

4.2 硬件屏障

硬件屏障生成实际的 CPU 屏障指令,阻止 CPU 乱序执行和 Store Buffer/Invalidation Queue 的延迟效应。

// C++11 标准方式 (独立屏障)
std::atomic_thread_fence(std::memory_order_acquire);   // LoadLoad + LoadStore
std::atomic_thread_fence(std::memory_order_release);   // LoadStore + StoreStore
std::atomic_thread_fence(std::memory_order_acq_rel);   // 以上全部
std::atomic_thread_fence(std::memory_order_seq_cst);   // Full fence (含 StoreLoad)

在 ARM 上的编译结果:

; atomic_thread_fence(acquire)  →  dmb ishld   (Load barrier)
; atomic_thread_fence(release)  →  dmb ish     (Full barrier)
; atomic_thread_fence(seq_cst)  →  dmb ish     (Full barrier)

注意: ARM 没有单独的 StoreStore 屏障指令,releaseseq_cst 都映射到 dmb ish (完全数据屏障)。这意味着 ARM 上 release fence 的开销与 seq_cst fence 相同。

4.3 C++ memory_order: 绑定到原子操作的屏障

C++11 定义了六种 memory_order,它们不是独立屏障,而是附加在原子操作上的排序约束:

memory_order语义组合的屏障效果
relaxed仅保证原子性无屏障
consume数据依赖序 (deprecated)理论上比 acquire 弱
acquire后续读写不提前到此 load 之前LoadLoad + LoadStore
release前面读写不延后到此 store 之后LoadStore + StoreStore
acq_relacquire + releaseLoadLoad + LoadStore + StoreStore
seq_cst全序 (所有线程看到相同顺序)Full fence (含 StoreLoad)

独立屏障 vs 原子操作上的 memory_order:

// 方式一: 独立屏障 (atomic_thread_fence)
data.store(42, std::memory_order_relaxed);
std::atomic_thread_fence(std::memory_order_release);
flag.store(1, std::memory_order_relaxed);

// 方式二: 原子操作附加 memory_order (更常用)
data.store(42, std::memory_order_relaxed);
flag.store(1, std::memory_order_release);  // release 语义附加在 flag 的 store 上

两种方式在 ARM 上生成的指令几乎相同,但方式二更简洁。C++ 标准推荐在原子操作上直接指定 memory_order,仅在需要与非原子操作建立 happens-before 关系时使用独立屏障。

4.4 三个层次总结

层次机制阻止的重排序硬件开销
编译器屏障asm volatile("" ::: "memory") / atomic_signal_fence仅编译器重排 (无机器指令)
硬件屏障atomic_thread_fence / 内联汇编编译器 + CPU 重排DMB/DSB 指令 (几十周期)
原子操作 memory_orderatomic.store/load(order)编译器 + CPU 重排 (绑定到特定操作)取决于 order 级别

5. x86 vs ARM: 强序与弱序

5.1 x86 TSO (Total Store Order)

x86 实现了 TSO (Total Store Order) 模型,这是一种接近顺序一致性的强序模型:

  • 每个核心的 Store 按程序顺序对所有核心可见 (StoreStore 天然保证)
  • 每个核心的 Load 按程序顺序执行 (LoadLoad 天然保证)
  • Load 不会被重排到 Store 之后 (LoadStore 天然保证)
  • 唯一允许的重排: Store 可以被后续的 Load 越过 (StoreLoad 可重排)
x86 允许的重排序:
  Store ALoad B   可能变成   Load BStore A   (StoreLoad 重排)

x86 禁止的重排序:
  Store AStore B  (StoreStore ✓ 保序)
  Load ALoad B   (LoadLoad ✓ 保序)
  Load AStore B  (LoadStore ✓ 保序)

因此,x86 上大部分 acquirerelease 操作不需要生成屏障指令 -- 硬件已经提供了足够的保证。只有 seq_cst 的 store 操作需要额外的 MFENCEXCHG 指令来阻止 StoreLoad 重排。

这也是为什么很多并发 bug 在 x86 上不会复现,却在 ARM 上崩溃 -- x86 的 TSO 掩盖了缺失的内存屏障。

5.2 ARM 弱序模型

ARM 实现了 弱序 (Weakly Ordered) 内存模型,四种重排序都可能发生:

ARM 允许的重排序:
  Store → Store   (StoreStore 可重排) ✗
  Load  → Load    (LoadLoad 可重排)   ✗
  Load  → Store   (LoadStore 可重排)  ✗
  Store → Load    (StoreLoad 可重排)  ✗

x86 上只有最后一种可重排

这意味着 ARM 上每个需要排序保证的操作都必须显式插入屏障指令。编译器在生成 ARM 代码时,会根据 memory_order 自动插入必要的 DMB 指令。

5.3 C++ memory_order 在两种架构上的代价

memory_orderx86 (TSO)ARM (弱序)
relaxed无额外指令无额外指令
acquire (load)无额外指令dmb ishld (或 ldar)
release (store)无额外指令dmb ish (或 stlr)
seq_cst (store)MFENCEXCHGdmb ish
seq_cst (load)无额外指令ldar + dmb ish

嵌入式实践: 在 ARM 上,acquire/releaseseq_cst 的实际开销差异取决于具体微架构,但原则是用最弱的足够的 memory_orderrelaxed 最快 (零开销),seq_cst 最慢 (完全屏障)。

// 嵌入式 SPSC: 只需要 acquire/release (不需要 seq_cst)
// 生产者
buffer[head] = data;
head_.store(new_head, std::memory_order_release);  // ARM: stlr 或 dmb ish + str

// 消费者
auto h = head_.load(std::memory_order_acquire);    // ARM: ldar 或 ldr + dmb ishld
auto data = buffer[h];

详见 SPSC 无锁环形缓冲区设计剖析共享内存进程间通信 中的 ARM 内存序实战分析。

6. ARM 三条屏障指令

ARM 提供三条屏障指令,语义精确且不可互换:

6.1 DMB (Data Memory Barrier)

DMB ISH      ; 全屏障: 在此之前的所有内存访问完成后, 才允许之后的内存访问
DMB ISHLD    ; Load 屏障: 在此之前的所有 Load 完成后, 才允许之后的 Load/Store
DMB ISHST    ; Store 屏障: 在此之前的所有 Store 完成后, 才允许之后的 Store

语义: 确保 DMB 之前和之后的数据内存访问的顺序性。DMB 只影响内存访问指令 (Load/Store),不影响其他指令的执行。

后缀含义:

  • ISH (Inner Shareable): 作用于内部共享域 (通常是所有 CPU 核心)
  • OSH (Outer Shareable): 作用于外部共享域 (含 DMA 控制器等)
  • SY (System): 作用于整个系统
  • LD: 仅限 Load 操作
  • ST: 仅限 Store 操作

C++ 映射:

memory_order_acquire  →  DMB ISHLD  (Load 屏障)
memory_order_release  →  DMB ISH    (Full 屏障, ARM 无单独 StoreStore)
memory_order_seq_cst  →  DMB ISH    (Full 屏障)

代价: DMB 的延迟在 ARM Cortex-A 系列上通常为 20-60 个时钟周期 (取决于 Store Buffer 深度和缓存一致性延迟)。

6.2 DSB (Data Synchronization Barrier)

DSB ISH      ; 等待之前的所有内存访问完成, 且对其他核心可见
DSB ISHST    ; 等待之前的所有 Store 完成并对其他核心可见

语义: DSB 比 DMB 更强。DMB 只保证顺序性 (A 在 B 之前完成),DSB 保证完成性 (A 已经完成,所有核心都已看到结果)。DSB 之后的任何指令 (不仅是内存访问) 都不会执行,直到 DSB 之前的所有内存访问完成。

DMB vs DSB 的区别:

DMB: "屏障之前的内存操作先于屏障之后的内存操作"
DSB: "屏障之前的内存操作全部完成后,才执行屏障之后的任何指令"
特性DMBDSB
保证内存操作顺序
等待内存操作完成
阻塞后续非内存指令
典型延迟20-60 周期更高 (需等待写入对所有核心可见)

适用场景: 修改页表、修改 MMU 配置、DMA 操作完成确认等需要确保写入已完成 (而非仅已排序) 的场景。

// 修改页表后必须用 DSB (而非 DMB)
modify_page_table_entry();
asm volatile("dsb ish" ::: "memory");  // 等待页表修改对所有核心可见
asm volatile("isb" ::: "memory");      // 刷新指令流水线

6.3 ISB (Instruction Synchronization Barrier)

ISB SY       ; 刷新指令流水线

语义: 刷新 CPU 的指令流水线和预取缓冲。ISB 之后获取的所有指令都从缓存或内存中重新获取,确保 ISB 之前的系统寄存器修改 (如 MMU 配置、中断控制器配置) 对后续指令可见。

ISB 与 DMB/DSB 的本质区别: DMB 和 DSB 作用于数据流 (Load/Store),ISB 作用于指令流 (指令预取和解码)。

适用场景:

  • 修改 SCTLR (系统控制寄存器) 后刷新流水线
  • 使能/关闭 MMU 后
  • 修改中断向量表后
  • 自修改代码 (Self-Modifying Code) 后
// 自修改代码: 修改内存中的指令后
write_new_instruction(addr);
asm volatile("dsb ish" ::: "memory");  // 确保新指令写入完成
asm volatile("isb" ::: "memory");      // 刷新流水线, 使新指令生效

6.4 选择指南

需要保证数据访问顺序?        → DMB (最轻量, C++ atomic 首选)
需要确保数据写入已完成?      → DSB (页表/DMA/MMIO 场景)
需要刷新指令流水线?          → ISB (系统寄存器/自修改代码)

嵌入式开发中的使用频率: C++ std::atomicatomic_thread_fence 只会生成 DMB,不会生成 DSBISB。后两者是系统级编程 (内核、Bootloader、BSP) 的工具,应用层代码几乎不会直接使用。

7. 实战: 内存屏障如何保护无锁数据结构

7.1 生产者-消费者 (Acquire-Release)

这是最常见的内存屏障使用模式,也是 SPSC 环形缓冲区的核心:

// 生产者 (写数据 → release store flag)
void produce(T data) {
    buffer[slot] = data;                              // (1) 普通 Store
    head.store(new_head, std::memory_order_release);  // (2) Release Store
}

// 消费者 (acquire load flag → 读数据)
bool consume(T& out) {
    auto h = head.load(std::memory_order_acquire);    // (3) Acquire Load
    if (h == tail) return false;
    out = buffer[tail];                               // (4) 普通 Load
    return true;
}

ARM 生成的指令:

; 生产者
str   r1, [buffer, slot]    ; (1) 普通 Store: data
dmb   ish                   ; release fence
str   r2, [head]            ; (2) Store: new_head

; 消费者
ldr   r3, [head]            ; (3) Load: head
dmb   ishld                 ; acquire fence
ldr   r4, [buffer, tail]    ; (4) 普通 Load: data

dmb ish 保证 (1) 在 (2) 之前对消费者可见; dmb ishld 保证 (3) 在 (4) 之前完成。两者配合,消费者看到 head 更新时,data 一定已写入。

7.2 DCLP (Double-Checked Locking Pattern)

C++ 单例模式的线程安全实现 详细分析了 DCLP 在 C++03 中失败的原因。其核心就是缺少内存屏障:

// C++11 正确的 DCLP
Singleton* Singleton::getInstance() {
    auto* p = instance.load(std::memory_order_acquire);  // (1) acquire
    if (!p) {
        std::lock_guard<std::mutex> lock(mtx);
        p = instance.load(std::memory_order_relaxed);    // (2) 在锁内 relaxed 即可
        if (!p) {
            p = new Singleton();
            instance.store(p, std::memory_order_release); // (3) release
        }
    }
    return p;
}

(3) 的 release 保证 new Singleton() 的所有构造操作在指针发布之前完成; (1) 的 acquire 保证其他线程通过指针访问对象时,看到完全构造的状态。

7.3 newosp FakeTSO: 单核优化

在单核 MCU (Cortex-M 系列) 上,没有多核缓存一致性问题,硬件屏障指令是纯粹的浪费。newosp 的 SPSC 环形缓冲区提供了 FakeTSO 编译选项:

// 单核模式: 所有 atomic_thread_fence 降级为 atomic_signal_fence
// 效果: 零硬件屏障指令, 仅阻止编译器重排序
#define OSP_FAKE_TSO 1

这是编译器屏障与硬件屏障的典型工程权衡:

  • 多核 ARM: 必须用 atomic_thread_fence → 生成 DMB
  • 单核 MCU: 只需 atomic_signal_fence → 零硬件开销

详见 SPSC 无锁环形缓冲区设计剖析 中的 FakeTSO 分析。

8. 常见误区

误区一: volatile = 内存屏障

volatile 只阻止编译器对该变量的优化 (不消除读写、不合并、不重排同一 volatile 变量的操作)。它不阻止:

  • 编译器对 volatile 和非 volatile 操作之间的重排
  • CPU 的乱序执行和 Store Buffer 延迟
volatile int flag = 0;
int data = 0;

// 线程 A
data = 42;      // 编译器可能将这条重排到 flag=1 之后
flag = 1;       // volatile 不保护 data 和 flag 之间的顺序

// 正确做法: 用 atomic + memory_order
std::atomic<int> flag{0};
data = 42;
flag.store(1, std::memory_order_release);  // data 写入在 flag 之前可见

误区二: seq_cst 最安全,应该到处用

seq_cst 是最强的排序保证,但也是最昂贵的。在 ARM 上,每个 seq_cst 操作都会生成 DMB 指令。如果代码中有大量 atomic 操作 (如无锁队列的 CAS 循环),过度使用 seq_cst 会显著降低性能。

原则: 分析数据流向,使用最弱的足够的 memory_order:

  • 单纯的计数器/统计: relaxed
  • 生产者-消费者: acquire/release
  • 需要全序 (如 Dekker 互斥算法): seq_cst

误区三: 在 x86 上测试通过 = 内存序正确

x86 的 TSO 模型天然保证了大部分排序 (只有 StoreLoad 可重排)。一段代码在 x86 上跑了一年没出问题,移植到 ARM 后可能立即出现数据竞争。

实践建议: 使用 ThreadSanitizer (-fsanitize=thread) 检测数据竞争,不依赖特定架构的内存模型。

参考资料

  1. C++多线程编程中的内存屏障/内存栅栏 -- 本文的 CSDN 原始版本
  2. Memory Barriers: a Hardware View for Software Hackers -- Paul McKenney 经典论文
  3. A Tutorial Introduction to the ARM and POWER Relaxed Memory Models -- ARM 弱序模型形式化教程
  4. Herb Sutter: atomic<> Weapons -- C++ 内存模型与硬件
  5. Jeff Preshing: Memory Barriers Are Like Source Control Operations -- 四种屏障类型的直觉解释
  6. ARM Architecture Reference Manual -- DMB/DSB/ISB 官方规范
  7. newosp GitHub 仓库 -- SPSC/MPSC 无锁实现中的内存序实战