Linux内核的内存屏障设计(下)

上篇

CPU缓存的影响

缓存的内存操作被系统交叉感知的方式,在一定程度上,受到CPU和内存之间的缓存、以及保持系统一致状态的内存一致性系统的影响。

若CPU和系统其它部分的交互通过cache进行,内存系统就必须包括CPU缓存,以及CPU及其缓存之间的内存屏障(内存屏障逻辑上如下图中的虚线):

        <--- CPU --->         :       <----------- Memory ----------->
                              :
    +--------+    +--------+  :   +--------+    +-----------+
    |        |    |        |  :   |        |    |           |    +--------+
    |  CPU   |    | Memory |  :   | CPU    |    |           |    |        |
    |  Core  |--->| Access |----->| Cache  |<-->|           |    |        |
    |        |    | Queue  |  :   |        |    |           |--->| Memory |
    |        |    |        |  :   |        |    |           |    |        |
    +--------+    +--------+  :   +--------+    |           |    |        |
                              :                 | Cache     |    +--------+
                              :                 | Coherency |
                              :                 | Mechanism |    +--------+
    +--------+    +--------+  :   +--------+    |           |    |        |
    |        |    |        |  :   |        |    |           |    |        |
    |  CPU   |    | Memory |  :   | CPU    |    |           |--->| Device |
    |  Core  |--->| Access |----->| Cache  |<-->|           |    |        |
    |        |    | Queue  |  :   |        |    |           |    |        |
    |        |    |        |  :   |        |    |           |    +--------+
    +--------+    +--------+  :   +--------+    +-----------+
                              :
                              :

虽然一些特定的load或store实际上可能不出现在发出这些指令的CPU之外,因为在该CPU自己的缓存内已经满足,但是,如果其它CPU关心这些数据,那么还是会产生完整的内存访问,因为高速缓存一致性机制将迁移缓存行到需要访问的CPU,并传播冲突。

只要能维持程序的因果关系,CPU核心可以以任何顺序执行指令。有些指令生成load和store操作,并将他们放入内存请求队列等待执行。CPU内核可以以任意顺序放入到队列中,并继续执行,直到它被强制等待某一个指令完成。

内存屏障关心的是控制访问穿越CPU到内存一边的顺序,以及系统其他组建感知到的顺序。

[!]对于一个给定的CPU,并不需要内存屏障,因为CPU总是可以看到自己的load和store指令,好像发生的顺序就是程序顺序一样。

[!]MMIO或其它设备访问可能绕过缓存系统。这取决于访问设备时内存窗口属性,或者某些CPU支持的特殊指令。

缓存一致性

但是事情并不像上面说的那么简单,虽然缓存被期望是一致的,但是没有保证这种一致性的顺序。这意味着在一个CPU上所做的更改最终可以被所有CPU可见,但是并不保证其它的CPU能以相同的顺序感知变化。

考虑一个系统,有一对CPU(1&2),每一个CPU有一组并行的数据缓存(CPU 1有A / B,CPU 2有C / D):

                :
                :                          +--------+
                :      +---------+         |        |
    +--------+  : +--->| Cache A |<------->|        |
    |        |  : |    +---------+         |        |
    |  CPU 1 |<---+                        |        |
    |        |  : |    +---------+         |        |
    +--------+  : +--->| Cache B |<------->|        |
                :      +---------+         |        |
                :                          | Memory |
                :      +---------+         | System |
    +--------+  : +--->| Cache C |<------->|        |
    |        |  : |    +---------+         |        |
    |  CPU 2 |<---+                        |        |
    |        |  : |    +---------+         |        |
    +--------+  : +--->| Cache D |<------->|        |
                :      +---------+         |        |
                :                          +--------+
                :

假设该系统具有以下属性:

  • 奇数编号的缓存行在缓存A或者C中,或它可能仍然驻留在内存中;
  • 偶数编号的缓存行在缓存B或者D中,或它可能仍然驻留在内存中;
  • 当CPU核心正在访问一个cache,其它的cache可能利用总线来访问该系统的其余组件
    —— 可能是取代脏缓存行或预加载;
  • 每个cache有一个操作队列,用来维持cache与系统其余部分的一致性;
  • 正常load已经存在于缓存行中的数据时,一致性队列不会刷新,即使队列中的内容可能会影响这些load。

接下来,试想一下,第一个CPU上有两个写操作,并且它们之间有一个write屏障,来保证它们到达该CPU缓存的顺序:

    CPU 1           CPU 2           COMMENT
    =============== =============== =======================================
                    u == 0, v == 1 and p == &u, q == &u
    v = 2;
    smp_wmb();                      Make sure change to v is visible before
                                    change to p
                      v is now in cache A exclusively
    p = &v;
                     p is now in cache B exclusively

write内存屏障强制系统中其它CPU能以正确的顺序感知本地CPU缓存的更改。现在假设第二个CPU要读取这些值:

    CPU 1           CPU 2           COMMENT
    =============== =============== =======================================
    ...
                    q = p;
                    x = *q;

上述一对读操作可能不会按预期的顺序执行,持有P的缓存行可能被第二个CPU的某一个缓存更新,而持有V的缓存行在第一个CPU的另外一个缓存中因为其它事情被延迟更新了;

    CPU 1           CPU 2           COMMENT
    =============== =============== =======================================
                    u == 0, v == 1 and p == &u, q == &u
    v = 2;
    smp_wmb();
      
                    
    p = &v;         q = p;
                    
     
                    
                    x = *q;
                       Reads from v before v updated in cache
                    
                    

基本上,虽然两个缓存行CPU 2在最终都会得到更新,但是在不进行干预的情况下不能保证更新的顺序与在CPU 1在提交的顺序一致。

所以我们需要在load之间插入一个数据依赖屏障或read屏障。这将迫使缓存在处理其他任务之前强制提交一致性队列;

    CPU 1           CPU 2           COMMENT
    =============== =============== =======================================
                    u == 0, v == 1 and p == &u, q == &u
    v = 2;
    smp_wmb();
      
                    
    p = &v;         q = p;
                    
     
                    
                    smp_read_barrier_depends()
                    
                    
                    x = *q;
                       Reads from v after v updated in cache

DEC Alpha处理器上可能会遇到这类问题,因为他们有一个分列缓存,通过更好地利用数据总线以提高性能。虽然大部分的CPU在读操作需要读取内存的时候会使用数据依赖屏障,但并不都这样,所以不能依赖这些。

其它CPU也可能页有分列缓存,但是对于正常的内存访问,它们会协调各个缓存列。在缺乏内存屏障的时候,Alpha 的语义会移除这种协作。

缓存一致性与DMA

对于DMA的设备,并不是所有的系统都维护缓存一致性。这时访问DMA的设备可能从RAM中得到脏数据,因为脏的缓存行可能驻留在各个CPU的缓存中,并且可能还没有被写入到RAM。为了处理这种情况,内核必须刷新每个CPU缓存上的重叠位(或是也可以直接废弃它们)。

此外,当设备以及加载自己的数据之后,可能被来自CPU缓存的脏缓存行写回RAM所覆盖,或者当前CPU缓存的缓存行可能直接忽略RAM已被更新,直到缓存行从CPU的缓存被丢弃和重载。为了处理这个问题,内核必须废弃每个CPU缓存的重叠位。

。### CPU能做的事情

程序员可能想当然的认为CPU会完全按照指定的顺序执行内存操作,如果确实如此的话,假设CPU执行下面这段代码:

    a = *A;
    *B = b;
    c = *C;
    d = *D;
    *E = e;

他们会期望CPU执行下一个指令之前上一个一定执行完成,然后在系统中可以观察到一个明确的执行顺序;

    LOAD *A, STORE *B, LOAD *C, LOAD *D, STORE *E.

当然,现实中是非常混乱的。对许多CPU和编译器来说,上述假设都不成立,因为:

  • load操作可能更需要立即完成的,以保持执行进度,而推迟store往往是没有问题的;
  • load操作可能预取,当结果证明是不需要的,可以丢弃;
  • load操作可能预取,导致取数的时间和预期的事件序列不符合;
  • 内存访问的顺序可能被重排,以更好地利用CPU总线和缓存;
  • 与内存和IO设备交互时,如果能批访问相邻的位置,load和store可能会合并,以提高性能,从而减少了事务设置的开销(内存和PCI设备都能够做到这一点);
  • CPU的数据缓存也可能会影响顺序,虽然缓存一致性机制可以缓解 —— 一旦store操作命中缓存 —— 并不能保证一致性能正确的传播到其它CPU。

所以对另一个CPU,上面的代码实际观测的结果可能是:

    LOAD *A, ..., LOAD {*C,*D}, STORE *E, STORE *B

    (Where "LOAD {*C,*D}" is a combined load)

但是,CPU保证自身的一致性:不需要内存屏障,也可以保证自己以正确的顺序访问内存,如下面的代码:

    U = *A;
    *A = V;
    *A = W;
    X = *A;
    *A = Y;
    Z = *A;

假设不受到外部的影响,最终的结果可能为:

    U == the original value of *A
    X == W
    Z == Y
    *A == Y

上面的代码CPU可能产生的全部的内存访问顺序如下:

    U=LOAD *A, STORE *A=V, STORE *A=W, X=LOAD *A, STORE *A=Y, Z=LOAD *A

对于这个顺序,如果没有干预,在保持一致的前提下,一些操作也可能被合并,丢弃。

在CPU感知这些操作之前,编译器也可能合并、丢弃、延迟加载这些元素。

例如:

    *A = V;
    *A = W;

可减少到:

    *A = W;

因为在没有write屏障的情况下,可以假定将V写入到*A的操作被丢弃了,同样:

    *A = Y;
    Z = *A;

若没有内存屏障,可被简化为:

    *A = Y;
    Z = Y;

在CPU之外根本看不到load操作。

ALPHA处理器

DEC Alpha CPU是最松散的CPU之一。不仅如此,一些版本的Alpha CPU有一个分列的数据缓存,允许它们在不同的时间更新语义相关的缓存。在同步多个缓存,保证一致性的时候,数据依赖屏障是必须的,使CPU可以正确的顺序处理指针的变化和数据的获得。

Alpha定义了Linux内核的内存屏障模型。

使用示例

循环缓冲区

内存屏障可以用来实现循环缓冲,不需要用锁来使得生产者与消费者串行。

参考

  • Alpha AXP Architecture Reference Manual, Second Edition (Sites &
    Witek, Digital Press)
  • AMD64 Architecture Programmer’s Manual Volume 2: System Programming
  • IA-32 Intel Architecture Software Developer’s Manual, Volume 3: System Programming Guide
  • The SPARC Architecture Manual, Version 9
  • UltraSPARC Programmer Reference Manual
  • UltraSPARC III Cu User’s Manual
  • UltraSPARC IIIi Processor User’s Manual
  • UltraSPARC Architecture 2005
  • UltraSPARC T1 Supplement to the UltraSPARC Architecture 2005
  • Solaris Internals, Core Kernel Architecture, p63-68:
  • Unix Systems for Modern Architectures, Symmetric Multiprocessing and
    Caching for Kernel Programmers:
  • Intel Itanium Architecture Software Developer’s Manual: Volume 1:

原文 Memory barriers
作者 David Howells、Paul E. McKenney
译者 曹姚君
校对 丁一
via ifeve.com

你可能感兴趣的:(操作系统,linux-kernel,内存屏障,cpu)