LINUX内核内存屏障
LINUX内核内存屏障
=================
LINUX内核内存屏障
=================
By: David Howells dhowells@redhat.com
Paul E. McKenney paulmck@linux.vnet.ibm.com
译: kouu kouucocu@126.com
出处: Linux内核文档 -- Documentation/memory-barriers.txt
文件夹:
(*) 内存訪问抽象模型.
- 操作设备.
- 保证.
(*) 什么是内存屏障?
- 各式各样的内存屏障.
- 关于内存屏障, 不能假定什么?
- 数据依赖屏障.
- 控制依赖.
- SMP内存屏障的配对使用.
- 内存屏障举例.
- 读内存屏障与内存预取.
(*) 内核中显式的内存屏障.
- 编译优化屏障.
- CPU内存屏障.
- MMIO写屏障.
(*) 内核中隐式的内存屏障.
- 锁相关函数.
- 禁止中断函数.
- 睡眠唤醒函数.
- 其它函数.
(*) 跨CPU的锁的屏障作用.
- 锁与内存訪问.
- 锁与IO訪问.
(*) 什么地方须要内存屏障?
- 处理器间交互.
- 原子操作.
- 訪问设备.
- 中断.
(*) 内核中I/O屏障的作用.
(*) 最小限度有序的假想模型.
(*) CPU cache的影响.
- Cache一致性.
- Cache一致性与DMA.
- Cache一致性与MMIO.
(*) CPU所能做到的.
- 特别值得一提的Alpha处理器.
(*) 使用演示样例.
- 环型缓冲区.
(*) 引用.
================
内存訪问抽象模型
================
考虑例如以下抽象系统模型:
: :
: :
: :
+-------+ : +--------+ : +-------+
| | : | | : | |
| | : | | : | |
| CPU 1 |<----->| 内存 |<----->| CPU 2 |
| | : | | : | |
| | : | | : | |
+-------+ : +--------+ : +-------+
^ : ^ : ^
| : | : |
| : | : |
| : v : |
| : +--------+ : |
| : | | : |
| : | | : |
+---------->| 设备 |<----------+
: | | :
: | | :
: +--------+ :
: :
如果每一个CPU都分别执行着一个会触发内存訪问操作的程序. 对于这样一个CPU, 其内存訪问
顺序是很松散的, 在保证程序上下文逻辑关系的前提下, CPU能够按它所喜欢的顺序来执
行内存操作. 类似的, 编译器也能够将它输出的指令安排成不论什么它喜欢的顺序, 仅仅要保证不
影响程序表面的运行逻辑.
(译注:
内存屏障是为应付内存訪问操作的乱序运行而生的. 那么, 内存訪问为什么会乱序呢? 这里
先简要介绍一下:
如今的CPU一般採用流水线来运行指令. 一个指令的运行被分成: 取指, 译码, 訪存, 运行,
写回, 等若干个阶段.
指令流水线并非串行化的, 并不会由于一个耗时非常长的指令在"运行"阶段呆非常长时间, 而
导致兴许的指令都卡在"运行"之前的阶段上.
相反, 流水线中的多个指令是能够同一时候处于一个阶段的, 仅仅要CPU内部对应的处理部件未被
占满. 比方说CPU有一个加法器和一个除法器, 那么一条加法指令和一条除法指令就可能同
时处于"运行"阶段, 而两条加法指令在"运行"阶段就仅仅能串行工作.
这样一来, 乱序可能就产生了. 比方一条加法指令出如今一条除法指令的后面, 可是因为除
法的运行时间非常长, 在它运行完之前, 加法可能先运行完了. 再比方两条訪存指令, 可能由
于第二条指令命中了cache(或其它原因)而导致它先于第一条指令完毕.
普通情况下, 指令乱序并非CPU在运行指令之前刻意去调整顺序. CPU总是顺序的去内存里
面取指令, 然后将其顺序的放入指令流水线. 可是指令运行时的各种条件, 指令与指令之间
的相互影响, 可能导致顺序放入流水线的指令, 终于乱序运行完毕. 这就是所谓的"顺序流
入, 乱序流出".
指令流水线除了在资源不足的情况下会卡住之外(如前所述的一个加法器应付两条加法指令)
, 指令之间的相关性才是导致流水线堵塞的主要原因.
下文中也会多次提到, CPU的乱序运行并非随意的乱序, 而必须保证上下文依赖逻辑的正
确性. 比方: a++; b=f(a); 因为b=f(a)这条指令依赖于第一条指令(a++)的运行结果, 所以
b=f(a)将在"运行"阶段之前被堵塞, 直到a++的运行结果被生成出来.
假设两条像这样有依赖关系的指令挨得非常近, 后一条指令必然会由于等待前一条运行的结果
, 而在流水线中堵塞非常久. 而编译器的乱序, 作为编译优化的一种手段, 则试图通过指令重
排将这种两条指令拉开距离, 以至于后一条指令运行的时候前一条指令结果已经得到了,
那么也就不再须要堵塞等待了.
相比于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操作所感知.
进一步举例说明, 考虑例如以下的操作序列:
CPU 1 CPU 2
=============== ===============
{ A == 1, B == 2, C = 3, P == &A, Q == &C }
B = 4; Q = P;
P = &B D = *Q;
这里有一处明显的数据依赖, 由于在CPU2上, LOAD到D里面的值依赖于从P获取到的地址. 在
操作序列的最后, 以下的几种结果都是有可能出现的:
(Q == &A) 且 (D == 1)
(Q == &B) 且 (D == 2)
(Q == &B) 且 (D == 4)
注意, CPU2决不会将C的值LOAD到D, 由于CPU保证在将P的值装载到Q之后才会运行对*Q的
LOAD操作(译注: 由于存在数据依赖).
操作设备
--------
对于一些设备, 其控制寄存器被映射到一组内存地址集合上, 而这些控制寄存器被訪问的顺
序是至关重要的. 如果, 一个以太网卡拥有一些内部寄存器, 通过一个地址port寄存器(A)
和一个数据port寄存器(D)来訪问它们. 要读取编号为5的内部寄存器, 可能使用例如以下代码:
*A = 5;
x = *D;
可是这可能会表现为下面两个序列之中的一个(译注: 由于从程序表面看, A和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, 重叠的LOAD和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交互时或
CPU与IO设备交互时, 这可能会成为问题. 我们须要一些手段来干预编译器和CPU, 使其限制
指令顺序.
内存屏障就是这种干预手段. 他们能保证处于内存屏障两边的内存操作满足部分有序. (
译注: 这里"部分有序"的意思是, 内存屏障之前的操作都会先于屏障之后的操作, 可是假设
几个操作出如今屏障的同一边, 则不保证它们的顺序. 这一点下文将多次提到.)
这种强制措施是很重要的, 由于系统中的CPU和其它设备能够使用各种各样的策略来提
高性能, 包含对内存操作的乱序, 延迟和合并运行; 预取; 投机性的分支预測和各种缓存.
内存屏障用于禁用或抑制这些策略, 使代码可以清楚的控制多个CPU和/或设备的交互.
各式各样的内存屏障
------------------
内存屏障有四种基本类型:
(1) 写(STORE)内存屏障.
写内存屏障提供这种保证: 全部出如今屏障之前的STORE操作都将先于全部出如今屏
障之后的STORE操作被系统中的其它组件所感知.
写屏障仅保证针对STORE操作的部分有序; 不要求对LOAD操作产生影响.
随着时间的推移, 一个CPU提交的STORE操作序列将被存储系统所感知. 全部在写屏障
之前的STORE操作将先于全部在写屏障之后的STORE操作出如今被感知的序列中.
[!] 注意, 写屏障一般须要与读屏障或数据依赖屏障配对使用; 參阅"SMP内存屏障配
对"章节. (译注: 由于写屏障仅仅保证自己提交的顺序, 而无法干预其它代码读内
存的顺序. 所以配对使用非常重要. 其它类型的屏障亦是同理.)
(2) 数据依赖屏障.
数据依赖屏障是读屏障的弱化版本号. 如果有两个LOAD操作的场景, 当中第二个LOAD操
作的结果依赖于第一个操作(比方, 第一个LOAD获取地址, 而第二个LOAD使用该地址去
取数据), 数据依赖屏障确保在第一个LOAD获取的地址被用于訪问之前, 第二个LOAD的
目标内存已经更新.
(译注: 由于第二个LOAD要使用第一个LOAD的结果来作为LOAD的目标, 这里存在着数
据依赖. 由前面的"保证"章节可知, 第一个LOAD必然会在第二个LOAD之前运行, 不需
要使用读屏障来保证顺序, 仅仅须要使用数据依赖屏障来保证内存已刷新.)
数据依赖屏障仅保证针对相互依赖的LOAD操作的部分有序; 不要求对STORE操作,
独立的LOAD操作, 或重叠的LOAD操作产生影响.
正如(1)中所提到的, 在一个CPU看来, 系统中的其它CPU提交到内存系统的STORE操作
序列在某一时刻能够被其感知到. 而在该CPU上触发的数据依赖屏障将保证, 对于在屏
障之前发生的LOAD操作, 假设一个LOAD操作的目标被其它CPU的STORE操作所改动, 那
么在屏障完毕之时, 这个相应的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之前的内存操作可能在LOCK完毕之后才发生.
LOCK操作总是跟UNLOCK操作配对出现的.
(6) UNLOCK操作.
它的作用也相当于一个单向渗透屏障. 它保证全部出如今UNLOCK之前的内存操作都将
在UNLOCK操作被系统中的其它组件所感知之前发生.
出如今UNLOCK之后的内存操作可能在UNLOCK完毕之前就发生了.
须要保证LOCK和UNLOCK操作严格依照相互影响的正确顺序出现.
(译注: LOCK和UNLOCK的这样的单向屏障作用, 确保临界区内的訪存操作不能跑到临界区
外, 否则就起不到"保护"作用了.)
使用LOCK和UNLOCK之后, 一般就不再须要其它内存屏障了(可是注意"MMIO写屏障"章节
中所提到的例外).
仅仅有在存在多CPU交互或CPU与设备交互的情况下才可能须要用到内存屏障. 假设能够确保某
段代码中不存在这种交互, 那么这段代码就不须要使用内存屏障. (译注: CPU乱序运行指
令, 相同会导致寄存器的存取顺序被打乱, 可是为什么不须要寄存器屏障呢?
就是由于寄存
器是CPU私有的, 不存在跟其它CPU或设备的交互.)
注意, 对于前面提到的最低限度保证. 不同的体系结构可能提供很多其它的保证, 可是在特定体
系结构的代码之外, 不能依赖于这些额外的保证.
关于内存屏障, 不能假定什么?
---------------------------
Linux内核的内存屏障不保证以下这些事情:
(*) 在内存屏障之前出现的内存訪问不保证在内存屏障指令完毕之前完毕; 内存屏障相当
于在该CPU的訪问队列中画一条线, 使得相关訪存类型的请求不能相互跨越. (译注:
用于实现内存屏障的指令, 其本身并不作为參考对象, 其两边的訪存操作才被当作參
考对象. 所以屏障指令运行完毕并不表示出如今屏障之前的訪存操作已经完毕. 而如
果屏障之后的某一个訪存操作已经完毕, 则屏障之前的全部訪存操作必然都已经完毕
了.)
(*) 在一个CPU上运行的内存屏障不保证会直接影响其它系统中的CPU或硬件设备. 仅仅会间
接影响到第二个CPU感知第一个CPU产生訪存效果的顺序, 只是请看下一点:
(*) 不能保证一个CPU可以按顺序看到还有一个CPU的訪存效果, 即使还有一个CPU使用了内存屏
障, 除非这个CPU也使用了与之配对的内存屏障(參阅"SMP内存屏障的配对使用"章节).
(*) 不保证一些与CPU相关的硬件不会乱序訪存. CPU cache一致性机构会在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;
<写屏障>
P = &B
Q = P;
D = *Q;
这里有明显的数据依赖, 在序列运行完之后, Q的值一定是&A和&B之中的一个, 也就是:
(Q == &A) 那么 (D == 1)
(Q == &B) 那么 (D == 4)
可是! CPU 2可能在看到P被更新之后, 才看到B被更新, 这就导致以下的情况:
(Q == &B) 且 (D == 2) ?
?
?
?
尽管这看起来似乎是一个一致性错误或逻辑关系错误, 但事实上不是, 而且在一些真实的CPU
中就能看到这种行为(就比方DEC Alpha).
为了解决问题, 必须在取地址和取数据之间插入一个数据依赖或更强的屏障:
CPU 1 CPU 2
=============== ===============
{ A == 1, B == 2, C = 3, P == &A, Q == &C }
B = 4;
<写屏障>
P = &B
Q = P;
<数据依赖屏障>
D = *Q;
这将强制终于结果是前两种情况之中的一个, 而避免出现第三种情况.
[!] 注意, 这样的很违反直觉的情况最easy出如今cache分列的机器上, 比方, 一个cache组
处理偶数号的cache行, 还有一个cache组处理奇数号的cache行. P指针可能存储在奇数号
的cache行中, 而B的值可能存储在偶数号的cache行中. 这样一来, 假设运行读操作的
CPU的偶数号cache组很繁忙, 而奇数号cache组空暇, 它就可能看到P已被更新成新值
(&B), 而B还是旧值(2).
还有一个可能须要数据依赖屏障的样例是, 从内存读取一个数值, 用于计算数组的訪问偏移:
CPU 1 CPU 2
=============== ===============
{ M[0] == 1, M[1] == 2, M[3] = 3, P == 0, Q == 3 }
M[1] = 4;
<写屏障>
P = 1
Q = P;
<数据依赖屏障>
D = M[Q];
数据依赖屏障对于RCU很重要, 举例来说. 參阅include/linux/rcupdate.h文件里的
rcu_dereference()函数. 这个函数使得当前RCU指针指向的对象被替换成新的对象时, 不会
发生新对象尚未初始化完毕的情况. (译注: 更新RCU对象时, 一般步骤是: 1-为新对象分配
空间; 2-初始化新对象; 3-调用rcu_dereference()函数, 将对象指针指到新的对象上, 这
就意味着新的对象已生效. 这个过程中假设出现乱序訪存, 可能导致对象指针的更新发生在
新对象初始化完毕之前. 也就是说, 新对象尚未初始化完毕就已经生效了. 那么别的CPU就
可能引用到一个尚未初始化完毕的新对象, 从而出现错误.)
更详尽的样例请參阅"Cache一致性"章节.
控制依赖
--------
控制依赖须要使用一个完整的读内存屏障, 简单的数据依赖屏障不能使其正确工作. 考虑
以下的代码:
q = &a;
if (p)
q = &b;
<数据依赖屏障>
x = *q;
这段代码可能达不到预期的效果, 由于这里事实上并非数据依赖, 而是控制依赖, CPU可能
试图通过提前预測结果而对"if (p)"进行短路. 在这种情况下, 须要的是:
q = &a;
if (p)
q = &b;
<读屏障>
x = *q;
(译注:
比如:
CPU 1 CPU 2
=============== ===============
{ a == 1, b == 2, p == 0}
a = 3;
b = 4;
<写屏障>
p = 1;
q = &a;
if (p)
q = &b;
<数据依赖屏障>
x = *q;
CPU 1上的写屏障是为了保证这种逻辑: 假设p == 1, 那么必然有a == 3 && b == 4.
可是到了CPU 2, 可能p的值已更新(==1), 而a和b的值未更新, 那么这时数据依赖屏障能够
起作用, 确保x = *q时a和b的值更新. 由于从代码逻辑上说, q跟a或b是有所依赖的, 数据
依赖屏障能保证这些有依赖关系的值都已更新.
然而, 换一个写法:
CPU 1 CPU 2
=============== ===============
{ a == 1, b == 2, p == 0}
p = 1;
<写屏障>
a = 3;
b = 4;
q = &a;
if (p)
q = &b;
<读屏障>
x = *q;
CPU 1上的写屏障是为了保证这种逻辑: 假设a == 3 || b == 4, 那么必然有p == 1.
可是到了CPU 2, 可能a或b的值已更新, 而p的值未更新. 那么这时使用数据依赖屏障就不能
保证p的更新. 由于从代码逻辑上说, p跟不论什么人都没有依赖关系. 这时必须使用读屏障, 以
确保x = *q之前, p被更新.
原文中"短路"的意思就是, 因为p没有数据依赖关系, CPU能够早早获得它的值, 而不必考虑
更新.)
SMP内存屏障的配对使用
---------------------
在处理CPU与CPU的交互时, 相应类型的内存屏障总是应该配对使用. 缺乏适当配对的使用基
本上能够肯定是错误的.
一个写屏障总是与一个数据依赖屏障或读屏障相配对, 尽管通用屏障也可行. 类似的, 一个
读屏障或数据依赖屏障也总是与一个写屏障相配对, 虽然一个通用屏障也相同可行:
CPU 1 CPU 2
=============== ===============
a = 1;
<写屏障>
b = 2; x = b;
<读屏障>
y = a;
或:
CPU 1 CPU 2
=============== ===============
a = 1;
<写屏障>
b = &a; x = b;
<数据依赖屏障>
y = *x;
基本上, 读屏障总是须要用在这些地方的, 虽然能够使用"弱"类型.
[!] 注意, 在写屏障之前出现的STORE操作通常总是期望匹配读屏障或数据依赖屏障之后出
现的LOAD操作, 反之亦然:
CPU 1 CPU 2
=============== ===============
a = 1; }---- --->{ v = c
b = 2; } \ / { w = d
<写屏障> \ <读屏障>
c = 3; } / \ { x = a;
d = 4; }---- --->{ y = b;
内存屏障举例
------------
首先, 写屏障用作部分有序的STORE操作. 考虑例如以下的操作序列:
CPU 1
=======================
STORE A = 1
STORE B = 2
STORE C = 3
<写屏障>
STORE D = 4
STORE E = 5
这个操作序列会按顺序被提交到内存一致性系统, 而系统中的其它组件可能看到
{ STORE A, STORE B, STORE C }的组合出如今{ STORE D, STORE E }的组合之前, 而组合
内部可能乱序:
+-------+ : :
| | +------+
| |------>| C=3 | } /\
| | : +------+ }----- \ -----> 操作被系统中的其它
| | : | A=1 | } / 组件所感知
| | : +------+ }
| CPU 1 | : | B=2 | }
| | +------+ }
| | wwwwwwwwwwwwwwww } <--- 在这一时刻, 写屏障要求在它之
| | +------+ } 前出现的STORE操作都先于在它
| | : | E=5 | } 之后出现的STORE操作被提交
| | : +------+ }
| |------>| D=4 | }
| | +------+
+-------+ : :
|
| CPU 1发起的STORE操作被提交到内存系统的顺序
|
V
其次, 数据依赖屏障用作部分有序的数据依赖LOAD操作. 考虑例如以下的操作序列:
CPU 1 CPU 2
======================= =======================
{ B = 7; X = 9; Y = 8; C = &Y }
STORE A = 1
STORE B = 2
<写屏障>
STORE C = &B LOAD X
STORE D = 4 LOAD C (得到&B)
LOAD *C (读取B)
没有干预的话, CPU 1的操作被CPU 2感知到的顺序是随机的, 虽然CPU 1运行了写屏障:
+-------+ : : : :
| | +------+ +-------+ | CPU 2所示
| |------>| B=2 |----- --->| Y->8 | | 更新序列
| | : +------+ \ +-------+ |
| CPU 1 | : | A=1 | \ --->| C->&Y | V
| | +------+ | +-------+
| | wwwwwwwwwwwwwwww | : :
| | +------+ | : :
| | : | C=&B |--- | : : +-------+
| | : +------+ \ | +-------+ | |
| |------>| D=4 | ----------->| C->&B |------>| |
| | +------+ | +-------+ | |
+-------+ : : | : : | |
| : : | |
| : : | CPU 2 |
| +-------+ | |
对B的取值显然不对 ---> | | B->7 |------>| |
| +-------+ | |
| : : | |
| +-------+ | |
对X的LOAD延误了B的 ---> \ | X->9 |------>| |
一致性更新 \ +-------+ | |
----->| B->2 | +-------+
+-------+
: :
在上面的样例中, CPU 2看到的B的值是7, 虽然对*C(值应该是B)的LOAD发生在对C的LOAD之
后.
可是, 如果一个数据依赖屏障被放到CPU 2的LOAD C和LOAD *C(如果值是B)之间:
CPU 1 CPU 2
======================= =======================
{ B = 7; X = 9; Y = 8; C = &Y }
STORE A = 1
STORE B = 2
<写屏障>
STORE C = &B LOAD X
STORE D = 4 LOAD C (获得&B)
<数据依赖屏障>
LOAD *C (读取B)
那么以下的情况将会发生:
+-------+ : : : :
| | +------+ +-------+
| |------>| B=2 |----- --->| Y->8 |
| | : +------+ \ +-------+
| CPU 1 | : | A=1 | \ --->| C->&Y |
| | +------+ | +-------+
| | wwwwwwwwwwwwwwww | : :
| | +------+ | : :
| | : | C=&B |--- | : : +-------+
| | : +------+ \ | +-------+ | |
| |------>| D=4 | ----------->| C->&B |------>| |
| | +------+ | +-------+ | |
+-------+ : : | : : | |
| : : | |
| : : | CPU 2 |
| +-------+ | |
| | X->9 |------>| |
| +-------+ | |
确保STORE C之前的影响 ---> \ ddddddddddddddddd | |
都被兴许的LOAD操作感 \ +-------+ | |
知到 ----->| B->2 |------>| |
+-------+ | |
: : +-------+
第三, 读屏障用作部分有序的LOAD操作. 考虑例如以下事件序列:
CPU 1 CPU 2
======================= =======================
{ A = 0, B = 9 }
STORE A=1
<写屏障>
STORE B=2
LOAD B
LOAD A
没有干预的话, CPU 1的操作被CPU 2感知到的顺序是随机的, 虽然CPU 1运行了写屏障:
+-------+ : : : :
| | +------+ +-------+
| |------>| A=1 |------ --->| A->0 |
| | +------+ \ +-------+
| CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 |
| | +------+ | +-------+
| |------>| B=2 |--- | : :
| | +------+ \ | : : +-------+
+-------+ : : \ | +-------+ | |
---------->| B->2 |------>| |
| +-------+ | CPU 2 |
| | A->0 |------>| |
| +-------+ | |
| : : +-------+
\ : :
\ +-------+
---->| A->1 |
+-------+
: :
可是, 假设一个读屏障被放到CPU 2的LOAD B和LOAD A之间:
CPU 1 CPU 2
======================= =======================
{ A = 0, B = 9 }
STORE A=1
<写屏障>
STORE B=2
LOAD B
<读屏障>
LOAD A
那么CPU 1所施加的部分有序将正确的被CPU 2所感知:
+-------+ : : : :
| | +------+ +-------+
| |------>| A=1 |------ --->| A->0 |
| | +------+ \ +-------+
| CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 |
| | +------+ | +-------+
| |------>| B=2 |--- | : :
| | +------+ \ | : : +-------+
+-------+ : : \ | +-------+ | |
---------->| B->2 |------>| |
| +-------+ | CPU 2 |
| : : | |
| : : | |
在这一时刻, 读屏障导致 ----> \ rrrrrrrrrrrrrrrrr | |
STORE B之前的影响都被 \ +-------+ | |
CPU 2所感知 ---->| A->1 |------>| |
+-------+ | |
: : +-------+
为了更全面地说明这一点, 考虑一下假设代码在读屏障的两边都有一个LOAD A的话, 会发生
什么:
CPU 1 CPU 2
======================= =======================
{ A = 0, B = 9 }
STORE A=1
<写屏障>
STORE B=2
LOAD B
LOAD A [第一次LOAD A]
<读屏障>
LOAD A [第二次LOAD A]
虽然两次LOAD A都发生在LOAD B之后, 它们也可能得到不同的值:
+-------+ : : : :
| | +------+ +-------+
| |------>| A=1 |------ --->| A->0 |
| | +------+ \ +-------+
| CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 |
| | +------+ | +-------+
| |------>| B=2 |--- | : :
| | +------+ \ | : : +-------+
+-------+ : : \ | +-------+ | |
---------->| B->2 |------>| |
| +-------+ | CPU 2 |
| : : | |
| : : | |
| +-------+ | |
| | A->0 |------>| 一次 |
| +-------+ | |
在这一时刻, 读屏障导致 ----> \ rrrrrrrrrrrrrrrrr | |
STORE B之前的影响都被 \ +-------+ | |
CPU 2所感知 ---->| A->1 |------>| 二次 |
+-------+ | |
: : +-------+
可是也可能CPU 2在读屏障结束之前就感知到CPU 1对A的更新:
+-------+ : : : :
| | +------+ +-------+
| |------>| A=1 |------ --->| A->0 |
| | +------+ \ +-------+
| CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 |
| | +------+ | +-------+
| |------>| B=2 |--- | : :
| | +------+ \ | : : +-------+
+-------+ : : \ | +-------+ | |
---------->| B->2 |------>| |
| +-------+ | CPU 2 |
| : : | |
\ : : | |
\ +-------+ | |
---->| A->1 |------>| 一次 |
+-------+ | |
rrrrrrrrrrrrrrrrr | |
+-------+ | |
| A->1 |------>| 二次 |
+-------+ | |
: : +-------+
这里仅仅保证, 假设LOAD B得到的值是2的话, 第二个LOAD A能得到的值是1. 对于第一个
LOAD A是不存在这种保证的; 它可能得到A的值是0或是1.
读内存屏障与内存预取
--------------------
很多CPU会对LOAD操作进行预取: 作为性能优化的一种手段, 当CPU发现它们将要从内存LOAD
一个数据时, 它们会寻找一个不须要使用总线来进行其它LOAD操作的时机, 用于LOAD这个数
据 - 虽然他们的指令运行流程实际上还没有到达该处LOAD的地方. 实际上, 这可能使得某
些LOAD指令可以马上完毕, 由于CPU已经预取到了所须要LOAD的值.
这也可能出现CPU实际上用不到这个预取的值的情况 - 可能由于一个分支而避开了这次LOAD
- 在这种情况下, CPU能够丢弃这个值或者干脆就缓存它以备兴许使用.
考虑例如以下场景:
CPU 1 CPU 2
======================= =======================
LOAD B
DIVIDE } 除法指令通常消耗
DIVIDE } 非常长的运行时间
LOAD A
这可能将表现为例如以下情况:
: : +-------+
+-------+ | |
--->| B->2 |------>| |
+-------+ | CPU 2 |
: :DIVIDE | |
+-------+ | |
CPU在运行除法指令 ---> -->| A->0 |~~~~ | |
的同一时候, 预取A +-------+ ~ | |
(译注: 此时总线空暇) : : ~ | |
: :DIVIDE | |
: : ~ | |
一旦除法结束, --> : : ~-->| |
CPU能立即使 : : | |
LOAD指令生效 : : +-------+
假设在第二个LOAD之前放一个读屏障或数据依赖屏障:
CPU 1 CPU 2
======================= =======================
LOAD B
DIVIDE
DIVIDE
<读屏障>
LOAD A
这在一定程度上将迫使预取所获得的值, 依据屏障的类型而被又一次考虑. 假设没有新的更新
操作作用到已经被预取的内存地址, 则预取到的值就会被使用:
: : +-------+
+-------+ | |
--->| B->2 |------>| |
+-------+ | CPU 2 |
: :DIVIDE | |
+-------+ | |
CPU在运行除法指令 ---> -->| A->0 |~~~~ | |
的同一时候, 预取A +-------+ ~ | |
: : ~ | |
: :DIVIDE | |
: : ~ | |
: : ~ | |
rrrrrrrrrrrrrrrr~ | |
: : ~ | |
: : ~-->| |
: : | |
: : +-------+
可是, 假设存在一个来自于其它CPU的更新或失效, 那么预取将被取消, 而且又一次加载值:
: : +-------+
+-------+ | |
--->| B->2 |------>| |
+-------+ | CPU 2 |
: :DIVIDE | |
+-------+ | |
CPU在运行除法指令 ---> -->| A->0 |~~~~ | |
的同一时候, 预取A +-------+ ~ | |
: : ~ | |
: :DIVIDE | |
: : ~ | |
: : ~ | |
rrrrrrrrrrrrrrrrr | |
+-------+ | |
预取被丢弃, 而且更 --> -->| A->1 |------>| |
新后的值被又一次获取 +-------+ | |
: : +-------+
====================
内核中显式的内存屏障
====================
linux内核拥有各式各样的屏障, 作用在不同层次上:
(*) 编译优化屏障.
(*) CPU内存屏障.
(*) MMIO写屏障.
编译优化屏障
------------
Linux内核有一个显式的编译器屏障函数, 可以防止编译器优化将訪存操作从它的任一側移
到还有一側:
barrier();
这是一个通用屏障 - 弱类型的编译优化屏障并不存在.
编译优化屏障并不直接作用到CPU, CPU依旧能够按其意愿乱序运行代码.
(译注:
既然编译优化屏障并不能限制CPU的乱序訪存, 那么单纯的编译优化屏障能起到什么作用呢?
以内核中的preempt_disable宏为例:
#define preempt_disable() \
do { \
inc_preempt_count(); \
barrier(); \
} while (0)
preempt_disable()和相应的preempt_enable()之间的代码是禁止内核抢占的, 通过对当前
进程的preempt_count进行++, 以标识进入禁止抢占状态(preempt_count==0时可抢占). 这
里在对preempt_count自增之后, 使用了编译优化屏障.
假设不使用屏障, 本该在不可抢占状态下运行的指令可能被重排到preempt_count++之前(因
为这些指令基本上不会对preempt_count有依赖). 而抢占可能是由中断处理程序来触发的,
在那些应该在不可抢占状态下运行的指令被运行之后, preempt_count++之前, 可能发生中
断. 中断来了, preempt_count的值还是0, 于是进程可能会被错误的抢占掉.
究其原因, 是由于编译器看到的上下文依赖逻辑是静态的, 它不知道这段代码跟中断处理程
序还存在依赖关系, 所以没法限制自己的乱序行为. 所以, 这里的编译优化屏障是必要的.
可是, 只使用编译优化屏障就足够了么? 是的, 由于preempt_count这个变量是属于当前
进程的, 仅会被当前CPU訪问.
CPU乱序可能导致后面应该在禁止抢占状态下运行的指令先于preempt_disable()运行完, 但
是没有关系, 由于前面也提到过, CPU是"顺序流入, 乱序流出"的, 就算后面的指令先运行
完, preempt_disable()也必然已经存在于流水线中了, CPU知道preempt_count变量将要被
改动. 而触发抢占的代码肯定会检查preempt_count是否为0, 而这里的检查又将依赖于
preempt_disable()的改动结果, 必然在preempt_disable()完毕之后才会进行.
究其原因, 是由于CPU看到的上下文依赖逻辑是动态的, 它无论指令是来自于普通的处理流
程, 还是来自于中断处理程序, 仅仅要指令存在依赖, 它都能发现. 所以, 对于类似这种仅仅
被一个CPU所关注的内存訪问, CPU的乱序訪存并不会存在问题.
)
CPU内存屏障
-----------
Linux内核有8种主要的CPU内存屏障:
类型 强制 SMP环境
=============== ======================= ===========================
通用 mb() smp_mb()
写 wmb() smp_wmb()
读 rmb() smp_rmb()
数据依赖 read_barrier_depends() smp_read_barrier_depends()
(译注: 这里所说有SMP屏障是仅仅在SMP环境下才生效的屏障, 而强制屏障则是无论在不在SMP
环境下都生效的屏障. 这里所谓的SMP环境, 确切的说, 事实上是内核的编译选项指定为SMP的
情况, 并非指实际执行内核的机器的环境. 只是既然编译选项指定了SMP环境, 那么编译
生成的内核也基本上将会执行在SMP环境. 以下提到的UP环境亦是同理.)
除了数据依赖屏障之外, 全部的内存屏障都隐含了编译优化屏障的功能. 数据依赖屏障不正确
编译器输出的代码顺序造成不论什么额外的影响.
注: 在存在数据依赖关系的情况下, 编译器预期会将LOAD指令按正确的顺序输出(比如, 在
a[b]
语句中, 对b的load必须放在对a[b]的load之前), 但在C规范下, 并不保证编译器不
去预測B的值(比方预測它等于1), 于是先load a再load b(比方,
tmp = a[1]; if (b != 1) tmp = a[b]😉. 编译器在load a[b]之后又又一次load b, 也可能
会存在问题, 由于b拥有比a[b]更新的副本. 这些问题的解决尚未达成共识, 然而内核中的
ACCESS_ONCE宏是解决这个问题的一个好的開始.
在UP系统中, SMP内存屏障将退化成编译器优化屏障, 由于它假定CPU可以保证自身的一致性
, 并本身就能以正确的顺序处理重叠的内存訪问.
[!] 注意, SMP内存屏障必须用于控制在SMP系统中的共享内存的引用顺序, 而使用锁也可以
满足需求.
强制屏障不应该用来控制SMP的影响, 由于强制屏障会过多地添加UP系统的开销. 只是, 在
使用MMIO来訪问松散属性的IO内存窗体时, 强制屏障能够用来控制这些訪存的影响. (译注:
这里所指的内存窗体, 是假定对于CPU来说, 能够设置属于不同区间的内存地址拥有不同的
属性. 这些属性能够指示一个内存段能否够松散訪问, 即乱序訪问.) 强制屏障即使在非
SMP环境下也可能须要, 由于它们能够通过禁止编译器和CPU的乱序訪存, 从而影响设备感知
到内存操作的顺序.
另一些更高级的屏障函数:
(*) set_mb(var, value)
该函数将value赋值到var变量中, 然后取决于详细编译參数下的函数实现, 可能在之
后插入一个内存屏障. 在UP系统中, 它不能保证会插入编译优化屏障以外的其它屏障.
(*) smp_mb__before_atomic_dec();
(*) smp_mb__after_atomic_dec();
(*) smp_mb__before_atomic_inc();
(*) smp_mb__after_atomic_inc();
它们跟一些进行原子操作的函数配合使用, 这些函数进行了原子加法, 减法, 自增和
自减, 而又不将原子变量的值返回, 特别被用于引用计数. 这些原子操作本身并不隐
含内存屏障. (译注: 像这样被操作的原子变量, 多半是孤立而没有数据依赖的. 假设
有数据依赖, 那么依赖关系将在一定程度上限制CPU的乱序. 否则, CPU的乱序就全然
要靠内存屏障来限制了.)
举个样例, 考虑例如以下代码段, 它将object标识为已删除, 然后将其引用计数自减:
obj->dead = 1;
smp_mb__before_atomic_dec();
atomic_dec(&obj->ref_count);
这样能够确保设置删除标记在自减引用计数之前生效.
很多其它信息请參阅Documentation/atomic_ops.txt. 想知道什么地方须要用到这些函数,
參阅"原子操作"章节.
(*) smp_mb__before_clear_bit(void);
(*) smp_mb__after_clear_bit(void);
它们的用途类似于原子加减的屏障. 它们一般是跟一些进行按位解锁操作的函数配合
使用, 必须小心, 由于位操作本身也并不隐含内存屏障.
考虑这样一个场景, 程序通过清除锁定位来实施一些解锁性质的操作. clear_bit()函
数须要像这种屏障:
smp_mb__before_clear_bit();
clear_bit( ... );
这样能够防止应该在锁定位被清除之前发生的内存操作漏到位清除之后去(译注: 注意
UNLOCK的屏障作用就是要保证它之前的訪存操作一定先于它而完毕). 关于UNLOCK操作
的实现, 请參阅"锁相关函数"章节.
很多其它信息请參阅Documentation/atomic_ops.txt. 想知道什么地方须要用到这些函
数, 參阅"原子操作"章节.
MMIO写屏障
----------
对于内存映射IO的写操作, Linux内核另一个特别的屏障:
mmiowb();
这是一个强制写屏障的变体, 可以将弱有序的IO内存窗体变成部分有序. 它的作用可能超出
CPU与硬件的界面, 从而影响到很多层次上的硬件设备.
很多其它信息请參阅"锁与IO訪问"章节.
====================
内核中隐式的内存屏障
====================
Linux内核中有一些其它的方法也隐含了内存屏障, 包含锁和调度方法.
这个范围是一个最低限度的保证; 一些特定的体系结构可能提供很多其它的保证, 可是在特定体
系结构的代码之外, 不能依赖于它们.
锁相关函数
----------
Linux内核有非常多锁结构:
(*) spin locks
(*) R/W spin locks
(*) mutexes
(*) semaphores
(*) R/W semaphores
(*) RCU
在全部情况下, 它们都是LOCK操作和UNLOCK操作的变种. 这些操作都隐含一定的屏障:
(1) LOCK操作所隐含的:
在LOCK操作之后出现的内存操作, 一定在LOCK操作完毕之后才会完毕.
而在LOCK操作之前出现的内存操作, 可能在LOCK操作完毕之后才完毕.
(2) UNLOCK操作所隐含的:
在UNLOCK操作之前出现的内存操作, 一定在UNLOCK操作完毕之前完毕.
而在UNLOCK操作之后出现的内存操作, 可能在LOCK操作完毕之前就完毕了.
(3) LOCK操作+LOCK操作所隐含的:
在某个LOCK操作之前出现的全部LOCK操作都将在这个LOCK之前完毕.
(4) LOCK操作+UNLOCK操作所隐含的:
在UNLOCK操作之前出现的全部LOCK操作都将在这个UNLOCK之前完毕.
在LOCK操作之前出现的全部UNLOCK操作都将在这个LOCK之前完毕.
(5) LOCK失败所隐含的:
某些变种的LOCK操作可能会失败, 比方可能由于不能立马获得锁(译注: 如try_lock操
作), 再比方由于在睡眠等待锁变为可用的过程中接收到了未被堵塞的信号(译注: 如
semaphores的down_interruptible操作). 失败的锁操作不隐含不论什么屏障.
因此, 依据(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环境下可能不提供顺序保证, 在这样的情况下不能被认作是真正的屏障 - 特
别是对于IO訪问 - 除非结合中断禁用操作.
參阅"跨CPU的锁的屏障作用"章节.
比如, 考虑例如以下代码:
*A = a;
*B = b;
LOCK
*C = c;
*D = d;
UNLOCK
*E = e;
*F = f;
例如以下的事件序列都是可接受的:
LOCK, {F,A}, E, {C,*D}, *B, UNLOCK
[+] 注意, {F,A} 代表一次合并訪问.
可是以下的序列都不可接受:
{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
禁止中断函数
------------
禁止中断(类似于LOCK)和启用中断(类似于UNLOCK)的函数仅仅会起到编译优化屏障的作用. 所
以, 假设在这样的情况下须要使用内存或IO屏障, 必须採取其它手段.
睡眠唤醒函数
------------
在一个全局事件标记上的睡眠和唤醒能够被看作是两条数据之间的交互: 正在等待事件的进
程的状态, 和用于表示事件发生的全局数据. 为了确保它们按正确的顺序发生, 进入睡眠的
原语和发起唤醒的原语都隐含了某些屏障.
首先, 睡眠进程通常运行类似于例如以下的代码序列:
for (;😉 {
set_current_state(TASK_UNINTERRUPTIBLE);
if (event_indicated)
break;
schedule();
}
set_current_state()在它更改进程状态之后会自己主动插入一个通用内存屏障:
CPU 1
===============================
set_current_state();
set_mb();
STORE current->state
<通用屏障>
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 <写屏障>
<通用屏障> 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()之后的那些
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的改动. 在这样的情况
下, 两边的代码必须在对my_data訪存之前插入自己的内存屏障. 因此上述的睡眠函数应该
这样做:
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()和类似函数隐含了完整的内存屏障.
(译注: schedule函数完毕了进程的切换, 它的两边可能相应着两个不同的上下文. 如
果訪存操作跨越schedule函数而进行了乱序, 那么基本上能够肯定是错误的.)
===================
跨CPU的锁的屏障作用
===================
在SMP系统中, 锁定原语给出了多种形式的屏障: 当中一种在一些特定的锁冲突的情况下,
会影响其它CPU上的内存訪问顺序.
锁与内存訪问
------------
如果系统中有(M)和(Q)这一对spinlock, 有三个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有
那样的限制. 比如, CPU 3可能看到的顺序是:
*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]
锁与IO訪问
----------
在某些情况下(特别是涉及到NUMA的情况), 两个CPU上发起的属于两个spinlock临界区的IO
訪问可能被PCI桥看成是交错发生的, 由于PCI桥并不一定參与cache一致性协议, 以至于无
法响应读内存屏障.
比如:
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
这可能会引起硬件操作的错误.
这里所须要的是, 在释放spinlock之前, 使用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操作将强制STORE操作在開始LOAD之前就完毕:
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可能在同一时间工作在相同的数据集上. 这
将产生同步问题, 而且这种问题通常要靠使用锁来解决. 可是, 锁是昂贵的, 所以不是万
不得已的情况下最好不要使用锁. 在这样的情况下, 为防止错误, 导致两个CPU相互影响的那
些内存操作可能须要细致协调好顺序.
比方, 考虑一下读写信号量的slow path. 信号量的等待队列里有一个进程正在等待, 这个
等待进程栈空间上的一段内存(译注: 也就是栈上分配的waiter结构)被链到信号量的等待链
表里:
struct rw_semaphore {
...
spinlock_t lock;
struct list_head waiters;
};
struct rwsem_waiter {
struct list_head list;
struct task_struct *task;
};
要唤醒这样一个等待进程, up_read()函数或up_write()函数须要这样做:
(1) 读取该等待进程所相应的waiter结构的next指针, 以记录下一个等待进程是谁;
(2) 读取waiter结构中的task指针, 以获取相应进程的进程控制块;
(3) 清空waiter结构中的task指针, 以表示这个进程正在获得信号量;
(4) 对这个进程调用wake_up_process()函数; 而且
(5) 释放waiter结构对进程控制块的引用计数.
换句话说, 这个过程会运行例如以下事件序列:
LOAD waiter->list.next;
LOAD waiter->task;
STORE waiter->task;
CALL wakeup
RELEASE task
而假设当中一些步骤发生了乱序, 那么整个过程可能会产生错误.
一旦等待进程将自己挂入等待队列, 并释放了信号量里的锁, 这个等待进程就不会再获得这
个锁了(译注: 參阅信号量的代码, 它内部使用了一个spinlock来进行同步); 它要做的事情
就是在继续工作之前, 等待waiter结构中的task指针被清空(译注: 然后自己会被唤醒). 而
既然waiter结构存在于等待进程的栈上, 这就意味着, 假设在waiter结构中的next指针被读
取之前, task指针先被清空了的话(译注: 等待进程先被唤醒了), 那么, 这个等待进程可能
已经在还有一个CPU上開始执行了(译注: 相对于唤醒进程所执行的CPU), 而且在up*()函数有
机会读取到next指针之前, 栈空间上相应的waiter结构可能已经被复用了(译注: 被唤醒的
进程从down*()函数返回, 然后可能进行新的函数调用, 导致栈空间被反复使用).
看看上面的事件序列可能会发生什么:
CPU 1 CPU 2
=============================== ===============================
down_xxx()
将waiter结构链入等待队列
进入睡眠
up_yyy()
LOAD waiter->task;
STORE waiter->task;
被CPU 1的UP事件唤醒
<被抢占了>
又一次得到执行
down_xxx()函数返回
继续调用foo()函数
foo()重用了栈上的waiter结构
<抢占返回>
LOAD waiter->list.next;
--- OOPS ---
对付这个问题能够使用信号量中的锁, 可是当进程被唤醒后, down_xxx()函数事实上不是必需重
新获得这个spinlock.
实际的解决的方法是插入一个通用SMP内存屏障:
LOAD waiter->list.next;
LOAD waiter->task;
smp_mb();
STORE waiter->task;
CALL wakeup
RELEASE task
这样, 对于系统中的其它CPU来说, 屏障将保证屏障之前的全部内存訪问先于屏障之后的所
有内存訪问发生. 屏障并不保证屏障之前的全部内存訪问都在屏障指令结束之前完毕.
在UP系统中 - 这样的情况将不是问题 - smp_mb()函数仅仅是一个编译优化屏障, 这就确保了编
译器生成顺序正确的指令, 而不须要干预CPU. 既然仅仅有一个CPU, 该CPU的数据依赖逻辑将
处理全部事情.
原子操作
--------
尽管原子操作在技术上实现了处理器之间的交互, 然而特别注意一些原子操作隐含了完整的
内存屏障, 而另外一些则没有, 可是它们却作为一个群体被整个内核严重依赖.
很多原子操作改动内存中的一些状态, 而且返回该状态相关的信息(旧状态或新状态), 就在
当中实际操作内存的两边各隐含一个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(); /* 假设成功 (返回 1) */
test_and_set_bit();
test_and_clear_bit();
test_and_change_bit();
它们被用于作为类LOCK和类UNLOCK操作的实现, 和用于控制对象析构的引用计数, 这些情况
下, 隐含内存屏障是有必要的.
下面操作因为没有隐含内存屏障, 会有潜在的问题, 但有可能被用于实现类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();
假设它们用于产生统计, 那么他们可能就不须要内存屏障, 除非统计数据之间存在耦合.
假设它们被用作控制对象生命周期的引用计数, 那么它们可能并不须要内存屏障, 由于要么
引用计数须要在一个锁的临界区里面进行调整, 要么调用者已经持有足够的引用而相当于拥
有了锁(译注: 一般在引用计数减为0的时候须要将相应的对象析构, 假设调用者知道引用计
数在某些情况下不可能减为0, 那么这个对象也就不可能在这些情况下被析构, 也就不须要
通过内存屏障来避免訪存乱序导致的对象在析构之后还被訪问的情况), 这种情况下并不
须要内存屏障.
假设它们用于构成锁的一些描写叙述信息, 那么他们可能就须要内存屏障, 由于锁原语一般须要
按一定的顺序来操作.
基本上, 每一个场景都须要细致考虑是否须要使用内存屏障.
下面操作是特殊的锁原语:
test_and_set_bit_lock();
clear_bit_unlock();
__clear_bit_unlock();
它们都运行了类LOCK和类UNLOCK的操作. 相比其它操作, 它们应该优先被用于实现锁原语,
由于它们的实现能够在很多体系结构下得到优化.
[!] 注意, 这些特殊的内存屏障原语对一些情况也是实用的, 由于在一些体系结构的CPU上,
使用的原子操作本身就隐含了完整的内存屏障功能, 所以屏障指令在这里是多余的, 在
这种情况下, 这些特殊的屏障原语将不使用额外的屏障操作.
很多其它信息请參阅Documentation/atomic_ops.txt.
訪问设备
--------
很多设备都能够被映射到内存, 因此在CPU看来, 它们仅仅是一组内存地址. 为了控制这些设
备, 驱动程序通常须要确保正确的内存訪问按正确的顺序来运行.
可是, 聪明的CPU或者聪明的编译器却导致了潜在的问题, 假设CPU或编译器觉得乱序, 或合
并訪问更有利于效率的话, 驱动程序代码中细致安排的訪存序列可能并不会按正确的顺序被
送到设备上 - 从而可能导致设备的错误.
在Linux内核里面, IO訪问应该使用适当的訪问函数 - 比如inb()或writel() - 它们知道如
何得到恰当的訪问顺序. 大多数情况下, 在使用这些函数之后就不必再显式的使用内存屏障
, 可是在两种情况下, 内存屏障可能还是须要的:
(1) 在一些系统中, IO存储操作对于全部CPU来说并非严格有序的, 所以对于全部的通用
驱动程序(译注: 通用驱动程序须要适应各种体系结构的系统), 须要使用锁, 而且一
定要在解锁临界区之前运行mmiowb()函数.
(2) 假设訪存函数訪问松散属性的IO内存窗体, 那么须要使用强制内存屏障来确保运行顺
序.
很多其它信息请參阅Documentation/DocBook/deviceiobook.tmpl.
中断
----
驱动程序可能被它自己的中断处理程序所打断, 然后驱动程序中的这两个部分可能会相互干
扰对方控制或訪问设备的意图.
通过禁用本地中断(一种形式的锁)可能至少部分缓解这种情况, 这种话, 驱动程序中的关
键操作都将包括在禁用中断的区间中. 于是当驱动程序的中断处理程序正在运行时, 驱动程
序的核心代码不可能在同样的CPU上执行, 而且在当前中断被处理完之前中断处理程序不允
许再次被调用, 于是中断处理程序就不须要再对这样的情况使用锁.
可是, 考虑一个驱动程序正通过一个地址寄存器和一个数据寄存器跟以太网卡交互的情况.
如果驱动程序的核心代码在禁用中断的情况下操作了网卡, 然后驱动程序的中断处理程序
被调用:
LOCAL IRQ DISABLE
writew(ADDR, 3);
writew(DATA, y);
LOCAL IRQ ENABLE
<进入中断>
writew(ADDR, 4);
q = readw(DATA);
<退出中断>
假设运行顺序的规则足够松散, 对数据寄存器的写操作可能发生在第二次对地址寄存器的写
操作之后:
STORE *ADDR = 3, STORE *ADDR = 4, STORE *DATA = y, q = LOAD *DATA
假设运行顺序像这样松散, 就须要假定在禁用中断区间内应该完毕的訪问可能泄漏到区间之
外, 而且可能漏到中断过程中进行訪问 - 反之亦然 - 除非使用隐式或显式的屏障.
通常这并非一个问题, 由于禁用中断区间内完毕的IO訪存将会包括严格有序的同步LOAD操
作, 形成隐式的IO屏障. 假设这还不够, 那么须要显式的调用一下mmiowb().
在一个中断服务程序与两个执行在不同CPU的程序相互通信的情况下, 类似的情况也可能发
生. 假设出现这种情况, 那么禁用中断的锁操作须要用于确保运行顺序. (译注: 也就是
类似于spinlock_irq这种操作.)
===================
内核中I/O屏障的作用
===================
在对IO内存进行存取的时候, 驱动程序应该使用适当的存取函数:
(*) inX(), outX():
它们都是倾向于跟IO空间打交道, 而不是普通内存空间, 只是这主要取决于详细CPU的
逻辑. i386和x86_64处理器确实有特殊的IO空间存取周期和指令, 可是很多系统结构
的CPU却并没有这些概念.
包含PCI总线也可能会定义成IO空间 - 比方在i386和x86_64的CPU上 - 非常easy将它映
射到CPU的IO空间上. 可是, 它也可能作为虚拟的IO空间被映射到CPU的内存空间上,
特别对于那些不支持IO空间的CPU.
訪问这些空间可能是全然同步的(比方在i386上), 可是对于桥设备(比方PCI主桥)可能
并不全然是这样.
他们能保证全然遵守IO操作之间的訪问顺序.
他们不能保证全然遵从IO操作与其它类型的内存操作之间的訪问顺序.
(*) readX(), writeX():
在发起调用的CPU上, 这些函数是否保证全然遵从内存訪问顺序并且不进行合并訪问,
取决于它们所訪问的内存窗体上定义的属性. 比如, 较新的i386体系结构的机器, 可
以通过MTRR寄存器来控制内存窗体的属性.
通常, 仅仅要不是訪问预取设备, 这些函数将保证全然有序而且不进行合并訪问.
可是对于桥设备(比方PCI桥), 假设它们愿意的话, 可能会倾向于对内存操作进行延迟
处理; 要冲刷一个STORE操作, 首选是对同样地址进行一次LOAD[*], 可是对于PCI来说
, 对同样设备或同样的配置的IO空间进行一次LOAD就足够了.
[*] 注意! 试图从刚写过的地址LOAD数据, 可能会导致错误 - 比方对于16550 Rx/Tx
串口寄存器.
遇到带预取的IO内存, 可能须要使用mmiowb()屏障来强制让STORE操作有序.
关于PCI事务交互方面的很多其它信息, 请參阅PCI规范.
(*) readX_relaxed()
这些函数类似于readX(), 可是不论什么情况下都不保证有序. 请注意, 这里没实用到IO读
屏障.
(*) ioreadX(), iowriteX()
这些函数在进行訪存的时候会依据訪存类型选择适当的操作, inX()/outX()或
readX()/writeX().
======================
最小限度有序的假想模型
======================
从概念上说, 必须假定的CPU是弱有序的, 可是它会保持程序上下文逻辑关系的外观. 一些
CPU(比方i386或x86_64)比还有一些(比方powerpc或frv)更具有约束力, 而在体系结构无关的
代码中, 必须假定为最松散的情况(也就是DEC Alpha).
也就是说, 必须考虑到CPU可能会按它喜欢的顺序来运行操作 - 甚至并行运行 - 仅仅是当指
令流中的一条指令依赖于之前的一条指令时, 之前的这条指定才必须在后面这条指令可能被
处理之前全然结束; 换句话说: 保持程序的上下文逻辑关系.
[*] 一些指令会产生不止一处影响 - 比方会改动条件码, 改动寄存器或改动内存 - 不同
的指令可能依赖于不同的影响.
CPU也可能丢弃那些终于不产生不论什么影响的操作序列. 比方, 假设两个相邻的指令都将一个
马上数LOAD到寄存器, 那么第一个LOAD指令可能被丢弃.
类似的, 也须要如果编译器可能按它认为舒服的顺序来调整指令流, 但相同也会保持程序的
上下文逻辑关系.
===============
CPU cache的影响
===============
操作cache中缓存的内存之后, 对应的影响会在整个系统间得到传播. 位于CPU和内存之间的
cache, 和保持系统状态一致的内存一致性机构, 在一定程度上影响了传播的方法.
自从CPU与系统中其它部分的交互通过使用cache来实现以来, 内存系统就包括了CPU的缓存,
而内存屏障基本上就工作在CPU和其cache之间的界面上(逻辑上说, 内存屏障工作在下图中
虚线所看到的的地方):
<--- CPU ---> : <----------- 内存 ----------->
:
+--------+ +--------+ : +--------+ +-----------+
| | | | : | | | | +--------+
| CPU | | 内存 | : | CPU | | | | |
| 核心 |--->| 请求 |----->| Cache |<-->| | | |
| | | 队列 | : | | | |--->| 内存 |
| | | | : | | | | | |
+--------+ +--------+ : +--------+ | | | |
: | Cache | +--------+
: | 一致性 |
: | 机构 | +--------+
+--------+ +--------+ : +--------+ | | | |
| | | | : | | | | | |
| CPU | | 内存 | : | CPU | | |--->| 设备 |
| 核心 |--->| 请求 |----->| Cache |<-->| | | |
| | | 队列 | : | | | | | |
| | | | : | | | | +--------+
+--------+ +--------+ : +--------+ +-----------+
:
:
一些LOAD和STORE可能不会实际出如今发起操作的CPU之外, 由于在CPU自己的cache上就能满
足须要, 虽然如此, 假设其它CPU关心这些数据, 那么完整的内存訪问还是会发生, 由于
cache一致性机构将迁移对应的cache行到訪问它的CPU, 使一致性得到传播.
在保持程序所期望的上下文逻辑的前提下, CPU核心可能会按它觉得合适的顺序来运行指令.
一些指令会产生LOAD和STORE操作, 而且将它们放到内存请求队列中, 等待被运行. CPU核心
可能会按它喜欢的顺序来将这些操作放进队列, 然后继续执行, 直到它必须等待这些訪存指
令完毕的时候为止.
内存屏障所须要关心的是訪存操作从CPU一側穿越到内存一側的顺序, 和系统中的其它部件
感知到的操作发生的顺序.
[!] 对于一个CPU自己的LOAD和STORE来说, 并不须要使用内存屏障, 由于CPU总是能按程序
运行顺序看到它们所运行的LOAD和STORE操作.
[!] MMIO或其它设备存取可能绕开cache系统. 这取决于訪问设备所经过的内存窗体的属性
和/或是否使用了CPU所特有的与设备进行交互的指令.
CACHE一致性
-----------
可是, 事情并非像上面所说的那样简单: 由于尽管能够期望cache是一致的, 可是一致性
传播的顺序却是没有保证的. 也就是说, 尽管一个CPU所做出的更新将终于被其他CPU都看到
, 可是却不保证其它CPU所示都是同样的顺序.
考虑这样一个系统, 它具有双CPU(1和2), 每一个CPU有一对并行的数据cache(CPU 1相应A/B,
CPU 2相应C/D):
:
: +--------+
: +---------+ | |
+--------+ : +--->| Cache A |<------->| |
| | : | +---------+ | |
| CPU 1 |<---+ | |
| | : | +---------+ | |
+--------+ : +--->| Cache B |<------->| |
: +---------+ | |
: | 内存 |
: +---------+ | 系统 |
+--------+ : +--->| Cache C |<------->| |
| | : | +---------+ | |
| CPU 2 |<---+ | |
| | : | +---------+ | |
+--------+ : +--->| Cache D |<------->| |
: +---------+ | |
: +--------+
:
想象一下该系统有例如以下属性:
(*) 一个奇数号的cache行可能被缓存在cache A, cache C, 或者可能依旧驻留在内存中;
(*) 一个偶数号的cache行可能被缓存在cache B, cache D, 或者可能依旧驻留在内存中;
(*) 而当CPU核心訪问一个cache时, 还有一个cache能够同一时候利用总线来訪问系统中的其它部
分 - 可能是替换一个脏的cache行或者进行预取;
(*) 每一个cache都有一个操作队列, 被用于保持cache与系统中的其它部分的一致性;
(*) 当LOAD命中了已经存在于cache中的行时, 该一致性队列并不会得到冲刷, 虽然队列中
的内容可能会影响这些LOAD操作. (译注: 也就是说, 队列中有针对某一cache行的更
新操作正在等待被运行, 而这时LOAD操作须要读这个cache行. 这样的情况下, LOAD并不
会等待队列中的这个更新完毕, 而是直接获取了更新前的值.)
接下来, 想象一下在第一个CPU上运行两个写操作, 并在它们之间使用一个写屏障, 以保证
它们按要求的顺序到达该CPU的cache:
CPU 1 CPU 2 说明
=============== =============== =======================================
u == 0, v == 1 而且 p == &u, q == &u
v = 2;
smp_wmb(); 确保对v的改动先于对p的改动被感知
<A:modify v=2> v的值仅仅存在于cache A中
p = &v;
<B:modify p=&v> p的值仅仅存在于cache B中
写内存屏障保证系统中的其它CPU会按正确的顺序看到本地CPU cache的更新. 可是设想一下
第二个CPU要去读取这些值的情形:
CPU 1 CPU 2 说明
=============== =============== =======================================
...
q = p;
x = *q;
上面这一对读操作可能不会在预期的顺序下运行, 比方持有p的cache行可能被更新到还有一个
CPU的cache, 而持有v的cache行由于其它一些cache事件的影响而延迟了对那个CPU的cache
的更新:
CPU 1 CPU 2 说明
=============== =============== =======================================
u == 0, v == 1 而且 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> 在v被更新到cache之前读取v
<C:unbusy>
<C:commit v=2>
基本上, 尽管终于CPU 2的两个cache行都将得到更新, 可是在没有干预的情况下, 并不能保
证更新的顺序跟CPU 1提交的顺序一致.
我们须要在两次LOAD之间插入一个数据依赖屏障或读屏障, 以作为干预. 这将强制cache在
处理兴许的请求之前, 先让它的一致性队列得到提交:
CPU 1 CPU 2 说明
=============== =============== =======================================
u == 0, v == 1 而且 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> 在v被更新到cache之后读取v
这些问题会在DEC Alpha处理器上遇到, 这些处理器使用了分列cache, 通过提高数据总线的
利用率以提升性能. 尽管大部分的CPU在读操作依赖于读操作的时候, 会在第二个读操作中
隐含一个数依赖屏障, 可是并非全部CPU都这样, 因此不能依赖这一点.
其它的CPU也可能使用分列cache, 但对于普通的内存訪问, 他们会协调各个cache列. 而
Alpha处理器的处理逻辑则取消了这种协调动作, 除非使用内存屏障.
cache一致性与DMA
----------------
对于进行DMA操作的设备, 并非全部系统都保持它们的cache一致性. 在这样的情况下, 准备
进行DMA的设备可能从RAM得到陈旧的数据, 由于脏的cache行可能还驻留在各个CPU的cache
中, 而尚未写回到RAM. 为了解决问题, 内核的对应部分必须将cache中重叠的数据冲刷
掉(或者使它们失效)(译注: 冲刷掉cache中的对应内容, 以保持cache与RAM的一致).
此外, 在设备已经通过DMA将数据写入RAM之后, 这些数据可能被cache写回RAM的脏的cache
行所覆盖, 或者CPU已缓存的cache行可能直接掩盖了RAM被更新的事实(译注: 使得相应的
LOAD操作仅仅能获得cache中的旧值, 而无法得到RAM中的新值), 直到cache行被从CPU cache
中丢弃而且又一次由RAM加载. 为解决问题, 内核的对应部分必须将cache中重叠的数据失
效.
很多其它关于cache管理的信息请參阅: Documentation/cachetlb.txt.
cache一致性与MMIO
-----------------
内存映射IO通常通过内存地址来触发, 这些地址是CPU内存空间的某个窗体中的一部分, 而
这个窗体相比于普通RAM相应的窗体会有着不同的属性.
这些属性通常包括这种情况: 訪存会全然绕过cache, 而直接到达设备总线. 这意味着在
效果上, MMIO可能超越先前发出的对被缓存内存的訪问(译注: 意思是, MMIO后运行, 可是
先到达内存; 而先运行的写内存操作则可能被缓存在cache上, 之后才干冲刷到内存). 这样的
情况下, 假设这两者有某种依赖的话, 使用一个内存屏障并不足够, 而须要在写被缓存内存
和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操作可能更加须要马上完毕以确保程序的运行速度(译注: 由于往往会有兴许指令
须要等待LOAD的结果), 而STORE操作推迟一下往往并不会有问题;
(*) LOAD操作能够通过预取来完毕, 而且在确认数据已经不须要之后, 预取结果能够丢弃;
(*) LOAD操作能够通过预取来完毕, 导致结果被获取的时机可能并不符合期望的运行顺序;
(*) 内存訪问的顺序可能被又一次排列, 以促进更好的使用CPU总线和cache;
(*) 有一些内存或IO设备支持对相邻地址的批量訪问, 在跟它们打交道的时候, LOAD和
STORE操作可能被合并, 从而削减訪存事务建立的成本, 以提高性能(内存和PCI设备可
能都能够这样做); 而且
(*) CPU的数据cache可能影响訪问顺序, 虽然cache一致性机构能够缓解这个问题 - 一旦
STORE操作命中了cache - 但并不能保证一致性将按顺序传播到其它CPU(译注: 假设
STORE操作命中了cache, 那么被更新过的脏数据可能会在cache中停留一段时间, 而不
会立马冲刷到内存中);
所以说, 还有一个CPU可能将上面的代码看作是:
LOAD A, ..., LOAD {C,*D}, STORE *E, STORE *B
("LOAD {C,D}"是一个合并的LOAD)
可是, CPU将保证自身的一致性: 它将按正确的顺序看到自己的内存操作, 而不须要使用内
存屏障. 以以下的代码为例:
U = *A;
*A = V;
*A = W;
X = *A;
*A = Y;
Z = *A;
如果不存在外部的干扰, 那么能够肯定终于的结果一定是:
U == the original value of *A
U == *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的STORE操作丢失了. 类似的:
*A = Y;
Z = *A;
在没有内存屏障的情况下, 可能削减为:
*A = Y;
Z = Y;
于是在该CPU之外, 根本就看不到有LOAD操作存在.
特别值得一提的Alpha处理器
-------------------------
DEC Alpha是现有的最为松散的CPU之中的一个. 不仅如此, 很多版本号的Alpha CPU拥有分列的数据
cache, 同意他们在不同的时间更新两个语义相关的缓存. 由于内存一致性系统须要同步更
新系统的两个cache, 数据依赖屏障在这里就真正成为了必要, 以使得CPU可以按正确的顺序
来处理指针的更新和新数据的获取.
Alpha处理器定义了Linux内核的内存屏障模型. (译注: 体系结构无关的代码须要以最坏情
况为基准来考虑.)
參阅前面的"Cache一致性"章节.
========
使用演示样例
========
环型缓冲区
----------
内存屏障能够用于实现环型缓冲区, 不须要使用锁, 就能使生产者和消费者串行化. 參阅:
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
[好文要顶](javascript:void(0)😉 [关注我](javascript:void(0)😉 [收藏该文](javascript:void(0)😉 [](javascript:void(0)😉 [
](javascript:void(0)😉