为什么我们需要读写屏障?
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 的内存模型使得 sfence
和 lfence
对于 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=]
sfence
和 lfence
在大多数情况下可能有点矫枉过正,例如围绕 MMIO 到强序 UC 内存。写入 WC 内存实际上可能需要一个 sfence。但与 I/O 相比它们并不太慢,并且在某些情况下可能会出现问题,因此 Linux 采取了安全的方法。
除此之外,x86 有不同类型的 read/write 障碍,它们也可能更快(例如我上面链接的那个)。有关使用 mfence
或虚拟 lock
ed 指令的完整障碍(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?
为什么我们需要定义两种具有相同实现的障碍?
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 的内存模型使得 sfence
和 lfence
对于 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=]
sfence
和 lfence
在大多数情况下可能有点矫枉过正,例如围绕 MMIO 到强序 UC 内存。写入 WC 内存实际上可能需要一个 sfence。但与 I/O 相比它们并不太慢,并且在某些情况下可能会出现问题,因此 Linux 采取了安全的方法。
除此之外,x86 有不同类型的 read/write 障碍,它们也可能更快(例如我上面链接的那个)。有关使用 mfence
或虚拟 lock
ed 指令的完整障碍(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?