https://lwn.net/Articles/728795/ 翻译:无牙 [编辑] 译注:

这篇文章需要对sys_membarrier(2)系统调用以及引入这个系统调用的原因有一些了解。在此简单介绍下,读者请自行阅读:

  1. sys_membarrier munal

    sys_membarrier(2)通过利用内核RCU的synchronize_sched()实现了写者调用sys_membarrier(2)而读者只需要使用编译barrier()便可以实现与读写者都调用CPU memory barrier指令同样的效果,其实现无非是将读者执行CPU memory barrier的开销转移到了写者身上;patch中有如下例子,Thread A是写者,Thread B是读者,barrier()仅仅是个针对编译器的barrier,提示编译器此处不要乱序。

    Before the change, we had, for each smp_mb() pairs:

    Thread A Thread B previous mem accesses previous mem accesses smp_mb() smp_mb() following mem accesses following mem accesses

    After the change, these pairs become:

    Thread A Thread B prev mem accesses prev mem accesses sys_membarrier() barrier() follow mem accesses follow mem accesses

    1. sys_membarrier历史

      sys_membarrier(2)系统调用在2015时进入了4.3的内核主线,增加这个系统调用的原因是,Mathieu Desnoyers在开发 LTTng的过程中想使用用户态RCU,在移植内核RCU到用户态的过程中发现多线程对共享变量的访问时需要有memory barrier来防止CPU乱序执行,而如果使用使用CPU提供的memory barrier指令,对于读者来说开销太大,因此Mathieu通过引入sys_membarrier(2)系统调用让写者在系统调用中通过执行synchronize_sched()阻塞等待所有其他CPU都执行完一次memory barrier。

      1. membarrier patch [编辑] 正文

        membarrier()系统调用可以说是Linux内核提供的最为奇怪的接口之一。它以较大的开销模拟了一个通过单条非特权的内存barrier指令即可完成的操作,它使用了内核的RCU机制,而这一切都是以性能的名义。但是,看起来membarrier()并不够快,导致一些使用者又回退到复杂且脆弱的方式。目前,对这个问题的修复正在讨论之中,但目前大家对这块分歧仍然较大。 [编辑] membarrier()

      membarrier()系统调用最初在2010时开始讨论,最初的使用场景是为了支持用户态的RCU,通过使用一个共享的变量来标示一个线程正运行在一个RCU的临界区中。当在有任意线程正处于一个RCU临界区中时,对该RCU保护的数据的修改都不能执行,因此修改被RCU保护的数据的代码必须检查这个共享的变量来保证修改的正确性。然而,当处理器乱序执行时,这个模式可能会有问题,导致数据在共享变量被检查之前就释放了。

           处理器本身提供了内存barrrier指令,可以避免这种场景。然后不幸的是,这些指令相对比较慢,因为他们必须串行化整台机器上对内存的访问。另外,内存barrier必须成对的使用才能正常工作;在这种场景下,当任何时候一个线程设置RCU临界区标志时,都需要有一个内存barrier,而另一个barrier则放在临界区标志检查完之后,任何其他操作执行之前。这种对称的barrier对在很多场景下都工作得很好,但是在RCU这种场景下,它效率很低。
      
             问题在于,进入RCU临界区本身是一个很频繁的事件,而改变被RCU保护的数据却非常罕见。可能成百上千甚至更多的rcu_read_lock()被调用而没有其他线程尝试去修改被RCU保护的数据;在这种情况下,所有这些读者的memory barrier都是一种不必要的浪费。对于这种读多写少的非对称的访问模型,很有必要将大部分的memory barrier的开销挪到写者上面去,从而让读者更快。
      
               这就是membarrier()诞生的原因。原始版本只是简单的发送一个IPI(inter-processor interrupt, 处理器间中断),导致所有的CPU都执行一条memory-barrier指令。显然,这么做不太受欢迎,因为这些IPI中断会唤醒系统上所有的其他CPU,给那些实时线程带来额外的时延。后续的讨论导致membarrier()的实现改成了调用synchronize_sched(),这个内核函数保证了会等待所有的处理器将执行完一个memory barrier后再返回。此时的patch里面其实已经包含了一个“expedited”选项,这个选项仍然使用IPI中断其他CPU,但是当membarrier()系统调用合入主干时,这个选项并没有包含进去。
           [编辑] expedited选项
      
                最近,Paul McKenney提了一个patch又把这个expedited选项加到了membarrier()中。这个修改吸引了不少眼球,原因是大家对IPI的担忧还没离去。Mathieu Desnoyers,原始membarrier()那个patch的作者, 问如何才能提供一个expedited选项而不影响那些实时线程,而Peter Zijlstra担心引入IPI会导致DoS攻击变得很容易,通过2行简单的代码就能实现:
      
                 for (;;)
                   membarrier(MEMBARRIER_CMD_SHARED_EXPEDITED, 0);
      
                     这个时候,看起来对这些问题没有新的答案,但是却有对expedited选项更强的需求,并且看起来这个选项并没有带来任何已有问题之外的新的问题。
      
                           就像McKenney描述的那样,有不少用户发现目前的membarrier()系统调用太慢了。这并不令人惊讶,synchronize_sched()会强制让调用线程阻塞等待直到系统上所有的CPU经历一次RCU的grace period,因此,必然会有一段额外的时延。而这些用户找到了一些tricky的方法来达到他们加速membarrier()的目的:他们调用mptrotec(2)或者munmap(2)系统调用而不是membarrier()。在x86系统上,这些系统调用会触发IPI中断来保证受影响的地址范围从TLB中移除。他们同样会导致一些没必要的内存管理的开销,但是显然,这样仍然会比使用membarrier()更快。
      
                             除了这些基本的一些不够优雅之外,这种方式还有一些其他问题。一个是它很容易被未来内核或者未来硬件打破,如果这些系统调用可以不依赖IPI中断工作;而这种优化(不依赖IPI)如果可行的话,内核开发者会很乐意这么做。事实上,IPI中断并不是在所有硬件架构上都存在的,因而,McKenney自己也在文中提到,这个方法“有不能在arm和arm64上工作的缺点”。在membarrier()中增加IPI中断的能力将会在所有的架构上有更好的性能而不需要诉诸于那些诡计。
      
                               由于有些用户已经通过内存管理的系统调用根据他们的意愿来触发IPI中断,McKenney并不认为在membarrier()中增加expedited选项会让情况变得更糟。但是,他认为仍然有一些方法来降低对expedited选项的滥用。譬如,在启动时就禁止expedited选项的grace period来限制membarrier()在一段时间内可以出发的IPI中断的数量。其他一些限制发送IPI到其他真正需要接受IPI中断的处理器(那些正在运行与调用membarrier()线程同一个进程的CPU)的方法也在考虑之中。提供expedited barrier的机制,至少可以给内核开发者提供一种应对对membarrier()滥用的方法。
      
                                 这个patch在最后进入主干之前,看起来还会有一些经修改和讨论。另外,那些需要更快速membarrier()的人也需要确认这些expedited选项确实能解决他们的问题。McKenney说:“显然,除非有很好的测试结果以及某些数量的用户对这个功能的热情,这个patch不会往主干上合”。而目前的patch,加起来也不到一页屏的大小,而针对它的讨论看起来远远超过了这个patch的大小。