首页 > 解决方案 > 理解 linux 中的 membarier 函数

问题描述

linux手册中使用membarrier函数的示例:https ://man7.org/linux/man-pages/man2/membarrier.2.html

       #include <stdlib.h>

       static volatile int a, b;

       static void
       fast_path(int *read_b)
       {
           a = 1;
           asm volatile ("mfence" : : : "memory");
           *read_b = b;
       }

       static void
       slow_path(int *read_a)
       {
           b = 1;
           asm volatile ("mfence" : : : "memory");
           *read_a = a;
       }

       int
       main(int argc, char **argv)
       {
           int read_a, read_b;

           /*
            * Real applications would call fast_path() and slow_path()
            * from different threads. Call those from main() to keep
            * this example short.
            */

           slow_path(&read_a);
           fast_path(&read_b);

           /*
            * read_b == 0 implies read_a == 1 and
            * read_a == 0 implies read_b == 1.
            */

           if (read_b == 0 && read_a == 0)
               abort();

           exit(EXIT_SUCCESS);
       }

上面转换为使用 membarrier() 的代码变为:

       #define _GNU_SOURCE
       #include <stdlib.h>
       #include <stdio.h>
       #include <unistd.h>
       #include <sys/syscall.h>
       #include <linux/membarrier.h>

       static volatile int a, b;

       static int
       membarrier(int cmd, unsigned int flags, int cpu_id)
       {
           return syscall(__NR_membarrier, cmd, flags, cpu_id);
       }

       static int
       init_membarrier(void)
       {
           int ret;

           /* Check that membarrier() is supported. */

           ret = membarrier(MEMBARRIER_CMD_QUERY, 0, 0);
           if (ret < 0) {
               perror("membarrier");
               return -1;
           }

           if (!(ret & MEMBARRIER_CMD_GLOBAL)) {
               fprintf(stderr,
                   "membarrier does not support MEMBARRIER_CMD_GLOBAL\n");
               return -1;
           }

           return 0;
       }

       static void
       fast_path(int *read_b)
       {
           a = 1;
           asm volatile ("" : : : "memory");
           *read_b = b;
       }

       static void
       slow_path(int *read_a)
       {
           b = 1;
           membarrier(MEMBARRIER_CMD_GLOBAL, 0, 0);
           *read_a = a;
       }

       int
       main(int argc, char **argv)
       {
           int read_a, read_b;

           if (init_membarrier())
               exit(EXIT_FAILURE);

           /*
            * Real applications would call fast_path() and slow_path()
            * from different threads. Call those from main() to keep
            * this example short.
            */

           slow_path(&read_a);
           fast_path(&read_b);

           /*
            * read_b == 0 implies read_a == 1 and
            * read_a == 0 implies read_b == 1.
            */

           if (read_b == 0 && read_a == 0)
               abort();

           exit(EXIT_SUCCESS);
       }

这个“成员”描述取自 Linux 手册。我仍然对“成员”功能如何将开销添加到慢速端并从快速端移除开销感到困惑,因此只要慢速端不频繁以至于成员的开销( ) 调用不会超过快速方面的性能提升。

你能帮我更详细地描述一下吗?

谢谢!

标签: clinuxx86memory-barriers

解决方案


这对 writes-then-read-the-other-var 是https://preshing.com/20120515/memory-reordering-caught-in-the-act/,StoreLoad重新排序的演示(x86 唯一允许的一种,给定它的程序顺序 +具有存储转发内存模型的存储缓冲区)。

只有一个本地 MFENCE 仍然可以重新排序:

   FAST                      using just mfence, not membarrier
a = 1 exec
read_b = b;  // 0
                             b = 1;
                             mfence   (force b=1 to be visible before reading a)
                             read_a = a;   // 0
a = 1 visible (global vis. delayed by store buffer)

但是考虑一下,如果在慢速路径的存储和重新加载之间,每个可能的顺序都必须包含 mfence-on-every-core 会发生什么。

这种排序将不再可能。如果read_b=b已经读取了 0,那么a=1已经是挂起的1(如果它还不可见)。它不可能保持私有直到之后,read_a = a因为 membarrier() 确保在每个核心上运行一个完整的屏障,并且 SLOW 在读取之前等待这种情况发生(成员返回)a

并且没有办法先0,0执行 SLOW ;它自己运行 membarrier,因此它的存储在读取之前对其他线程绝对a可见。

脚注 1:等待执行,或在存储缓冲区中等待提交到 L1d 缓存。确保了这asm("":::"memory")一点,但实际上是多余的,因为volatile它本身保证了访问按程序顺序在 asm 中发生。在手动滚动原子而不是使用 C11 时,我们基本上需要 volatile 其他原因_Atomic。(但通常不要这样做,除非您实际上正在编写内核代码。使用atomic_store_explicit(&a, 1, memory_order_release);)。


请注意,它实际上是创建 StoreLoad 重新排序的存储缓冲区(x86 允许的唯一类型),而不是 OoO exec。实际上,存储缓冲区还允许 x86 乱序执行存储,然后使它们按程序顺序全局可见(如果事实证明它们不是错误推测或其他原因的结果!)。

另请注意,有序 CPU 可以无序地访问内存。它们按顺序开始指令(包括加载),但可以让它们不按顺序完成,例如通过记分板加载以允许击中未命中。另请参阅如何使用按序提交进行加载->存储重新排序?


推荐阅读