原文链接 作者:David Howells、Paul E. McKenney 译者:曹姚君 校对:丁一 内容: 抽象的内存访问模型考虑下面这个系统的抽象模型: : : : : : : +-------+ : +--------+ : +-------+ | | : | | : | | | | : | | : | | | CPU 1 |<----->| Memory |<----->| CPU 2 | | | : | | : | | | | : | | : | | +-------+ : +--------+ : +-------+ ^ : ^ : ^ | : | : | | : | : | | : v : | | : +--------+ : | | : | | : | | : | | : | +---------->| Device |<----------+ : | | : : | | : : +--------+ : : : 每个CPU执行一个有内存访问操作的程序。在这个抽象的CPU中,内存操作的顺序是非常宽松的。假若能让程序的因果关系看起来是保持着的,CPU就可以以任意它喜欢的顺序执行内存操作。同样,只要不影响程序的结果,编译器可以以它喜欢的任何顺序安排指令。 因此,上图中,一个CPU执行内存操作的结果能被系统的其它部分感知到,因为这些操作穿过了CPU与系统其它部分之间的接口(虚线)。 例如,请考虑以下的事件序列: CPU 1 CPU 2 =============== =============== { A == 1; B == 2 } A = 3; x = A; B = 4; y = B; 内存系统能看见的访问顺序可能有24种不同的组合: STORE A=3, STORE B=4, x=LOAD A->3, y=LOAD B->4 STORE A=3, STORE B=4, y=LOAD B->4, x=LOAD A->3 STORE A=3, x=LOAD A->3, STORE B=4, y=LOAD B->4 STORE A=3, x=LOAD A->3, y=LOAD B->2, STORE B=4 STORE A=3, y=LOAD B->2, STORE B=4, x=LOAD A->3 STORE A=3, y=LOAD B->2, x=LOAD A->3, STORE B=4 STORE B=4, STORE A=3, x=LOAD A->3, y=LOAD B->4 STORE B=4, ... ... 因此,可能产生四种不同的值组合: x == 1, y == 2 x == 1, y == 4 x == 3, y == 2 x == 3, y == 4 此外,一个CPU 提交store指令到存储系统,另一个CPU执行load指令时感知到的这些store的顺序可能并不是第一个CPU提交的顺序。 另一个例子,考虑下面的事件序列: CPU 1 CPU 2 =============== =============== { A == 1, B == 2, C = 3, P == &A, Q == &C } B = 4; Q = P; P = &B D = *Q; 这里有一个明显的数据依赖,D的值取决于CPU 2从P取得的地址。执行结束时,下面任一结果都是有可能的; (Q == &A) and (D == 1) (Q == &B) and (D == 2) (Q == &B) and (D == 4) 注意:CPU 2永远不会将C的值赋给D,因为CPU在对*Q发出load指令之前会先将P赋给Q。 硬件操作一些硬件的控制接口,是一组存储单元,但这些控制寄存器的访问顺序是非常重要的。例如,考虑拥有一系列内部寄存器的以太网卡,它通过一个地址端口寄存器(A)和一个数据端口寄存器(D)访问。现在要读取编号为5的内部寄存器,可能要使用下列代码: *A = 5; x = *D; 但上面代码可能表现出下列两种顺序: STORE *A = 5, x = LOAD *D x = LOAD *D, STORE *A = 5 其中第二个几乎肯定会导致故障,因为它在读取寄存器之后才设置地址值。 保障下面是CPU必须要保证的最小集合:
还有一些必须要和一定不能假设的东西:
什么是内存屏障?如上所述,没有依赖关系的内存操作实际会以随机的顺序执行,但对CPU-CPU的交互和I / O来说却是个问题。我们需要某种方式来指导编译器和CPU以约束执行顺序。 内存屏障就是这样一种干预手段。它们会给屏障两侧的内存操作强加一个偏序关系。 这种强制措施是很重要的,因为一个系统中,CPU和其它硬件可以使用各种技巧来提高性能,包括内存操作的重排、延迟和合并;预取;推测执行分支以及各种类型的缓存。内存屏障是用来禁用或抑制这些技巧的,使代码稳健地控制多个CPU和(或)设备的交互。 内存屏障的种类内存屏障有四种基本类型:
write内存屏障保证:所有该屏障之前的store操作,看起来一定在所有该屏障之后的store操作之前执行。 write屏障仅保证store指令上的偏序关系,不要求对load指令有什么影响。 随着时间推移,可以视CPU提交了一系列store操作到内存系统。在该一系列store操作中,write屏障之前的所有store操作将在该屏障后面的store操作之前执行。 [!]注意,write屏障一般与read屏障或数据依赖障碍成对出现;请参阅“SMP屏障配对”小节。 一对隐式的屏障变种:
仅当两个CPU之间或者CPU与其它设备之间有交互时才需要屏障。如果可以确保某段代码中不会有任何这种交互,那么这段代码就不需要内存屏障。 注意,这些是最低限度的保证。不同的架构可能会提供更多的保证,但是它们不是必须的,不应该依赖其写代码(they may not be relied upon outside of arch specific code)。 什么是内存屏障不能确保的?有一些事情,Linux内核的内存屏障并不保证:
数据依赖屏障数据依赖屏障的使用条件有点微妙,且并不总是很明显。为了阐明问题,考虑下面的事件序列: CPU 1 CPU 2 =============== =============== { A == 1, B == 2, C = 3, P == &A, Q == &C } B = 4; <write barrier> P = &B Q = P; D = *Q; 这里很明显存在数据依赖,看起来在执行结束后,Q不是&A就是&B,并且: (Q == &A) 意味着 (D == 1) (Q == &B) 意味着 (D == 4) 但是,从CPU 2可能先感知到P更新,然后才感知到B更新,这就导致了以下情况: (Q == &B) and (D == 2) ???? 虽然这可能看起来像是一致性或因果关系维护失败,但实际并不是的,且这种行为在一些真实的CPU上也可以观察到(如DEC Alpha)。 为了处理这个问题,需要在地址load和数据load之间插入一个数据依赖屏障或一个更强的屏障: CPU 1 CPU 2 =============== =============== { A == 1, B == 2, C = 3, P == &A, Q == &C } B = 4; <write barrier> P = &B Q = P; <data dependency barrier> D = *Q; 这将迫使结果为前两种情况之一,而防止了第三种可能性的出现。 [!]注意:这种极其有违直觉的场景,在有多个独立缓存(split caches)的机器上很容易出现,比如:一个cache bank处理偶数编号的缓存行,另外一个cache bank处理奇数编号的缓存行。指针P可能存储在奇数编号的缓存行,变量B可能存储在偶数编号的缓存行中。然后,如果在读取CPU缓存的时候,偶数的bank非常繁忙,而奇数bank处于闲置状态,就会出现指针P(&B)是新值,但变量B(2)是旧值的情况。 另外一个需要数据依赖屏障的例子是从内存中读取一个数字,然后用来计算某个数组的下标; CPU 1 CPU 2 =============== =============== { M[0] == 1, M[1] == 2, M[3] = 3, P == 0, Q == 3 } M[1] = 4; <write barrier> P = 1 Q = P; <data dependency barrier> D = M[Q]; 数据依赖屏障对RCU系统是很重要的,如,看include/linux/ rcupdate.h的rcu_dereference()函数。这个函数允许RCU的指针被替换为一个新的值,而这个新的值还没有完全的初始化。 更多详细的例子参见”高速缓存一致性”小节。 控制依赖控制依赖需要一个完整的read内存屏障来保证其正确性,而不简单地只是数据依赖屏障。考虑下面的代码: q = &a; if (p) q = &b; <data dependency barrier> x = *q; 这不会产生想要的结果,因为这里没有实际的数据依赖,而是一个控制依赖,CPU可能会提前预测结果而使if语句短路。在这样的情况下,实际需要的是下面的代码: q = &a; if (p) q = &b; <read barrier> x = *q; SMP屏障配对当处理CPU-CPU之间的交互时,相应类型的内存屏障总应该是成对出现的。缺少相应的配对屏障几乎可以肯定是错误的。 write屏障应始终与数据依赖屏障或者read屏障配对,虽然通用内存屏障也是可以的。同样地,read屏障或数据依赖屏障应至少始终与write屏障配对使用,虽然通用屏障仍然也是可以的: CPU 1 CPU 2 =============== =============== a = 1; <write barrier> b = 2; x = b; <read barrier> y = a; 或者: CPU 1 CPU 2 =============== =============================== a = 1; <write barrier> b = &a; x = b; <data dependency barrier> y = *x; 基本上,那个位置的read屏障是必不可少的,尽管可以是“更弱“的类型。 [!]注意:write屏障之前的store指令通常与read屏障或数据依赖屏障后的load指令相匹配,反之亦然: CPU 1 CPU 2 =============== =============== a = 1; }---- --->{ v = c b = 2; } \ / { w = d <write barrier> \ <read barrier> c = 3; } / \ { x = a; d = 4; }---- --->{ y = b; 内存屏障序列实例首先,write屏障确保store操作的偏序关系。考虑以下事件序列: CPU 1 ======================= STORE A = 1 STORE B = 2 STORE C = 3 <write barrier> STORE D = 4 STORE E = 5 这一连串的事件提交给内存一致性系统的顺序,可以使系统其它部分感知到无序集合{ STORE A,STORE B, STORE C } 中的操作都发生在无序集合{ STORE D, STORE E}中的操作之前: +-------+ : : | | +------+ | |------>| C=3 | } /\ | | : +------+ }----- \ -----> Events perceptible to | | : | A=1 | } \/ the rest of the system | | : +------+ } | CPU 1 | : | B=2 | } | | +------+ } | | wwwwwwwwwwwwwwww } <--- At this point the write barrier | | +------+ } requires all stores prior to the | | : | E=5 | } barrier to be committed before | | : +------+ } further stores may take place | |------>| D=4 | } | | +------+ +-------+ : : | | Sequence in which stores are committed to the | memory system by CPU 1 V 其次,数据依赖屏障确保于有数据依赖关系的load指令间的偏序关系。考虑以下事件序列: CPU 1 CPU 2 ======================= ======================= { B = 7; X = 9; Y = 8; C = &Y } STORE A = 1 STORE B = 2 <write barrier> STORE C = &B LOAD X STORE D = 4 LOAD C (gets &B) LOAD *C (reads B) 在没有其它干涉时,尽管CPU 1发出了write屏障,CPU2感知到的CPU1上事件的顺序也可能是随机的: +-------+ : : : : | | +------+ +-------+ | Sequence of update | |------>| B=2 |----- --->| Y->8 | | of perception on | | : +------+ \ +-------+ | CPU 2 | CPU 1 | : | A=1 | \ --->| C->&Y | V | | +------+ | +-------+ | | wwwwwwwwwwwwwwww | : : | | +------+ | : : | | : | C=&B |--- | : : +-------+ | | : +------+ \ | +-------+ | | | |------>| D=4 | ----------->| C->&B |------>| | | | +------+ | +-------+ | | +-------+ : : | : : | | | : : | | | : : | CPU 2 | | +-------+ | | Apparently incorrect ---> | | B->7 |------>| | perception of B (!) | +-------+ | | | : : | | | +-------+ | | The load of X holds ---> \ | X->9 |------>| | up the maintenance \ +-------+ | | of coherence of B ----->| B->2 | +-------+ +-------+ : : 在上述的例子中,尽管load *C(可能是B)在load C之后,但CPU 2感知到的B却是7; 然而,在CPU2中,如果数据依赖屏障放置在loadC和load *C(即:B)之间: CPU 1 CPU 2 ======================= ======================= { B = 7; X = 9; Y = 8; C = &Y } STORE A = 1 STORE B = 2 <write barrier> STORE C = &B LOAD X STORE D = 4 LOAD C (gets &B) <data dependency barrier> LOAD *C (reads B) 将发生以下情况: +-------+ : : : : | | +------+ +-------+ | |------>| B=2 |----- --->| Y->8 | | | : +------+ \ +-------+ | CPU 1 | : | A=1 | \ --->| C->&Y | | | +------+ | +-------+ | | wwwwwwwwwwwwwwww | : : | | +------+ | : : | | : | C=&B |--- | : : +-------+ | | : +------+ \ | +-------+ | | | |------>| D=4 | ----------->| C->&B |------>| | | | +------+ | +-------+ | | +-------+ : : | : : | | | : : | | | : : | CPU 2 | | +-------+ | | | | X->9 |------>| | | +-------+ | | Makes sure all effects ---> \ ddddddddddddddddd | | prior to the store of C \ +-------+ | | are perceptible to ----->| B->2 |------>| | subsequent loads +-------+ | | : : +-------+ 第三,read屏障确保load指令上的偏序关系。考虑以下的事件序列: CPU 1 CPU 2 ======================= ======================= { A = 0, B = 9 } STORE A=1 <write barrier> STORE B=2 LOAD B LOAD A 在没有其它干涉时,尽管CPU1发出了一个write屏障,CPU 2感知到的CPU 1中事件的顺序也可能是随机的: +-------+ : : : : | | +------+ +-------+ | |------>| A=1 |------ --->| A->0 | | | +------+ \ +-------+ | CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 | | | +------+ | +-------+ | |------>| B=2 |--- | : : | | +------+ \ | : : +-------+ +-------+ : : \ | +-------+ | | ---------->| B->2 |------>| | | +-------+ | CPU 2 | | | A->0 |------>| | | +-------+ | | | : : +-------+ \ : : \ +-------+ ---->| A->1 | +-------+ : : 然而,如果在CPU2上的load A和load B之间放置一个read屏障: CPU 1 CPU 2 ======================= ======================= { A = 0, B = 9 } STORE A=1 <write barrier> STORE B=2 LOAD B <read barrier> LOAD A CPU1上的偏序关系将能被CPU2正确感知到: +-------+ : : : : | | +------+ +-------+ | |------>| A=1 |------ --->| A->0 | | | +------+ \ +-------+ | CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 | | | +------+ | +-------+ | |------>| B=2 |--- | : : | | +------+ \ | : : +-------+ +-------+ : : \ | +-------+ | | ---------->| B->2 |------>| | | +-------+ | CPU 2 | | : : | | | : : | | At this point the read ----> \ rrrrrrrrrrrrrrrrr | | barrier causes all effects \ +-------+ | | prior to the storage of B ---->| A->1 |------>| | to be perceptible to CPU 2 +-------+ | | : : +-------+ 为了更彻底说明这个问题,考虑read屏障的两侧都有load A将发生什么: CPU 1 CPU 2 ======================= ======================= { A = 0, B = 9 } STORE A=1 <write barrier> STORE B=2 LOAD B LOAD A [first load of A] <read barrier> LOAD A [second load of A] 即使两个load A都发生在loadB之后,它们仍然可能获得不同的值: +-------+ : : : : | | +------+ +-------+ | |------>| A=1 |------ --->| A->0 | | | +------+ \ +-------+ | CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 | | | +------+ | +-------+ | |------>| B=2 |--- | : : | | +------+ \ | : : +-------+ +-------+ : : \ | +-------+ | | ---------->| B->2 |------>| | | +-------+ | CPU 2 | | : : | | | : : | | | +-------+ | | | | A->0 |------>| 1st | | +-------+ | | At this point the read ----> \ rrrrrrrrrrrrrrrrr | | barrier causes all effects \ +-------+ | | prior to the storage of B ---->| A->1 |------>| 2nd | to be perceptible to CPU 2 +-------+ | | : : +-------+ 但是,在read屏障完成之前,CPU1对A的更新就可能被CPU2看到: +-------+ : : : : | | +------+ +-------+ | |------>| A=1 |------ --->| A->0 | | | +------+ \ +-------+ | CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 | | | +------+ | +-------+ | |------>| B=2 |--- | : : | | +------+ \ | : : +-------+ +-------+ : : \ | +-------+ | | ---------->| B->2 |------>| | | +-------+ | CPU 2 | | : : | | \ : : | | \ +-------+ | | ---->| A->1 |------>| 1st | +-------+ | | rrrrrrrrrrrrrrrrr | | +-------+ | | | A->1 |------>| 2nd | +-------+ | | : : +-------+ 如果load B == 2,可以保证第二次load A总是等于 1。但是不能保证第一次load A的值,A == 0或A == 1都可能会出现。 |