============================
By: David Howells <dhowells@redhat.com> Paul E. McKenney <paulmck@linux.vnet.ibm.com>
(*)抽象内存访问模型。
- 设备操作。
- 什么是确保的。
(*)什么是内存屏障?
- 内存屏障的种类。
- 什么是内存屏障不能确保的?
- 数据依赖屏障。
- 控制依赖。
- SMP屏障配对。
- 内存屏障顺序的例子。
- read内存屏障 与 load预取。
- 传递性
(*)显式内核屏障。
- 编译屏障。
- CPU内存屏障。
- MMIO写屏障。
(*)隐式内核内存屏障。
- 锁(Locking )功能。
- 中断(Interrupt )禁用功能。
- 休眠(Sleep )和唤醒(wake-up)功能。
- 其他功能。
(*)CPU之间锁屏障效应。
- 锁与内存访问。
- 锁与I / O访问。
(*)何时需要内存障碍?
- 多处理器之间的交互。
- 原子操作。
- 设备访问。
- 中断。
(*)内核的I / O屏障效应。
(*)最小执行顺序的假想模型。
(*)CPU缓存的作用。
- 缓存的一致性。
- 缓存的一致性与DMA。
- 缓存的一致性与MMIO。
(*)CPU能做到的
- Alpha CPU。
(*)使用示例。
- 循环缓冲区。
(*)引用
============================
抽象内存访问模型
============================
考虑下面的抽象系统模型:
图1
每一个CPU执行一个有内存访问操作的程序。在这个抽象的CPU中,内存操作的顺序是非常简单的。但是实际上CPU在维护因果关系的前提下,可能以它喜欢的任何顺序执行内存操作。同样,编译器也可以按照它喜欢的任何顺序生成指令,只要它不影响到程序的真正意图。
因此,在上图中一个CPU执行内存操作的影响,要通过CPU和系统其他部分之间的接口(虚线),才能被系统其他部分感知。
例如,请考虑以下的事件序列:
访问集在内存系统中可能出现下面24种不同的组合:
因此,可能产生四种不同值组合的结果:
x == 1, y == 2 x == 1, y == 4 x == 3, y == 2 x == 3, y == 4
此外,一个CPU 提交stores 指令到存储系统,另外一个CPU执行load指令时,并不能保证能正确感知到stores和load指令提交的顺序。
作为进一步的例子,考虑下面的事情顺序:
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之前一定会执行将P的值赋值给Q;
设备操作
-----------------
目前有些设备的控制接口,是映射到内存空间上一组地址。这些控制寄存器的访问顺序是非常重要的。例如,考虑拥有一系列内部寄存器的以太网卡,它通过一个地址端口寄存器(A)和一个数据端口寄存器(D)访问。现在要读取编号为5的内部寄存器,假设实现代码如下:
*A = 5; x = *D;
实际运行时可能是下面的两个序列之一:
STORE *A = 5, x = LOAD *D x = LOAD *D, STORE *A = 5
其中第二个肯定会导致故障,因为它设置值在读取寄存器的地址之后。
担保
----------
下面是CPU必须要保证的最小集合:
(*)任何给定的CPU,依赖的内存访问必须在自己内部是有序的。这意味着对于
Q = P; D = *Q;
CPU执行的内存操作顺序:
Q = LOAD P, D = LOAD *Q
并且总是相同的顺序。
(*)一个特定的CPU内,交叉的加载(loads)和存储(store)指令必须是有序的。这意味着对于:
a = *X; *X = b;
CPU的内存操只能出现下面的顺序:
a = LOAD *X, STORE *X = b
对于:
*X = c; d = *X;
CPU的内存操只能出现下面的顺序:
STORE *X = c, d = LOAD *X
(如果它们的目标内存片段是重叠的,称为加载(load)和存储(store)交叉)。
下面是必须要确保的和一定不能确保的:
(*)独立的加载(load)和存储(store)一定不能确保特定的顺序,这意味着对于:
X = *A; Y = *B; *D = Z;
我们可能得到下面的序列之一:
X = LOAD *A, Y = LOAD *B, STORE *D = Z X = LOAD *A, STORE *D = Z, Y = LOAD *B Y = LOAD *B, X = LOAD *A, STORE *D = Z Y = LOAD *B, STORE *D = Z, X = LOAD *A STORE *D = Z, X = LOAD *A, Y = LOAD *B STORE *D = Z, Y = LOAD *B, X = LOAD *A
(*)必须要确保是重复的内存访问可以被合并或者取消,这意味着对于
X = *A; Y = *(A + 4);
我们可能得到下面的序列之一:
X = LOAD *A; Y = LOAD *(A + 4); Y = LOAD *(A + 4); X = LOAD *A; {X, Y} = LOAD {*A, *(A + 4) };
对于:
*A = X; Y = *A;
我们可能会得到下面的序列之一:
STORE *A = X; Y = LOAD *A; STORE *A = Y = X;
=========================
什么是内存屏障?
=========================
如上所述,独立的存储操作以随机的顺序执行,但是对于CPU与CPU的交互和I / O来说会产生问题,我们需要某种方式来干预编译器和CPU的执行顺序。
内存屏障就是这样一种干预手段。它们通过在一侧设置屏障来保证内存操作的局部顺序。
如此强制的手段是有必要的,因为在一个系统中CPU和其他设备可以使用各种技巧来提高性能,包括内存操作的重排、延迟加载和合并;预取;推测执行分支和各种类型的缓存。使用内存屏障来禁用或抑制这些技巧,使代码稳健的控制多个CPU和(或)设备之间的交互。
内存屏障类型
---------------------------
内存屏障的四个种基本类型:
(1)写(write)(或存储(store))内存屏障。
写内存屏障保证所有在指定的屏障之前的存储(store)操作一定在所有在指定屏障之后的存储(store)操作之前执行。
写屏障仅仅保证存储(store)指令的局部顺序,不对加载(load)指令有任何影响。
一个CPU可以被视为随着时间的推移顺序提交存储操作(store)给内存系统。在序列中所有在写屏障之前的存储(store)指令一定在所有在写屏障之后存储(store)指令之前执行。
[!]请注意,写障碍一般与读屏障或数据依赖障碍搭配使用,请参阅,“SMP屏障配对”章节。
(2)数据依赖屏障。
数据依赖屏障是读屏障的一种较弱形式。在两个load指令执行的情况下,第二个依赖于第一个的执行结果(例如:第一个load执行获取某个地址,第二个load指令取该地址的值),数据依赖屏障会确保第二个load指令在获取目标地址的值的时候,第一个load指令一定以及更新过该地址。
数据依赖屏障仅仅保证相互依赖的load指令的局部顺序,不对stores指令,独立的load指令或者交叉的load指令有影响!
如(1)中提到的,系统中的其它CPU可以被看作能够感知该CPU提交到内存系统的存储(store)指令顺序。由该CPU发出的数据依赖屏障可以确保任何在该屏障之前的load指令,如果该load指令的目标被另一个CPU的存储(store)指令修改,在屏障执行完成之后,所有在该load指令对应的store指令之前的store指令的更新都会被所有在数据依赖屏障之后的load指令感知。
请参阅“内存屏障顺序实例”涨价展示的时序图。
[!]注意:第一个load指令必须是真正数据依赖,而不是控制依赖。如果第二个load指令的目标地址依赖于第一个load,但是这个依赖实际上时依赖一个条件而不是加载的地址本身,那么它是一个控制依赖, 需要一个完整的读屏障或更强的屏障。查看“控制依赖”章节,了解更多信息。
[!]注意:数据依赖屏障一般与写障碍成对出现;看到“SMP屏障配对”章节。
(3)读(或load)内存屏障。
读屏障是数据依赖屏障,并且能确保所有在读屏障之前的load操作一定在所有在读屏障之后的load操作之前执行并让其他系统组件感知。
读屏障仅仅只是保证load指令的部分顺序,不对store指令有任何影响。
读屏障包含数据依赖屏障,因此可以替代他们。
[!]注意:读屏障通常与写屏障成对出现;请参阅“SMP屏障配对”章节。
(4)通用内存屏障。
通用屏障能确保所有在屏障之前的load和store操作一定在所有在屏障之后的load和store操作之前执行并让其他系统组件感知。
通用屏障能保证load和store指令的部分顺序。
通用屏障包含了读屏障和写屏障,所以可以替代他们两者。
一对隐含的类型:
(5)锁(LOCK)操作。
它可以看着是一个单向渗透的屏障。它保证所有在LOCK之后的内存操作一定在锁操作后会发生,并且系统中的其他组件可感知。
在LOCK操作之前内存操作可能会在LOCK 完成之后发生。
锁操作几乎总是与解锁操作成对出现。
(6)解锁(UNLOCK)操作。
这也是一个单向渗透屏障。它保证所有解锁(UNLOCK)操作之前的内存操作一定在解锁(UNLOCK)操作之前发生,并且系统中的其他组件可感知。
在解锁操作之后的内存操作可能会出现解锁UNLOCK发生之前完成。
锁(LOCK)和解锁(UNLOCK)操作各自保证自己对指令的严格顺序。
使用锁和解锁操作,一般不需要使用其他种类的内存屏障(但要注意在“MMIO写屏障”一节中提到的例外)。
仅仅在两个CPU之间或者CPU和其他设备有交互的时候才需要屏障。如果可以确保代码中不会有任何这种交互,那么这段代码是不需要使用内存屏障。
注意:这些是都最低限度的保证。不同的架构可能会提供更多的保证,但是他们不是必须的,不能依赖其写代码。
内存屏障不能确保的事情:
----------------------------------------------
有一些事情,Linux内核的内存屏障并不保证:
(*)不能保证,任何在内存屏障之前的内存访问操作在内存屏障指令执行完成后也执行完成,内存屏障相当于在CPU的访问队列中画一条线,相关的访问请求不能交叉。
(*)不能保证,在一个CPU发出的内存屏障会对另一个CPU或系统其他硬件有任何直接影响。只会间接影响到第二个CPU看到第一个CPU的访问效果的顺序,但请看下一条:
(*)不能保证,一个CPU从第二个CPU的访问中得到的正确顺序,即便第二个CPU使用了内存屏障,除非第一个CPU同样使用匹配的内存屏障(见款SMP屏障配对“)。
(*)不能保证,一些CPU相关的硬件[*]不会对内存访问重排序。 CPU缓存的一致性机制会在多个CPU之间传播内存屏障的间接影响。并且可能不是有序的。
[*]总线主控DMA和一致性的信息,请查阅:
Documentation/PCI/pci.txt
Documentation/PCI/PCI-DMA-mapping.txt
Documentation/DMA-API.txt
数据依赖屏障
------------------------
数据依赖屏障的使用条件有点微妙,并且不是很明确。为了说明问题,考虑下面的事件序列:
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;
这将强制结果是前两种之一,防止第三种可能性。
[!]注意:这种看上去非常违反常理的情况,在有多个独立缓存的系统中是非常常见的。比如:当一个缓存处理偶数编号的缓存行,另外一个缓存处理奇数编号的缓存行。指针P可能被存储在奇数编号的缓存行,变量B可能被存储在偶数编号的缓存行中。然后,如果在读取CPU缓存的时候,偶数编号的缓存非常繁忙,而奇数编号的缓存处于闲置状态,就会出现指针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的指针被替换为一个新的值,而这个新的值还没有完全的初始化。
更多例子参见“高速缓存一致性”章节。
控制依赖
--------------------
控制依赖需要一个完整的读内存屏障,而不是简单的数据依赖屏障,来保证其正确性。考虑下面的代码:
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 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;
基本上,读屏障总是必须在那里,尽管它可以“弱“类型。
[!]注意:写屏障之前的store指令通常与读屏障或数据依赖屏障后的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;
内存屏障序列实例
------------------------------------
首先,写屏障作用与是确保部分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有写屏障,CPU2感知到的CPU1的顺序是随机的:
+-------+ : : : : | | +------+ +-------+ | | |------>| B=2 |----- --->| Y->8 | | | | : +------+ \ +-------+ | (CPU2 感知的更新顺序) | CPU 1 | : | A=1 | \ --->| C->&Y | V | | +------+ | +-------+ | | wwwwwwwwwwwwwwww | : : | | +------+ | : : | | : | C=&B |--- | : : +-------+ | | : +------+ \ | +-------+ | | | |------>| D=4 | ----------->| C->&B |------>| | | | +------+ | +-------+ | | +-------+ : : | : : | | | : : | | | : : | CPU 2 | | +-------+ | | B的赋值显然是错误的 ---> | | B->7 |------>| | | +-------+ | | | : : | | | +-------+ | | 对X的load操作 ---> \ | X->9 |------>| | 延迟对B一致性的维护 \ +-------+ | | ----->| B->2 | +-------+ +-------+ : :
在上述的例子中,尽管load *Ç(也就是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 +-------+ | | : : +-------+
第三,读屏障确保load指定部分有序。考虑以下的事件序列:
CPU 1 CPU 2 ======================= ======================= { A = 0, B = 9 } STORE A=1 <write barrier> STORE B=2 LOAD B LOAD A
没有干预的情况下,CPU 2对CPU 1中事件的感知是随机的,尽管CPU 1有一个写屏障:
+-------+ : : : : | | +------+ +-------+ | |------>| A=1 |------ --->| A->0 | | | +------+ \ +-------+ | CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 | | | +------+ | +-------+ | |------>| B=2 |--- | : : | | +------+ \ | : : +-------+ +-------+ : : \ | +-------+ | | ---------->| B->2 |------>| | | +-------+ | CPU 2 | | | A->0 |------>| | | +-------+ | | | : : +-------+ \ : : \ +-------+ ---->| A->1 | +-------+ : :
然而,如果在CPU2 loadA 和loadB之间放置一个读屏障:
CPU 1 CPU 2 ======================= ======================= { A = 0, B = 9 } STORE A=1 <write barrier> STORE B=2 LOAD B <read barrier> LOAD A
CPU2将可以正确的感知CPU1的顺序:
+-------+ : : : : | | +------+ +-------+ | |------>| 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 +-------+ | | : : +-------+
为了更彻底说明这个问题,考虑读屏障的两侧都用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 +-------+ | | : : +-------+
在读屏障完成之前,CPU2 可能无法感知CPU1中A的更新
+-------+ : : : : | | +------+ +-------+ | |------>| 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,可以保证第二次loadA总是等于 1。但是不能保证第一次load a的值,可能会出现的A == 0或A == 1。
读内存屏障与load预加载
----------------------------------------
许多CPU都会预测并提前加载:即是当系统发现它需要从内存中加载一项数据的时候,系统会寻找没有其他load指令占用总线资源的时候提前加载 - 即使接下去执行的指令还没有到达load指令处。这使的部分load指令可能立即完成,因为CPU已经获得了值。
也可能CPU根本不会使用这个值,因为执行到另外的分支而不需要load - 在这种情况下,它可以丢弃该值或只是缓存供以后使用。
考虑下面的场景:
CPU 1 CPU 2 ======================= ======================= LOAD B DIVIDE } Divide instructions generally DIVIDE } take a long time to perform LOAD A
可能出现:
: : +-------+ +-------+ | | --->| B->2 |------>| | +-------+ | CPU 2 | : :DIVIDE | | +-------+ | | The CPU being busy doing a ---> --->| A->0 |~~~~ | | division speculates on the +-------+ ~ | | LOAD of A : : ~ | | : :DIVIDE | | : : ~ | | Once the divisions are complete --> : : ~-->| | the CPU can then perform the : : | | LOAD with immediate effect : : +-------+
如果在第二个LOAD指令之前,放置一个读屏障或者数据依赖屏障
CPU 1 CPU 2 ======================= ======================= LOAD B DIVIDE DIVIDE <read barrier> LOAD A
这在一定程度上根据使用的不同类型的屏障,需要考虑强制重新获取,如果值没有发送变化,则可以使用预取的值。
: : +-------+ +-------+ | | --->| B->2 |------>| | +-------+ | CPU 2 | : :DIVIDE | | +-------+ | | The CPU being busy doing a ---> --->| A->0 |~~~~ | | division speculates on the +-------+ ~ | | LOAD of A : : ~ | | : :DIVIDE | | : : ~ | | : : ~ | | rrrrrrrrrrrrrrrr~ | | : : ~ | | : : ~-->| | : : | | : : +-------+
但如果另一个CPU更新或者失效之后,就必须重新加载值:
: : +-------+ +-------+ | | --->| B->2 |------>| | +-------+ | CPU 2 | : :DIVIDE | | +-------+ | | The CPU being busy doing a ---> --->| A->0 |~~~~ | | division speculates on the +-------+ ~ | | LOAD of A : : ~ | | : :DIVIDE | | : : ~ | | : : ~ | | rrrrrrrrrrrrrrrrr | | +-------+ | | The speculation is discarded ---> --->| A->1 |------>| | and an updated value is +-------+ | | retrieved : : +-------+
传递性
------------
传递性是对顺序一个深刻直观的概念,但是真实的计算机系统往往并不保证。下面的例子演示传递性(也可称为“积累规律cumulativity”):
CPU 1 CPU 2 CPU 3 ======================= ======================= ======================= { X = 0, Y = 0 } STORE X=1 LOAD X STORE Y=1 <general barrier> <general barrier> LOAD Y LOAD X
假设CPU 2 的load X返回1及load Y返回0。这表明,在某些情况下CPU 2的LOAD X在CPU 1 store X之后,在CPU3 store y 之前。问题是“CPU 3的 load X是否可能返回0?”
因为CPU 2的load X在某种情况下在CPU 1的store之后,我们很自然地希望CPU 3的load X也必须返回1。我们期望的就是传递性的一个例子:如果在CPU A上执行的一个load指令,随后在CPU B 在又对相同变量的load指令,此时CPU A的load和CPU B的load指令必须返回相同的值。
在Linux内核中,使用通用内存屏障保证传递。因此,在上面的例子中,如果从CPU 2的load X指令返回1,并且其load Y返回0,那么CPU 3的load X必须返回1。
但是,读或写屏障不保证传递性。例如,在下面的例子中把在上述例子中的通用屏障改变读屏障,如下所示:
CPU 1 CPU 2 CPU 3 ======================= ======================= ======================= { X = 0, Y = 0 } STORE X=1 LOAD X STORE Y=1 <read barrier> <general barrier> LOAD Y LOAD X
此替换破坏了传递性,在本例中,CPU 2的load X返回1,load Y返回0,但是CPU 3的load X返回0是完全合法的。
关键的问题是,虽然CPU 2的读屏障保证了load指令的顺序,但是它并不能保证CPU 1的store顺序,如果这个例子运行在CPU 1和2共享store 缓冲区或者某一级缓存,CPU 2可能会提前获得到CPU 1写入的值。因此,通用屏障需要确保所有的CPU都能感知CPU 1和CPU 2的访问顺序。
要重申的是,如果你的代码需要传递性,请使用通用屏障。
========================
显式内核屏障
========================
Linux内核有多种不同的屏障,工作在不同的层上:
(*)编译器屏障。
(*)CPU内存屏障。
(*)MMIO写屏障。
编译器屏障
----------------
Linux内核有一个显式的编译器的屏障函数,防止编译器优化时将内存访问从任一侧到另一侧
barrier();
这是一个通用屏障 - 不存在弱类型的编译屏障。
编译屏障并不直接影响CPU,CPU依然可以按照它所希望的重新排序。
CPU内存屏障
-------------------
Linux内核有8个基本的CPU内存屏障:
TYPE MANDATORY SMP CONDITIONAL =============== ======================= =========================== GENERAL mb() smp_mb() WRITE wmb() smp_wmb() READ rmb() smp_rmb() DATA DEPENDENCY read_barrier_depends() smp_read_barrier_depends()
除了数据依赖屏障之外的所有屏障都实现类似编译器屏障的功能。数据依赖关系不对编译器产生的顺序有任务额外的影响。
旁白:在数据依赖的情况下,编译器有望发出正确的load顺序(如:一个'a[b]' 将会在load a[b]之前load b),但在C规范下并不能确保如此,编译器可能预先推测b的值(比如:是否等于1),然后在load b之前load a(如: tmp =a [1];if(b!= 1)TMP = a[b])。即便在编译器load a[b]之后重新load b问题依然存在,因为b拥有比a[b]更新的副本。关于这些问题尚未达成一个共识,然而ACCESS_ONCE宏是解决这个问题很好的开始。
在单处理器编译系统中SMP内存屏障将退化为编译屏障,因为它假定CPU可以保证自身的一致性,并且以正确的顺序访问内存。
[!]注意:SMP内存屏障必须用在SMP系统中来控制引用共享内存的顺序,使用锁也可以满足需求。
强制性屏障不应该被用来控制SMP,因为强制屏障在UP系统中会产生过多不必要的开销。但是,他们可以用于控制在通过松散内存I / O窗口访问时的MMIO操作。即使在非SMP系统中,他们也是必须的,因为它们可以禁止编译器和CPU的重排从而影响内存访问顺序。
下面是更高级的屏障函数:
(*) set_mb(var, value)
这个函数值赋给变量,然后插入一个完整的内存屏障,根据不同的实现。在UP编译器中它不能保证插入编译器屏障之外的屏障。
(*) smp_mb__before_atomic_dec(); (*) smp_mb__after_atomic_dec(); (*) smp_mb__before_atomic_inc(); (*) smp_mb__after_atomic_inc();
这些都为原子加,减,递增和递减而使用的, 函数无返回值,主要用于引用计数。这些函数并不实现内存屏障。
作为一个例子,考虑下面的代码片段,它表示一个对象已经删除, 然后对该对象的引用计数减1:
obj->dead = 1; smp_mb__before_atomic_dec(); atomic_dec(&obj->ref_count);
这可以确保对对象的死亡标志在引用计数递减之前;
更多信息参见Documentation/atomic_ops.txt ,并在Atomic operations 章节介绍了它的使用场景。
(*) smp_mb__before_clear_bit(void); (*) smp_mb__after_clear_bit(void);
这些类似于原子自增,自减屏障。他们典型的应用场景是按位解锁操作,但是必须注意他们也没有实现内存屏障。
考虑通过清除一个lock 位来实现解锁操作。 clear_bit()函数将需要使用内存屏障:
smp_mb__before_clear_bit(); clear_bit( ... );
这可以防止在清除lock标志位之前的内存操作在之后执行。对于UNLOCK的实现见“锁的功能”章节
更多信息见Documentation/atomic_ops.txt ,并且在 “原子操作“章节有关于使用场景的介绍;
MMIO写屏障
------------------
对于映射为内存访问的I / O写操作,Linux内核有一个特殊的障碍;
mmiowb();
这是一个强制性写屏障的变体,能够确保I / O区域部分弱有序。其影响可能超越CPU和硬件之间的接口,在一定程度上实际影响到硬件。
更多信息参见“锁与I / O访问”章节。
===============================
隐式内核内存屏障
===============================
在Linux内核中的一些其他的功能也实现内存屏障,其中包括锁和调度功能。
该规范是一个的最低限度的保证,有些特定的体系结构可能提供更多的保证,但是在特定体系结构之外不能依赖他们。
锁功能:
-----------------
Linux内核有很多锁结构:
(*)自旋锁
(*)R / W自旋锁
(*)互斥
(*)信号量
(*)R / W信号量
(*)RCU
在所有的情况下,他们都是LOCK操作和UNLOCK操作的变种。这些操作都隐含着特定的屏障:
(1)锁定操作的含义:
在LOCK操作之后的内存操作一定在LOCK操作完成之后完成;
在LOCK操作之前的内存操作可能在LOCK操作完成之后完成;
(2)解锁操作的含义:
在UNLOCK操作之前的内存操作一定在UNLOCK操作完成之前完成;
在UNLOCK操作之后的内存操作可能在UNLOCK操作完成之前完成;
(3)锁与锁的含义:
在一个LOCK之前的其他LOCK操作一定在该LOCK完成之前完成;
(4)锁定与解锁的含义:
在一个UNLOCK之前的其他所有LOCK操作一定在该UNLOCK完成之前完成;
在一个LOCK之前的其他所有UNLOCK操作一定在该LOCK完成之前完成;
(5)条件锁失败的含义:
某些锁操作的变种可能会失败,比如可能是由于无法立即获得锁,或者在睡眠等待锁可用的时候收到一个unblocked信号。失败的锁操作不隐含任何形式的屏障。
因此,根据(1),(2)和(4)可以得出一个无条件的LOCK后面跟着一个UNLOCK操作相当于一个完整的屏障,但一个UNLOCK后面跟着一个LOCK却不是;
[!]注意:使用锁的一个结果是,因为LOCK和UNLOCK都只能单向的屏障,关键指令以外的指令可能渗入关键部分里面执行。
一个UNLOCK后面跟着一个LOCK不是一个完成的屏障是因为可能一个在LOCK之前的内存操作发生在LOCK之后,并且一个UNLOCK之后的操作可能在UNLOCK之前发生,两个访问可能再交叉:
*A = a; LOCK UNLOCK *B = b;
可能会发生:
LOCK, STORE *B, STORE *A, UNLOCK
锁和信号量在UP编译系统中不保证任何顺序,所以在这种情况下根本不能考虑为屏障,- 尤其是对于I / O访问 - 除非结合与中断禁用操作。
更多信息请参阅“CPU之间的锁屏障”章节。
考虑下面的例子:
*A = a; *B = b; LOCK *C = c; *D = d; UNLOCK *E = e; *F = f;
以下的顺序是可以接受的:
LOCK, {*F,*A}, *E, {*C,*D}, *B, UNLOCK [+] Note that {*F,*A} indicates a combined access.
但有下列情形的,是不能接受的:
{*F,*A}, *B, LOCK, *C, *D, UNLOCK, *E *A, *B, *C, LOCK, *D, UNLOCK, *E, *F *A, *B, LOCK, *C, UNLOCK, *D, *E, *F *B, LOCK, *C, *D, UNLOCK, {*F,*A}, *E
禁止中断功能
-----------------------------
禁止中断(等价于锁)和允许中断(等价于解锁)可以充当编译屏障。所以,如果某些场景下需要内存或I / O屏障,它们必须通过其他的手段来提供。
休眠和唤醒功能
---------------------------
在一个被标记在全局数据上的某个事件的休眠和唤醒可以被看作是一个是两块数据之间的交互:正在等待的任务的状态和标记这个事件的全局数据。为了确保正确的顺序,进入休眠的原语和唤醒的原语都意味着某些屏障。
首先,通常一个休眠任务执行类似如下的事件序列:
for (;;) { set_current_state(TASK_UNINTERRUPTIBLE); if (event_indicated) break; schedule(); }
当set_current_state()执行后,任务状态发生变化,它会自动插入一个通用内存屏障;
CPU 1 =============================== set_current_state(); set_mb(); STORE current->state <general barrier> LOAD event_indicated
set_current_state()可能包含在下面的函数中:
prepare_to_wait(); prepare_to_wait_exclusive();
因此这些函数也意味在设置状态后有一个通用内存屏障。上面的各个函数有被包含在其他函数中,所有这些函数都在对应的地方插入内存屏障;
wait_event(); wait_event_interruptible(); wait_event_interruptible_exclusive(); wait_event_interruptible_timeout(); wait_event_killable(); wait_event_timeout(); wait_on_bit(); wait_on_bit_lock();
其次,执行正常唤醒的代码如下:
event_indicated = 1; wake_up(&event_wait_queue);
或:
event_indicated = 1; wake_up_process(event_daemon);
类似wake_up()的函数都暗含一个内存屏障。当且仅当他们唤醒某个任务的时候。任务状态被清除之前内存屏障执行,也即是在设置唤醒标志事件的store操作和设置TASK_RUNNING的store操作之间:
CPU 1 CPU 2 =============================== =============================== set_current_state(); STORE event_indicated set_mb(); wake_up(); STORE current->state <write barrier> <general barrier> STORE current->state LOAD event_indicated
可用唤醒函数包括:
complete(); wake_up(); wake_up_all(); wake_up_bit(); wake_up_interruptible(); wake_up_interruptible_all(); wake_up_interruptible_nr(); wake_up_interruptible_poll(); wake_up_interruptible_sync(); wake_up_interruptible_sync_poll(); wake_up_locked(); wake_up_locked_poll(); wake_up_nr(); wake_up_poll(); wake_up_process();
[!]注意:在休眠任务执行set_current_state()之后,唤醒任务load这些store指令之前,休眠任务和唤醒任务的内存屏障都不能保证多个store指令的顺序。例如:休眠函数如下
set_current_state(TASK_INTERRUPTIBLE); if (event_indicated) break; __set_current_state(TASK_RUNNING); do_something(my_data);
和唤醒函数如下:
my_data = value; event_indicated = 1; wake_up(&event_wait_queue);
并不能保证休眠函数在对my_data做过修改之后能够感知到event_indicated的变化。在这种情况下,在两侧的代码中必须在隔离的数据访问中插入它自己的内存屏障。因此,上面的休眠任务应该这样:
set_current_state(TASK_INTERRUPTIBLE); if (event_indicated) { smp_rmb(); do_something(my_data); }
并唤醒者应该做的:
my_data = value; smp_wmb(); event_indicated = 1; wake_up(&event_wait_queue);
其他函数
-----------------------
其他隐含类内存屏障的函数:
(*)schedule()实现类似完整内存屏障。
=================================
CPU之间的锁的屏障效应
=================================
在SMP系统中,锁原语提供了更加丰富的屏障类型:其中一种在特定的锁冲突的情况下会影响其它CPU上的内存访问顺序。
锁与内存访问
------------------------
考虑下面的场景:系统有一对自旋锁(M)、(Q)和三个CPU,然后发生以下的事件序列:
CPU 1 CPU 2 =============================== =============================== *A = a; *E = e; LOCK M LOCK Q *B = b; *F = f; *C = c; *G = g; UNLOCK M UNLOCK Q *D = d; *H = h;
对CPU 3来说从 *A到*H的顺序是没有保证的,不同于单独的锁在单独的CPU上的作用。例如,它可能感知的顺序如下:
*E, LOCK M, LOCK Q, *G, *C, *F, *A, *B, UNLOCK Q, *D, *H, UNLOCK M
但它不会看到任何下面的场景:
*B, *C or *D 在 LOCK M 之前 *A, *B or *C 在 UNLOCK M 之后 *F, *G or *H 在 LOCK Q 之前 *E, *F or *G 在 UNLOCK Q 之后
但是,如果发生以下情况:
CPU 1 CPU 2 =============================== =============================== *A = a; LOCK M [1] *B = b; *C = c; UNLOCK M [1] *D = d; *E = e; LOCK M [2] *F = f; *G = g; UNLOCK M [2] *H = h;
CPU 3可能会看到:
*E, LOCK M [1], *C, *B, *A, UNLOCK M [1], LOCK M [2], *H, *F, *G, UNLOCK M [2], *D
但是,假设CPU 1先得到锁,CPU 3将不会看到任何下面的场景:
*B, *C, *D, *F, *G or *H 在 LOCK M [1] 之前 *A, *B or *C 在 UNLOCK M [1] 之后 *F, *G or *H 在 LOCK M [2] 之前 *A, *B, *C, *E, *F or *G 在 UNLOCK M [2] 之后
锁与 I / O访问
---------------------
在某些情况下(尤其是涉及NUMA)在两个不同CPU上的两个自旋锁临界区内的I / O访问,在PCI桥看来可能是交叉的,因为PCI桥不一定保证缓存一致性,此时内存屏障将失效。
例如:
CPU 1 CPU 2 =============================== =============================== spin_lock(Q) writel(0, ADDR) writel(1, DATA); spin_unlock(Q); spin_lock(Q); writel(4, ADDR); writel(5, DATA); spin_unlock(Q);
PCI桥可能看到的顺序如下所示:
STORE *ADDR = 0, STORE *ADDR = 4, STORE *DATA = 1, STORE *DATA = 5
这可能会导致硬件故障。
此时在释放自旋锁之前需要插入mmiowb()函数,例如:
CPU 1 CPU 2 =============================== =============================== spin_lock(Q) writel(0, ADDR) writel(1, DATA); mmiowb(); spin_unlock(Q); spin_lock(Q); writel(4, ADDR); writel(5, DATA); mmiowb(); spin_unlock(Q);
这将确保在CPU 1上的两次store比CPU 2上的两次store操作先被PCI感知。
此外,相同的设备上如果store指令后跟随一个load指令,可以省去mmiowb()函数,因为load强制在load执行前store指令必须完成
CPU 1 CPU 2 =============================== =============================== spin_lock(Q) writel(0, ADDR) a = readl(DATA); spin_unlock(Q); spin_lock(Q); writel(4, ADDR); b = readl(DATA); spin_unlock(Q);
更多信息参见:Documentation/DocBook/deviceiobook.tmpl
=================================
什么时候需要内存障碍?
=================================
在正常操作情况下,一个单线程代码片段中内存操作重新排序一般不会产生问题,仍然可以正常工作,即使是在一个SMP内核系统中也是如此。但是,下面四种场景下,重新排序肯定会引发问题:
(*)多理器间的交互。
(*)原子操作。
(*)设备访问。
(*)中断。
多理器间的交互
--------------------------
当有一个系统具有一个以上的处理器,系统中多个CPU可能在同一时间访问处理同一数据集。这可能会导致同步的问题,通常处理这种场景使用锁。然而,锁是相当昂贵的,所以如果有其他的选择尽量不使用锁,在这种情况下,多CPU的操作可能要仔细排列的,以防止故障。
例如,在R / W信号量慢的路径的场景。这里有一个waiter进程在信号量上排队,并且它的堆栈上的一块空间链接到信号量上的等待进程列表:
struct rw_semaphore { ... spinlock_t lock; struct list_head waiters; }; struct rwsem_waiter { struct list_head list; struct task_struct *task; };
要唤醒一个特定的waiter进程,up_read()或up_write()函数必须如下:
(1)读取waiter记录的next指针,获取下一个waiter记录的地址;
(2)读取waiter的task结构的指针;
(3)清除task指针,通知waiter已经获取信号量
(4)调用task的wake_up_process()函数;
(5)释放指向waiter的task结构的引用。
换句话说,它必须执行下面的事件:
LOAD waiter->list.next; LOAD waiter->task; STORE waiter->task; CALL wakeup RELEASE task
如果这些步骤的顺序发生任何改变,那么整个事情会出问题。
一旦进程将自己排队并且释放信号锁,waiter将不再获得锁,它只需要等待它的任务指针被清零,然后继续执行。由于记录在waiter的堆栈上,这意味着如果在列表中的next指针读取之前,task指针被清零,另一个CPU可能会开始处理,up*()函数在有机会读取next指针之前waiter的堆栈就被修改。
考虑上述事件序列可能发生什么:
CPU 1 CPU 2 =============================== =============================== down_xxx() Queue waiter Sleep up_yyy() LOAD waiter->task; STORE waiter->task; Woken up by other event <preempt> Resume processing down_xxx() returns call foo() foo() clobbers *waiter </preempt> LOAD waiter->list.next; --- OOPS ---
虽然这中场景可以使用的信号锁处理,但在唤醒后的down_xxx()函数不必要的再次获得自旋锁。
这个问题可以通过插入一个通用的SMP内存屏障处理:
LOAD waiter->list.next; LOAD waiter->task; smp_mb(); STORE waiter->task; CALL wakeup RELEASE task
在这种情况下,即是是在其他的CPU上,屏障确保所有在屏障之前的内存操作一定能够在屏障之后的内存操作先执行,但是它不能确保所有在屏障之前的内存操作一定先于屏障指令身执行完成时执行;
在一个UP系统 , 这种场景不会产生问题 , smp_mb()仅仅是一个编译屏障,可以确保编译器产生正确的顺序,不实际干预CPU,在只有一个CPU的时候,CPU的依赖顺序逻辑会打理好一切;
原子操作
-----------------
虽然它们在技术上考虑了处理器间的交互,但是特别注意,原子操作中的有一些隐含完整的内存屏障,另外一些却不包含,但是他们作为一个整体在内存中应用广泛;
许多原子操作,修改内存中一些状态的值,并返回有关状态(新的或旧的)的信息,这意味着在实际操作(明确的lock操作除外)的两侧插入一个SMP条件通用内存屏障(smp_mb()),包括;
xchg(); cmpxchg(); atomic_cmpxchg(); atomic_inc_return(); atomic_dec_return(); atomic_add_return(); atomic_sub_return(); atomic_inc_and_test(); atomic_dec_and_test(); atomic_sub_and_test(); atomic_add_negative(); atomic_add_unless(); /* when succeeds (returns 1) */ test_and_set_bit(); test_and_clear_bit(); test_and_change_bit();
他们都是用于实现诸如LOCK和UNLOCK的操作,和判断引用计数器决定对象销毁,作为隐含的内存障碍是必要的。
下面的操作存在潜在的问题,因为他们并没有实现内存障碍,但可能被用于执行诸如解锁的操作:
atomic_set(); set_bit(); clear_bit(); change_bit();
如果有必要,这些应使用适当显示内存屏障(例如:smp_mb__before_clear_bit())。
下面这些也没有实现内存屏障,因此在某些场景下可能需要明确的内存屏障(例如:smp_mb__before_atomic_dec()):
atomic_add(); atomic_sub(); atomic_inc(); atomic_dec();
如果他们用于统计,那么他们可能并不需要内存屏障,除非和统计数据之间的有耦合。
如果他们用于对象的引用计数器来控制生命周期,他们也许也不需要内存屏障,因为可能是引用计数内部已经实现了锁,或调用方已经考虑了锁,因此内存屏障不是必须的;
如果他们用于构建一个锁的描述,那么他们可能需要内存屏障,锁原语通用使用特定的顺序来处理这些事情;
基本上,每一个使用场景都必须仔细考虑是否需要内存屏障。
以下操作是特殊的锁原语:
test_and_set_bit_lock(); clear_bit_unlock(); __clear_bit_unlock();
这些实现了诸如LOCK和UNLOCK的操作。在实现锁原语时他们也优先考虑使用,因为他们的实现可以在很多架构中进行了优化。
[!]注意:特殊的内存屏障原语在这些情况下也适用,因为某些CPU上原子指令意味着完整的内存屏障,再使用内存屏障显得多余,在这种情况下的特殊屏障原语将不需要内存屏障。
更多信息见 Documentation/atomic_ops.txt。
设备访问
-----------------
许多设备都可以映射到内存上,因此对CPU来说他们只是一组内存单元。为了控制这些设备,驱动程序通常必须确保正确的顺序来正确访问内存。
然而,聪明的CPU或者聪明的编译器可能为引发潜在的问题,如果CPU或者编译器认为重排、合并、联合访问更加高效,驱动程序精心编排的指令顺序可能在实际访问设备是并不是按照这个必须的顺序访问, 这会导致设备故障。
在Linux内核中,I / O通常需要适当的访问函数 --如: inb() 或者 writel() - 他们知道如何保持适当的顺序。虽然这在大多数情况下明确的使用内存屏障不在必要,但是下面两个场景可能需要:
(1)在某些系统中,I / O储存操作对于所有CPU并不是严格有序,所以对所有的通用设备驱动锁是必须的,并在解锁临界区之前执行mmiowb();
(2)如果储存函数是用来访问一个松散访问属性的I / O存储窗口,那么需要强制内存屏障保证顺序。
更多信息参见 Documentation/DocBook/deviceiobook.tmpl。
中断
----------
一个设备可能由他自己的中断服务程序中断,从而驱动程序两个部分可能会干扰彼此,尝试控制或访问该设备。
通过禁用本地中断(一种锁的形似)可以建减少这种情况,例如,驱动程序中关键的操作都包含在中断禁止的区间中 。虽然驱动程序的中断程序被执行,但是驱动程序的核心不会在相同的CPU上执行,并且直到当前的中断已被处理结束之前不允许其他中断,因此,在中断处理程序并不需要锁。
但是,考虑一个驱动使用地址寄存器和数据寄存器跟以太网卡交互,如果该驱动的核心在中断禁用下与网卡通信,然后驱动程序的中断处理程序被调用:
LOCAL IRQ DISABLE writew(ADDR, 3); writew(DATA, y); LOCAL IRQ ENABLE <interrupt> writew(ADDR, 4); q = readw(DATA); </interrupt>
如果排序规则充分宽松,数据寄存器的存储可能发生在第二次地址寄存器之后:
STORE *ADDR = 3, STORE *ADDR = 4, STORE *DATA = y, q = LOAD *DATA
如果宽松的排序规则,它必须假设中断禁止部分的内存访问可能向外泄漏,它可能会和中断部分交叉访问 - 反之亦然 - 除非使用隐或显示的屏障。
通常情况下,这不会产生问题,因为这样区间中的I / O访问将包括在严格有序的同步load操作中。形成隐式内存屏障,如果这还不够,则显式地使用一个mmiowb()。
类似的情况可能发生在一个中断服务程序和两个运行在单独的CPU上的程序进行通信的的时候。这样的情况下应该使用中断禁用锁来保证顺序。
==========================
内核I / O屏障效应
==========================
访问I / O内存时,驱动应使用适当的存取函数:
(*) inX(), outX():
他们都旨在跟I / O空间打交道,而不是内存空间,但他们主要是一个CPU特定的概念。在 i386和x86_64处理器中确实有特殊的I / O空间的访问周期和指令,但许多CPU没有这样的概念。
包括PCI总线也定义了I / O空间,比如在i386和x86_64的CPU 上很容易将它映射到CPU的I / O空间上。然而,对于那些不支持IO空间的CPU,它也可能作为虚拟的IO空间被映射CPU的的内存空间。
访问这个空间可能是完全同步的(在i386),但桥设备(如PCI主桥)可能不完全履行这一点。
他们彼此之间也完全保证访问顺序;
对于其他类型的内存和I / O操作,他们都不能保证访问顺序。
(*) readX(), writeX():
无论是保证完全有序还是不合并访问取决于他们访问时定义的访问窗口属性,例如,最新的i386架构的机器通过 MTRR寄存器控制。
通常情况下,不是访问预取设备他们保证完全有序,不合并。
然而,对于中间链接硬件(如PCI桥)可能会倾向延迟处理,当刷新一个store时,首先从同一位置load,但是对同一个设备或从同一配置空间load的是,对与PC来说一次就足够了。
[*]注意:试图从刚写过的相同的位置load数据可能导致故障 - 考虑16550 RX / TX串行寄存器的例子。
对于可预取的I / O内存,可能需要一个mmiowb()屏障保证顺序;
请参阅PCI规范获得PCI事务和接口更多信息;
(*)readX_relaxed()
这些类似readX(),但在任何时候都不保证顺序。因为没有I / O读屏障。
(*) ioreadX(), iowriteX()
他们通过选择inX()/outX() or readX()/writeX()来实际操作
========================================
最小可执行的假想顺序模型
========================================
首先假定概念上CPU是弱有序的,同时它又会维护程序因果关系。某些CPU(如i386或x86_64)比其他类型的CPU(如PowerPC的或FRV)受到更多的制约,所以必须假设最宽松的场景(即DEC ALPHA)无关于具体体系结构的代码;
这意味着必须考虑CPU将以任何它喜欢的顺序执行它的指令流甚至是并行的, 如果流中的某个指令依赖前面较早的指令,则该较早的指令必须在后者执行之前完全结束[*],换句话说:保持逻辑关系。
[*]有些指令会产生多个效果 - 如改变条件码,改变寄存器或修改内存 -不同的指令产生不同的影响;
一个CPU也可能会放弃那些最终不产生效果的指令。例如,如果两个相邻的指令加载到同一个寄存器中值,第一个可能被丢弃。
同样地,必须假定编译器可能以任何它认为合适的方式会重新排列指令流,但同样维护程序因果关系。
============================
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核心可以以任何顺序执行指令,有些指令生成lod和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或者Ç中,或它可能仍然驻留在内存中;
(*)偶数编号的缓存行在缓存B或者D中,或它可能仍然驻留在内存中;
(*)当CPU核心正在访问一个cache,其他的cache可能利用总线来访问该系统的其余组件 - 可能是取代脏缓存行或预加载;
(*)每个cache有一个操作队列,用来维持cache与系统其余部分的一致性;
(*)正常load以及存在在缓存行中的数据时一致性队列不会刷新,即使这些load操作可能会潜在的影响队列的内存。
接下去,试想一下,第一个CPU上有两个写操作,并且有一个写屏障在他们之间,以保证他们到达该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 <A:modify v=2> v is now in cache A exclusively p = &v; <B:modify p=&v> p is now in cache B exclusively
写内存屏障强制系统中其他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(); <A:modify v=2> <C:busy> <C:queue v=2> p = &v; q = p; <D:request p> <B:modify p=&v> <D:commit p=&v> <D:read p> x = *q; <C:read *q> Reads from v before v updated in cache <C:unbusy> <C:commit v=2>
基本上,虽然两个缓存行CPU 2在最终都得到更新,但是在不进行干预的情况下不能保证更新的顺序与在CPU 1在提交的顺序一致。
所以我们需要在load之间插入一个的数据依赖性屏障或读屏障。这将迫使缓存在处理其他任务之前强制提交一致性队列;
CPU 1 CPU 2 COMMENT =============== =============== ======================================= u == 0, v == 1 and p == &u, q == &u v = 2; smp_wmb(); <A:modify v=2> <C:busy> <C:queue v=2> p = &v; q = p; <D:request p> <B:modify p=&v> <D:commit p=&v> <D:read p> smp_read_barrier_depends() <C:unbusy> <C:commit v=2> x = *q; <C:read *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缓存的重叠位。
更多信息参见Documentation/cachetlb.txt。
缓存一致性与MMIO
-----------------------
I / O映射的内存通常作为CPU内存空间窗口中的一部分地址,他们与直接访问RAM的的窗口有不同的属性。
这些属性通常是,访问绕过缓存直接进入到设备总线。这意味着MMIO的访问可能先于早些时候发出的访问被缓存的内存的请求到达。在这样的情况下,一个内存屏障还不够,如果缓存的内存写和MMIO访问存在依赖,cache必须刷新;
=========================
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;
在没有写屏障的情况下,可以假定将V写入到*A的操作被丢弃了,同样:
*A = Y; Z = *A;
没有内存屏障,可被简化为:
*A = Y; Z = Y;
在CPU之外根本没有load操作。
ALPHA处理器
--------------------------
DEC Alpha CPU是有最松散的CPU之一。不仅如此,一些版本的Alpha CPU有一个分列的数据缓存,允许他们在不同的时间更新更新语义相关的缓存。在同步多个缓存,保证一致性的时候,数据依赖屏障是必须的,使CPU可以正确的顺序处理指针的变化和数据的获得。
Alpha定义类Linux内核的内存屏障模型。
参见上面的“缓存一致性”章节。
============
示例使用
============
循环缓冲区
----------------
内存屏障可以用来实现循环缓冲,不需要锁来使得生产者与消费者串行。
更多详情参考“Documentation/circular-buffers.txt”
==========
参考
==========
Alpha AXP Architecture Reference Manual, Second Edition (Sites & Witek,
Digital Press)
Chapter 5.2: Physical Address Space Characteristics
Chapter 5.4: Caches and Write Buffers
Chapter 5.5: Data Sharing
Chapter 5.6: Read/Write Ordering
AMD64 Architecture Programmer's Manual Volume 2: System Programming
Chapter 7.1: Memory-Access Ordering
Chapter 7.4: Buffering and Combining Memory Writes
IA-32 Intel Architecture Software Developer's Manual, Volume 3:
System Programming Guide
Chapter 7.1: Locked Atomic Operations
Chapter 7.2: Memory Ordering
Chapter 7.4: Serializing Instructions
The SPARC Architecture Manual, Version 9
Chapter 8: Memory Models
Appendix D: Formal Specification of the Memory Models
Appendix J: Programming with the Memory Models
UltraSPARC Programmer Reference Manual
Chapter 5: Memory Accesses and Cacheability
Chapter 15: Sparc-V9 Memory Models
UltraSPARC III Cu User's Manual
Chapter 9: Memory Models
UltraSPARC IIIi Processor User's Manual
Chapter 8: Memory Models
UltraSPARC Architecture 2005
Chapter 9: Memory
Appendix D: Formal Specifications of the Memory Models
UltraSPARC T1 Supplement to the UltraSPARC Architecture 2005
Chapter 8: Memory Models
Appendix F: Caches and Cache Coherency
Solaris Internals, Core Kernel Architecture, p63-68:
Chapter 3.3: Hardware Considerations for Locks and
Synchronization
Unix Systems for Modern Architectures, Symmetric Multiprocessing and Caching
for Kernel Programmers:
Chapter 13: Other Memory Models
Intel Itanium Architecture Software Developer's Manual: Volume 1:
Section 2.6: Speculation
Section 4.4: Memory Access
相关推荐
### Linux内核内存屏障知识点详解 #### 一、引言 在现代计算机系统尤其是多处理器系统(SMP)中,为了提高性能,处理器通常会采用缓存机制来减少访问主存的时间延迟。然而,这种机制可能导致不同处理器之间数据的...
### Linux内核内存屏障知识点详解 #### 一、内存访问抽象模型 在现代计算机系统中,内存访问操作可能会出现乱序执行的现象。这种现象主要来源于CPU的指令流水线技术,该技术通过并行处理指令的不同阶段来提高...
Linux内核内存管理是操作系统设计中的关键部分,它负责有效地分配和管理系统的物理和虚拟内存。在Linux系统中,内存管理的复杂性在于它需要在多个进程之间共享有限的资源,同时确保系统的稳定性和高效性。以下是...
4. **Linux内核中的应用**:在Linux内核中,内存屏障被广泛用于同步和并发控制,例如在中断处理、设备驱动和锁机制中。谢宝友可能分享了内核代码中的具体实例,展示了如何正确使用内存屏障。 5. **性能影响**:虽然...
总的来说,优化屏障和内存屏障是Linux内核中确保多处理器系统同步和内存一致性的重要工具。它们通过阻止编译器优化和强制处理器按照特定顺序执行内存操作,保证了内核代码的正确性和系统的稳定性。在编写和理解内核...
以上内容仅是Linux内核内存管理机制的一部分,实际的内存管理涉及到许多更复杂的细节和优化,包括内存故障处理、物理内存的预留、伙伴系统扩展等。"Linux物理内存管理.pdf"文档应包含了这些详细信息,通过深入阅读和...
### 内存屏障机制及其在Linux ...Linux内核中提供的多种内存屏障宏为开发者提供了灵活的选择,可以根据具体的应用场景选择合适的内存屏障来确保程序的正确执行。理解并正确使用这些宏是编写高性能并发程序的基础。
为了检测内存错误,Linux内核还引入了内存屏障(Memory Barriers)和锁机制,以保证内存操作的顺序性和一致性。此外,还有内存调试工具如KMemleak,它可以检测到未释放的内存,帮助开发者定位内存泄漏问题。 另外,...
此外,还会涉及内存对齐、内存屏障等概念。 4. **中断和设备驱动**:内核与硬件交互的主要方式是通过中断。书中会介绍中断处理的基本流程,以及如何编写设备驱动程序来控制硬件设备,如网络接口卡、硬盘控制器等。 ...
- **知识点**:探讨了Linux内核中用于确保数据一致性的各种同步机制,如内存屏障等。 - **重要性**:正确使用数据同步技术可以避免数据损坏和一致性问题。 - **第18章:页面回收与交换** - **知识点**:介绍了...
遵循最佳实践,比如使用锁和信号量避免数据竞争,利用内存屏障保证缓存一致性,以及遵循内核编程规范,能有效提高内核质量。此外,持续关注Linux社区的更新,及时应用安全补丁和性能改进,也是保持系统安全和高效...
5. **内存屏障**:在多处理器系统中,内存屏障用于确保数据的一致性,防止指令重排序带来的问题。 6. **内存泄漏检测**:为了保证系统的长期稳定运行,开发者需要了解如何检测和避免内存泄漏,如使用`kmalloc_stats...
Linux内核内存模型中提到的内存屏障,包括了对特定操作的前后条件的控制,其原理是通过强制处理器在执行屏障指令前后的内存操作时,要按照特定的顺序进行,从而防止编译器和CPU的优化重新排列指令,导致程序运行结果...
设计内存屏障指令。 ##### 9.4 SMP结构中的中断机制 - **内容**:描述SMP架构中中断处理的方法。 - **实现**: 1. 设计中断控制器。 2. 支持中断路由和优先级调整。 ##### 9.5 SMP结构中的进程调度 - **内容*...
内存屏障、内存映射、同步机制、GDB基本功能、CPU缓存、内核启动流程、 syncookie、读写分析、NFS实现框架、网络新特性、skb核心操作、HASH算法、过滤框架Nftables、接 收框架、页缓存PageCache、Netfilter框架、...
在Linux内核中,同步与互斥是两个关键的概念,它们用于管理多线程和并发执行的进程,确保数据的一致性和完整性。本篇将深入探讨这两个概念以及它们在Linux内核中的实现。 同步是指在多任务环境中,控制多个线程或...
本文将详细分析`mutex.c`中的核心概念,包括内核抢占、临界区、并发问题以及解决这些问题的同步技术,如原子操作、内存屏障、自旋锁和信号量。 **内核抢占** 内核抢占是操作系统的一个关键特性,允许在内核态运行的...
4. **读写屏障(Memory Barriers)**:在多处理器系统中,由于缓存一致性问题,有时需要插入内存屏障来确保指令的执行顺序。这在处理内核同步时非常关键,可以防止数据的错误排序。 5. **读写复制(Copy-On-Write, ...
#### 三、Memory Barrier在Linux内核中的应用 1. **SMP环境下的使用**:在多处理器环境中,为了保证共享内存区域的一致性,Linux 内核通过内存屏障来实现必要的同步。 2. **与其他技术的关系**: - **Cache一致...