为什么我们需要读写屏障?

Why do we need both read and write barriers?

为什么我们需要定义两种具有相同实现的障碍?

For example, this code from io_uring in Linux:

#if defined(__x86_64) || defined(__i386__)
#define read_barrier()  __asm__ __volatile__("":::"memory")
#define write_barrier() __asm__ __volatile__("":::"memory")
#else

它们在 x86 上碰巧是相同的,但在其他体系结构上它们可能会有所不同。因此,为了使代码可移植,即使是 x86 也需要单独的宏。

真正的答案是:因为 x86 的内存模型已经足够强大,阻塞 compile-time reordering 足以满足加载或存储排序;运行时重新排序已被硬件阻止。

这些只是通过一段内联汇编形成的通用编译时障碍,如果使用的话,它们会阻止 GCC 重新排序指令。解释得很好in this other post。使用此 "trick" 可以实现的功能通常也可以使用 C volatile 限定符实现。

请注意,Linux 内核不会在代码中的任何地方使用那些特定的宏,它们只是为 io_uring 用户空间测试工具定义的两个宏。 It definitely uses asm volatile ("" ::: "memory") 在需要的地方,但名称不同(例如 smp_rmb()smp_wmb())。

x86 的内存模型使得 sfencelfence 对于 CPU 之间的通信完全无用;阻止编译时重新排序就足够了:参见

smp_mb() 是一个完整的障碍,确实需要一个实际的 asm 指令,以及阻止编译时重新排序。


x86 确实有一些内存屏障 asm 指令用于只读和只写 "real"(运行时)内存屏障。它们是 sfence(存储栅栏)、lfence(加载栅栏)和 mfence(内存栅栏 = 满栅栏)。

mfence 序列化读取和写入(完全屏障),而其他只序列化两者之一(读取或写入 a.k.a 加载或存储)。 The wikipedia page 关于内存排序在解释这些含义方面做得不错。 lfence 实际上会阻止 LoadStore 重新排序,而不仅仅是 LoadLoad,用于来自 WC 内存的弱排序 movntdqa 加载。已经禁止从其他内存类型重新排序其他类型的负载,因此几乎没有任何理由实际使用 lfence 进行内存排序,而不是阻止乱序执行的其他效果。

内核在 I/O 代码中使用那些实际的 asm 指令作为内存屏障,例如 mb()rmb()wmb() which expand exactly to mfence, lfence, sfence, and others (example)。 =45=]

sfencelfence 在大多数情况下可能有点矫枉过正,例如围绕 MMIO 到强序 UC 内存。写入 WC 内存实际上可能需要一个 sfence。但与 I/O 相比它们并不太慢,并且在某些情况下可能会出现问题,因此 Linux 采取了安全的方法。

除此之外,x86 有不同类型的 read/write 障碍,它们也可能更快(例如我上面链接的那个)。有关使用 mfence 或虚拟 locked 指令的完整障碍(C11 称为顺序一致性)的更多信息,请参阅以下答案:

  • Which is a better write barrier on x86: lock+addl or xchgl?
  • What does the "lock" instruction mean in x86 assembly?
  • How does Linux kernel flush_write_buffers() work on x86?