【问题标题】:Why do we need both read and write barriers?为什么我们需要读写屏障?
【发布时间】:2020-04-19 16:17:43
【问题描述】:

为什么我们需要用相同的实现定义两种类型的障碍?

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

【问题讨论】:

    标签: c gcc linux-kernel x86 memory-barriers


    【解决方案1】:

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

    这些只是通过一段内联程序集形成的通用编译时障碍,如果使用,它会阻止 GCC 重新排序指令。解释得很好in this other post。使用此“技巧”可以实现的目标通常也可以使用 C volatile 限定符来实现。

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

    x86 的内存模型使sfencelfence 完全无法用于CPU 之间的通信;阻塞编译时重新排序就足够了:参见Does the Intel Memory Model make SFENCE and LFENCE redundant?

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


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

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

    内核将这些实际的 asm 指令用于 I/O 代码中的内存屏障,例如 mb()rmb()wmb() which expand exactly to mfence, lfence, sfence 以及其他 (example)。

    sfencelfence 在大多数情况下可能是多余的,例如从 MMIO 到强排序的 UC 内存。写入 WC 内存实际上可能需要一个 sfence。但与 I/O 相比,它们并不算太慢,而且在某些情况下可能会出现问题,因此 Linux 采用了安全的方法。

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

    【讨论】:

    • 除非内核执行任何movnti 存储,否则它不需要sfence。除非它从 WC 内存加载任何movntdqa SSE4.1,否则它不需要lfence 来进行内存排序。 Does the Intel Memory Model make SFENCE and LFENCE redundant?(剧透警告:是的)。阻塞编译时重新排序为您提供了底层的 asm 内存模型。在 x86 上,这是程序顺序 + 带有存储转发的存储缓冲区,它比 C++11 acq_rel 更强。 mfence 仅在需要完整屏障的极少数情况下才需要。
    • Linux 真正的wmb()rbm() 供内核内部使用的宏最终在x86 上扩展为asm("":::"memory") 以阻止StoreStore 或LoadLoad(和LoadStore)重新排序,即获取发布/获取(不是 seq_cst) 语义在 volatile 之上。只有 mb() 完全屏障需要任何 asm 指令。请注意,CONFIG_X86_PPRO_FENCE 是一些奇怪的弱排序 x86 模型,没有任何用途。 IDK 如果硬件与该模型一样弱,IRL 曾经存在过,但现代 CPU 绝对不是那样的。 IIRC 最近从 Linux 中删除了该选项,因为它只是噪音。
    • @PeterCordes 确定吗?我没有看到,请参阅here for example。我的意思是,它肯定可以在某处使用asm("":::"memory"),但我没有发现它是用那个名字定义的。
    • 是的,我绝对 100% 确定 x86 的硬件内存模型是程序顺序 + 存储转发,并且 sfence 基本上是一个没有运行中的 NT 存储的空操作(AMD 提供SFENCE 更多语义,如 MFENCE 但英特尔没有)。我还确定上次我跟踪 Linux 的宏链中的宏,smp_rmb()smp_wmb() 是纯编译器障碍,而 C11 atomic_thread_fence(mo_release) 编译为零 asm 指令。 (Linux 的 rmbwmb 可能是别的东西;我忘记了 smp_ 的区别。它们用于 I/O,而不是 SMP)。
    • @PeterCordes 无论哪种方式我都可以,但非常感谢。我可以看到我的原始答案可能非常具有误导性。您的编辑很有意义,并为普通读者澄清了这一点。我并没有真正试图解决 x86 内存模型使 lfencesfence 无用并允许通过仅编译时排序进行序列化这一事实,但解释它很有意义。
    【解决方案2】:

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

    【讨论】:

    • 这样的架构存在吗?
    • @Y.A.:一些弱排序的 ISA 有一些障碍。 preshing.com/20120930/weak-vs-strong-memory-models。例如cl.cam.ac.uk/~pes20/cpp/cpp0xmappings.html 显示了从 C++11 std::atomic 内存命令到 asm 的标准映射,PowerPC 获取负载使用 ld; cmp; bc; isync 而存储释放可以使用 lwsync; st。但是对于像smp_rmbsmp_wmb 这样的获取或释放栅栏,两者都将使用lwsync,它会阻止除StoreLoad 之外的重新排序。
    • @Y.A.:或者显然 ARMv8 可以使用 DMB ISH LD(阻止加载重新排序)作为获取屏障,而 dmb ish(阻止所有内容)作为释放屏障。或者 SPARC(如果您不使用 SPARC-TSO 强内存排序)有 membar #LoadLoad | #LoadStoremembar #LoadStore | #StoreStore。见preshing.com/20130922/acquire-and-release-fences
    猜你喜欢
    • 2011-03-30
    • 2011-12-12
    • 1970-01-01
    • 2019-06-09
    • 1970-01-01
    • 2013-08-31
    • 1970-01-01
    • 2016-01-23
    • 2014-06-18
    相关资源
    最近更新 更多