您的位置:首页 > 运维架构 > Linux

Linux 内核内存屏障

2017-06-23 11:14 274 查看
linux 内核内存屏障

By: David Howells dhowells@redhat.com

Paul E. McKenney paulmck@linux.vnet.ibm.com

Will Deacon will.deacon@arm.com

Peter Zijlstra peterz@infradead.org

1 免责声明

这个文档不是一个规范,这是为了让文档更加简洁,但不是为了文档的不完整。

这个文档为使用linux提供的各种内存屏障功能提供了指南,但是如果有任何疑问请询问。

强调,这个文档不是一个linux对硬件的期望的规范。

文档的目的有两个:

(1) 指定对于任何内存屏障,我们能够期望的最低功能。

(2) 提供使用内存屏障的指南

注意:一个体系结构可以提供比最低功能更多的功能,但是如果少于该功能,这个体系结构就是有错误的。在某个体系结构下内存屏障可以是一个空的操作,因为在该体系结构的工作方式明确内存屏障是没必要的

2 目录

linux 内核内存屏障 1

1 免责声明 1

2 目录 1

3 内存访问抽象模型 2

4 操作设备 4

4.1 保证 4

5 什么是内存屏障? 6

5.1 内存屏障的种类 7

5.2 关于内存屏障, 不能保证什么? 9

5.3 数据依赖屏障 10

5.4 控制依赖 12

5.5 SMP 屏障配对使用 17

5.6 内存屏障举例 18

5.7 读内存屏障与内存预取 24

5.8 传递性 26

6 内核中的显式内存屏障 29

6.1 编译优化屏障 29

6.2 CPU内存屏障 34

6.3 MMIO写屏障 37

7 内核中隐式的内存屏障 37

7.1 锁定获取函数 37

7.2 中断函数 40

7.3 睡眠唤醒函数 40

7.4 其他函数 43

8 跨CPU的获取的屏障作用 43

8.1 ACQUIRES与内存访问 43

8.2 ACQUIRES与IO访问 44

9 什么地方需要内存屏障? 45

9.1 处理器间交互 45

9.2 原子操作 47

9.3 访问设备 49

9.4 中断 50

10 内核中I/O屏障的作用 51

11 最小限度有序的假想模型 52

12 CPU cache的影响 52

12.1 CACHE一致性 54

12.2 cache一致性与DMA 57

12.3 cache一致性与MMIO 57

13 CPU所能做到的 58

13.1 特别值得一提的Alpha处理器 60

13.2 VIRTUAL MACHINE GUESTS 60

14 使用示例 60

14.1 环型缓冲区 61

15 引用 61

3 内存访问抽象模型

考虑如下抽象系统模型:

假设每个CPU执行一个产生内存访问操作的程序。 在抽象CPU中,存储器操作顺序是非常松散的,在保证程序上下文逻辑关系的前提下,

CPU可以按照其所喜欢的任何顺序来执行内存操作。 类似的,编译器也可以将它输出的指令安排成任何它喜欢的顺序, 只要保证不

影响程序表面的执行逻辑.

在上面的图示中, 一个CPU执行内存操作所产生的影响, 一直要到该操作穿越该CPU与系统中

其他部分的界面(见图中的虚线)之后, 才能被其他部分所感知.

举例来说, 考虑如下的操作序列:

CPU 1       CPU 2
=============== ===============
{ A == 1; B == 2 }
A = 3;      x = B;
B = 4;      y = A;


这一组访问指令(见上图的中间部分)在内存系统上生效的顺序, 可以有24种不同的组合:

STORE A=3, STORE B=4, y=LOAD A->3, x=LOAD B->4

STORE A=3, STORE B=4, x=LOAD B->4, y=LOAD A->3

STORE A=3, y=LOAD A->3, STORE B=4, x=LOAD B->4

STORE A=3, y=LOAD A->3, x=LOAD B->2, STORE B=4

STORE A=3, x=LOAD B->2, STORE B=4, y=LOAD A->3

STORE A=3, x=LOAD B->2, y=LOAD A->3, STORE B=4

STORE B=4, STORE A=3, y=LOAD A->3, x=LOAD B->4

STORE B=4, …



然后这就产生四种不同组合的结果值:

x == 2, y == 1

x == 2, y == 3

x == 4, y == 1

x == 4, y == 3

此外,一个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;

在这里存在明显的数据依赖,因为在CPU 2上,LOAD到D中的值取决于从P中获取的地址。

在操作序列结束时,可能获得以下几种结果:

(Q == &A) and (D == 1)

(Q == &B) and (D == 2)

(Q == &B) and (D == 4)

注意,CPU 2将永远不会尝试将C加载到D中,因为(数据依赖)CPU将在发出* Q的加载之前将P加载到Q中。

4 操作设备

一些设备将其控制寄存器映射到一组内存地址集合上,但这些控制寄存器的被访问顺序非常

重要。 例如,想像一个带有一组内部的以太网卡

通过地址端口寄存器(A)访问的寄存器和数据

端口寄存器(D)。 要读取内部寄存器5,则可能会执行以下代码

使用:

*A = 5;

x = *D;

但这可能会执行为以下两个序列之一:

STORE *A = 5, x = LOAD *D

x = LOAD *D, STORE *A = 5

其中第二个几乎肯定会导致错误,因为它在尝试读取寄存器后设置地址。

4.1 保证

CPU可能会有一些最低限度的保证:

(*)对于一个CPU, 在它上面出现的有上下文依赖关系的内存访问将被按顺序执行。 这意味着:

Q = READ_ONCE(P); smp_read_barrier_depends(); D = READ_ONCE(*Q);

CPU将顺序执行以下内存操作:

Q = LOAD P, D = LOAD *Q

并始终按这个顺序。 在大多数CPU上,smp_read_barrier_depends()不执行任何操作,但它是DEC Alpha所必需的。


READ_ONCE()用于防止编译器的恶作剧。 请注意,您通常应该使用rcu_dereference()而不是开放式编码smp_read_barrier_depends()。

(*) 对于一个CPU,重叠的LOAD和STORE将在该CPU内按照顺序执行。 这意味着:

a = READ_ONCE(*X); WRITE_ONCE(*X, b);

CPU只会按照以下的操作顺序操作内存:

a = LOAD *X, STORE *X = b

对于:

WRITE_ONCE(*X, c); d = READ_ONCE(*X);

CPU只会按照以下的操作顺序操作内存:

STORE *X = c, d = LOAD *X

(如果LOAD和STORE的目标指向同一块内存地址, 则认为是重叠的操作)

还有一些事情必须被假定或者必须不被假定的:

(*) 在使用不被READ_ONCE()和WRITE_ONCE()保护的内存引用时,不要假设编译器将执行你想要的操作(通常使用volatile来实现保护)。

没有它们,编译器在其权利范围内进行各种“创造性”转换,这些转换将在“编译屏障”章节说明

(*)不能假定无关的加载和存储操作将按照给定的顺序发出。 这意味着:

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; (A + 4) = Y;

我们可能会得到以下任何一个执行顺序:

STORE A = X; STORE (A + 4) = Y;

STORE *(A + 4) = Y; STORE *A = X;

STORE {A, (A + 4) } = {X, Y};

本节描述的保证在一些情况下无效(不能保证操作顺序、原子性等):

(*) 保证对位域操作无效,因为编译器通常生成的代码以使用非原子的

读-修改-写 指令顺序来实现这些功能。 因此不要尝试使用位域同步并行逻辑。

(*) 即使在通过锁保护位域的情况下,给定位域中的所有字段都必须被同一个锁保护。

如果给定位域中的两个字段受到不同的锁的保护,则编译器的非原子操作

读-修改-写入 顺序可能导致一个字段的更新破坏相邻字段的值。

(*) 保证仅适用于正确对齐和正确大小的变量。

“正确大小”在这里是指与“char”,“short”,“int”和“long”大小相同的变量。“正确对齐”是指自然对齐,因此“char”没有对齐限制,“short”是两字节对齐,“int”是四字节对齐,“long”在32位和64位系统上分别是四字节或八字节对齐。

请注意,这些保证被引入了C11标准,所以请在使用C11标准之前的编译器时注意(例如gcc 4.6)。 C11标准的第3.14节包含此保证的内容,章节名称是“memory location存储单元”,如下所示:

存储单元

存储单元的定义:对于标量类型的对象,或者相邻位字段(具有非零宽度)的最大序列

注1:存储单元的特性:两个执行线程可以更新和访问独立的存储单元,不会产生相互的干扰。

注2:一个位域和相邻的非位域成员将位于分开的存储单元中。

这同样适用于两个位域,

如果一个在嵌套的结构中声明,而另一个不在,

或者两个位域被一个零长度的位域声明分隔开,

或者它们被一个非位域的成员分开。

在一个数据结构中如果两个位字段之间声明的所有成员也是位字段,则同时更新这两个位字段是不安全的,无论中间这些位字段的大小是多少。

例如一个数据结构声明如下

struct {

char a;

int b:5, c:11, :0, d:8;

struct { int ee:8; } e;

}

该数据结构包含四个独立的存储单元:成员a、位字段d和e.ee分别位于各自独立的存储单元,他们可以并发的修改而不会彼此干扰。

位域b和c一起构成第四个存储单元。位域b和c不能并发修改,但b和a可以并发修改。

5 什么是内存屏障?

正如上文所说,无关的内存操作实际是以随机顺序执行的,但这对于CPU-CPU交互或者CPU与I/O交互可能是一个问题。

需要一些手段来干预编译器和CPU,使其限制指令顺序。

内存屏障就是这样的干预手段。 他们保证处于屏障两侧的内存操作满足部分有序。

(部分有序的:内存屏障之前的操作都会先于屏障之后的操作,

但是如果几个操作出现在屏障的同一边, 则不保证它们的顺序. 这一点下文将多次提到)

这样的干预是非常重要的,因为系统中的CPU和其他设备可以使用各种各样的优化策略来提高性能,包括内存操作重新排序,

延迟和内存操作的合并执行; 预取、分支预测和各种类型的缓存。

内存屏障用于禁止或抑制这些策略,使代码正确的控制多个CPU或CPU与设备的交互。

5.1 内存屏障的种类

内存屏障有四个基本种类

(1) STORE内存屏障

写内存屏障提供这样的保证: 所有出现在屏障之前的STORE操作都将先于所有出现在屏障之后的STORE操作被系统中的其他组件所感知.

写屏障仅保证针对STORE操作的部分有序; 不要求对LOAD操作没有任何影响。

CPU可以被视为一个随着时间的推移向存储系统提交一系列STORE操作的设备。在写内存屏障之前的所有STORE

操作将出现在写入屏障之后的所有STORE操作之前。

[!]请注意,写内存屏障通常应与读内存屏障配对; 请参阅“SMP屏障配对”小节。

(2) 数据依赖屏障

8数据依赖屏障是读屏障的弱化版本。 假设有两个LOAD操作的场景,其中第二个LOAD操作依赖于第一个操作的结果

(例如:第一个LOAD获取地址,而第二个LOAD使用该地址去取数据),这时候需要数据依赖屏障

确保第一个LOAD获得的地址被用于访问之前,第二个LOAD的目标被更新。

数据依赖屏障仅对相互依赖的LOAD操作产生部分排序; 不对STORE操作、独立LOAD操作或重叠的LOAD操作产生影响。

如(1)中所述,系统中的CPU可以感知到其他CPU提交到存储器系统的STORE操作序列。

而在该CPU上触发的数据依赖屏障将保证, 对于在屏障之前发生的LOAD操作,

如果这个LOAD操作的目标被其他CPU的STORE操作所修改,那么在屏障完成的时候,

这个LOAD操作之前的所有STORE操作所产生的影响,将被数据依赖屏障之后执行的任何LOAD操作所感知.

有关排序约束的图表,请参见:“内存屏障序列示例”。

请注意,第一个LOAD实际上必须具有数据依赖关系,而不是控制依赖。

如果第二个LOAD的地址依赖于第一个LOAD,但是依赖关系是通过一个条件语句而不是实际加载地址本身,

那么它是一个控制依赖关系,最好需要一个完整的读屏障。 有关详细信息,请参阅“控制依赖关系”小节。

[!] 请注意,数据依赖障碍通常应与写入障碍配对; 请参阅“SMP障碍配对”小节。

(3) LOAD内存屏障

读屏障包含数据依赖屏障的功能, 并且保证所有出现在屏障之前的LOAD操作都将先于所有出现在屏障之后的LOAD操作被系统中的其他组件所感知.

读屏障仅保证针对LOAD操作的部分有序; 不要求对STORE操作产生影响.

读内存屏障隐含了数据依赖屏障, 因此可以用于替代数据依赖屏障.

[!] 注意, 读屏障一般要跟写屏障配对使用; 参阅”SMP内存屏障的配对使用”章节.

(4)通用内存屏障.

通用内存屏障保证所有出现在屏障之前的LOAD和STORE操作都将先于所有出现在屏障

之后的LOAD和STORE操作被系统中的其他组件所感知.

通用内存屏障是针对LOAD和STORE操作的部分有序.

通用内存屏障隐含了读屏障和写屏障, 因此可以用于替代它们.

内存屏障还有两种隐式类型:

(5) ACQUIRE操作

这是一个单向的可渗透的屏障。它保证所有出现在ACQUIRE之后的内存操作都将在

ACQUIRE操作被系统中的其他组件所感知之后才能发生.ACQUIRE包括LOCK操作、

smp_load_acquire()和smp_cond_acquire()操作。后来的版本ACQUIRE语义包括控制依赖和smp_rmb()。

出现在ACQUIRE之前的内存操作可能在ACQUIRE之后才发生

ACQUIRE操作应该总是跟RELEASE操作成对出现的。

(6) RELEASE操作

这是一个单向的可渗透的屏障。它保证所有出现在RELEASE之前的内存操作都将在

RELEASE操作被系统中的其他组件所感知之前发生.RELEASE操作包括UNLOCK操作和smp_store_release()操作。

出现在RELEASE之后的内存操作可能在RELEASE完成之前就发生了.
使用ACQUIRE和RELEASE操作通常不需要其他种类的内存屏障(但请注意“MMIO写屏障”一节中提到的例外情况)。
此外,RELEASE + ACQUIRE对不能保证能替代完整的内存屏障。
然而,在ACQUIRE后的给定的变量,ACQUIRE之前的任何RELEASE之前的该变量的所有存储器访问都保证是可见的。
换句话说,在给定变量的关键部分中,该变量的所有先前关键部分的所有访问都将保证已完成。
这意味着ACQUIRE操作是一个最小的“获取”操作(获取之前发布的内存访问状态),
RELEASE操作时一个最小的“发布”操作(发布当前内存状态)。


在atomic_ops.txt中描述的原子操作的子集除了完全有序和自由排序(无屏障语义)之外还有ACQUIRE和RELEASE变体。

对于复合的原子操作LOAD和STORE,ACQUIRE语义仅应用于LOAD,RELEASE语义仅应用于操作的STORE部分。

只有在存在多CPU交互或CPU与设备交互的情况下才可能需要用到内存屏障。

如果可以确保某段代码中不存在这样的交互,那么这段代码就不需要内存屏障。

注意:对于前边提到的都是最低限度的保证,不同的体系结构可能提供更多的保证,

但是在特定体系结构的代码之外,不能依赖于这些额外的保证。

5.2 关于内存屏障, 不能保证什么?

Linux内核的内存屏障不保证下面这些事情:

(*) 不能保证内存屏障之前出现的任何内存访问都会在内存屏障指令之前完成。

内存屏障相当于在该CPU的访问队列中画一条线, 使得相关访存类型的请求不能跨越内存屏障。

(*) 不保证在一个CPU上执行的内存屏障会对其他系统中的CPU或硬件设备产生任何直接影响。

间接影响就是第二个CPU感知到第一个CPU访问内存的顺序,不过请看下一点:

(*) 不能保证CPU能够观察到第二个CPU的访问内存的正确顺序,即使第二个CPU使用内存屏障,

除非第一个CPU也使用了与之匹配的内存屏障(参阅”SMP内存屏障的配对使用”部分)

(*) 不能保证一些CPU外硬件不会对内存访问重新排序。CPU cache一致性机制会在CPU间传播内存屏障所带来的间接影响,

但是可能不是按照原顺序的。

[*]更多关于总线主控DMA和一致性的问题请参阅:

Documentation/PCI/pci.txt

Documentation/DMA-API-HOWTO.txt

Documentation/DMA-API.txt

5.3 数据依赖屏障

数据依赖屏障的使用要求有点微妙, 并不总是很明显就能看出是否需要他们。

为了说明这点,考虑如下的操作队列:

CPU 1             CPU 2
===============       ===============
{ A == 1, B == 2, C == 3, P == &A, Q == &C }
B = 4;
<write barrier>
WRITE_ONCE(P, &B)
Q = READ_ONCE(P);
D = *Q;


这里有明显的数据依赖, 在序列执行完之后,Q的值一定是&A和&B之一,执行结果可能是:

(Q == &A) implies (D == 1)

(Q == &B) implies (D == 4)

但是! CPU 2可能在看的P被更新之后, 才看到B被更新, 这就导致下面的情况:

(Q == &B) and (D == 2) ????

虽然这看起来似乎是一致性错误或逻辑关系错误,但其实不是,这种现象可以在特定的cpu上观察到(比如DEC Alpha)。

为了解决这个问题, 必须在取地址和取数据之间插入一个数据依赖或更强的屏障:

CPU 1 CPU 2

=============== ===============

{ A == 1, B == 2, C == 3, P == &A, Q == &C }

B = 4;

WRITE_ONCE(P, &B);

Q = READ_ONCE(P);

D = *Q;

这就将执行结果强制为前两种结果之一,避免了第三种结果的产生。

数据依赖性屏障还必须对依赖写入进行顺序操作

CPU 1 CPU 2

=============== ===============

{ A == 1, B == 2, C = 3, P == &A, Q == &C }

B = 4;

WRITE_ONCE(P, &B);

Q = READ_ONCE(P);

*Q = 5;

数据依赖性屏障必须将读入Q值和写入*Q顺序操作。 这样避免了以下结果:

(Q == &B) && (B == 4)

请注意,这种模式应该很少见。毕竟,依赖排序的主要目的是防止产生对数据结构的写入,以及这些写入导致的高速缓存未命中的昂贵开销。

该模式可用于记录罕见的错误条件等,并且排序可以防止这些记录丢失。????????????????

[!] 注意, 这种非常违反直觉的情况最容易出现在独立cache的机器上,

例如一个高速缓存处理偶数编号的cache行,而另一个处理奇数编号的cache行。

P指针可能存储在奇数号的cache行中, 而B的值可能存储在偶数号的cache行中。

这样一来,如果正在进行读取操作的CPU的偶数编号cache组非常繁忙,而奇数编号的cache组空闲。

则CPU可以看到指针P是新值(&B),但看的的变量B还是旧值(2),

数据依赖屏障对于RCU系统非常重要,

举例来说请参阅include/linux/rcupdate.h文件中的rcu_assign_pointer()和rcu_dereference()函数.

这个函数使得当前RCU指针指向的对象被替换成新的对象时, 不会发生新对象尚未初始化完成的情况.

更详尽的例子请参见“Cache一致性”章节。

5.4 控制依赖

控制依赖可能有点棘手,因为目前的编译器不了解它们。本节的目的是帮助您预防编译器的无知破坏你的代码。

为了使LOAD-LOAD控制依赖正确工作,需要完整的读内存屏障,而不仅仅是一个数据依赖障碍。

考虑以下代码:

q = READ_ONCE(a);

if (q) {

/* BUG: 没有数据依赖!!! */

p = READ_ONCE(b);

}

这段代码可能达不到预期的效果因为这里其实并不是数据依赖, 而是控制依赖,CPU可能

试图通过提前预测结果而对”if (p)”进行短路,其他cpu也可以看到b的LOAD发生在a的load之前。

在这样的情况下, 需要的是:

q = READ_ONCE(a);

if (q) {

p = READ_ONCE(b);

}

然而,对于STORE操作不能这么认为。这意味着针对LOAD-STORE控制依赖关系提供了排序,如下例所示:

q = READ_ONCE(a);

if (q) {

WRITE_ONCE(b, 1);

}

控制依赖关系通常与其他类型的屏障配对。也就是说,请注意,READ_ONCE()和WRITE_ONCE()都不是可选的

没有READ_ONCE(),编译器可能将’a’的LOAD与其他LOAD操作合并。

没有WRITE_ONCE(),编译器可能将‘b’STORE与其他STORE操作合并。

这可能会对排序的特别违反直觉的影响。

更糟糕的是,如果编译器能够证明变量’a’的值总是非零值,

编译器将在它的权利范围内通过删除“if”条件判断语句对原示例进行优化,结果如下:

q = a;

b = 1; /* BUG: 编译器和CPU都能对指令进行重排!!! */

所以不要丢弃READ_ONCE()。

在“if”语句的两个分支上执行相同STORE操作进行强制排序是非常诱人的。代码如下:

q = READ_ONCE(a);

if (q) {

barrier();

WRITE_ONCE(b, 1);

do_something();

} else {

barrier();

WRITE_ONCE(b, 1);

do_something_else();

}

不幸的是,现在的编译器会在高优化等级的时候进行如下优化:

q = READ_ONCE(a);

barrier();

WRITE_ONCE(b, 1); /* BUG: No ordering vs. load from a!!! */

if (q) {

/* WRITE_ONCE(b, 1); – moved up, BUG!!! */

do_something();

} else {

/* WRITE_ONCE(b, 1); – moved up, BUG!!! */

do_something_else();

}

现在从LOAD“A”和STORE“b”之间没有条件语句,这意味着CPU有权限对他们进行重新排序:

条件语句是绝对必需的,即使在使用所有编译器优化之后,它也必须存在于汇编代码中。

因此,如果在本例中需要固定排序,则需要显式的内存障碍,例如smp_store_release()

q = READ_ONCE(a);

if (q) {

smp_store_release(&b, 1);

do_something();

} else {

smp_store_release(&b, 1);

do_something_else();

}

相比之下,如果没有明确的内存屏障,只有当条件语句的两条腿中的STORE操作不同时,

控制排序才能有效。例如:

q = READ_ONCE(a);

if (q) {

WRITE_ONCE(b, 1);

do_something();

} else {

WRITE_ONCE(b, 2);

do_something_else();

}

例子中的READ_ONCE()仍然是必要的,以防止编译器计算’a’的值

另外,你需要注意的是用局部变量’q’做了什么操作,否则编译器可能能够预测该值并再次删除所需的条件。例如:

q = READ_ONCE(a);

if (q % MAX) {

WRITE_ONCE(b, 1);

do_something();

} else {

WRITE_ONCE(b, 2);

do_something_else();

}

如果MAX被定义为1,则编译器知道(q%MAX)等于零,在这种情况下,编译器将在它的权利范围将上述代码转换为以下代码:

q = READ_ONCE(a);

WRITE_ONCE(b, 1);

do_something_else();

这页,CPU就不再需要保证LOAD’a’和STORE’b’的顺序。

添加一个barrier()是很有吸引力的,但这没有帮助。

条件语句没了,控制屏障不会再回来了。

因此,如果你需要这个执行顺序,你应该确保MAX大于1,如下:

q = READ_ONCE(a);

BUILD_BUG_ON(MAX <= 1); /* 顺序执行从a LOAD和STORE到b. */

if (q % MAX) {

WRITE_ONCE(b, 1);

do_something();

} else {

WRITE_ONCE(b, 2);

do_something_else();

}

请再次注意,STORE“b”的两个参数不同。如果他们相同的,正像前面提到的,

编译器可以将这个STORE操作移动到if语句外。

你还必须小心,不要太多依赖于布尔短路评估(或运算时只有第一个条件为假时才会计算第二个条件)

考虑如下例子:

q = READ_ONCE(a);

if (q || 1 > 0)

WRITE_ONCE(b, 1);

因为第一个条件不能错误,第二个条件总是为真,编译器可以将此示例转换为以下内容:

q = READ_ONCE(a);

WRITE_ONCE(b, 1);

此示例强调了确保编译器无法猜测您的代码的需要。????????????????????

更一般来说,虽然READ_ONCE()强制编译器执行给定的LOAD代码,但并不强制编译器使用该执行结果。

另外,控制依赖只适用于所讨论的if语句的then分支和else分支。

特别地,控制依赖不一定适用于if语句后面的代码。

q = READ_ONCE(a);

if (q) {

WRITE_ONCE(b, 1);

} else {

WRITE_ONCE(b, 2);

}

WRITE_ONCE(c, 1); /* BUG: No ordering against the read from ‘a’. */

人们很容易认为这个代码实际上是有序的,因为编译器不能对volatile修饰的操作(READ_ONCE、WRITE_ONCE操作)重新排序

也不能对条件语句中的WRITE操作排序。

不幸的是,对于这种推理,编译器可能将两个写入“b”编译为条件移动指令,就像在这个奇怪的伪汇编代码:

ld r1,a

cmp r1,0cmov,ner4,1

cmov,eq r4,2str4,bst1,c

一个弱排序的CPU认为STORE’a’和LOAD’c’之间没有任何依赖关系。

控制依赖关系只会展开成一对cmov指令和依赖这两个指令的存储操作。

简而言之,控制依赖仅适用于所讨论的if语句的then分支和else分支中的STORE操作(包括这两个分支所包含的函数调用),

但不包括if语句后面的代码。

最后,控制依赖不提供传递性。这通过两个相关示例来证明,初始值“x”和“y”都为零:

CPU 0 CPU 1

======================= =======================

r1 = READ_ONCE(x); r2 = READ_ONCE(y);

if (r1 > 0) if (r2 > 0)

WRITE_ONCE(y, 1); WRITE_ONCE(x, 1);

assert(!(r1 == 1 && r2 == 1));


上述包含两个CPU的示例将永远不会触发断言。

但是,如果控制依赖性保证了传输性(它们没有),则添加以下CPU及执行代码将保证触发断言。

CPU 2

=====================

WRITE_ONCE(x, 2);

assert(!(r1 == 2 && r2 == 1 && x == 2)); /* FAILS!!! */


但是由于控制依赖关系不能提供传输性,因此在包含三个CPU的示例完成后,上述断言可能会失败。

如果需要在三CPU的例子中来保证顺序,那么需要在CPU 0和CPU 1代码片段中的LOAD和STORE之间(if语句之前或之后)增加smp_mb()

此外,原来的两CPU实例非常脆弱,应该避免这样。

These two examples are the LB and WWC litmus tests from this paper:

http://www.cl.cam.ac.uk/users/pes20/ppc-supplemental/test6.pdf and this

site: https://www.cl.cam.ac.uk/~pes20/ppcmem/index.html.

这两个例子来自于http://www.cl.cam.ac.uk/users/pes20/ppc-supplemental/test6.pdfhttps://www.cl.cam.ac.uk/~pes20/ppcmem/index.html

网站的litmus测试程序的LB和WWC测试例程。

In summary:

综上所述:

(*)控制依赖可以对LOAD-STORE顺序操作进行排序。然而控制依赖不保证其他种类的操作按照顺序执行:

不保证LOAD-LOAD操作,也不保证先STORE与后来的任何操作的执行顺序。

如果您需要这些其他形式的顺序保证,请使用smp_rmb(),smp_wmb(),或者在先STORE和后LOAD的情况下使用smp_mb()

(*) 如果“if”语句的两条分支以同一变量的相同STORE开始,那么必须在STORE前面增加的smp_mb()或smp_store_release()来保证STORE顺序。

请注意,在“if”语句的每个分支的开始处使用barrier()是不够的,

因为上边的例子说明,优化编译器可以在遵守barrier()规定的情况下破坏控制依赖关系。

(*) 控制依赖关系要求在前边的LOAD和后续的STORE之间至少有一个执行时的条件语句,而这个条件语句必须与前面的LOAD有关联。

如果编译器能够优化条件语句,那么它也将优化代码顺序。 使用READ_ONCE()和WRITE_ONCE()可以帮助程序保留所需的条件语句。

(*)使用控制依赖性需要避免编译器重新排序导致依赖关系不存在。

小心的使用 READ_ONCE()和 atomic_read()可以保护控制依赖关系。

有关的更多信息,请参阅编译屏障章节。

(*)控制依赖仅适用于包含控件依赖关系的if语句的then分支和else分支(包括这两个分支所包含的函数调用)。

控制依赖关系不适用于包含控件依赖关系的if语句之后的代码

(*)控制依赖关系通常与其他类型的屏障配对使用。

(*)控制依赖不提供传递性。 如果需要传递性,请使用smp_mb()。

(*) 编译器不理解控制依赖。 因此,您的工作是确保编译器不会破坏您的代码。

5.5 SMP 屏障配对使用

处理CPU-CPU交互时,某些类型的内存屏障应该始终配对使用。 缺乏适当的配对使用基本上可以肯定是错误的。

尽管没有传递性,一般屏障应该配对使用,虽然他们与其他类型的屏障也能配对。

acquire屏障与release屏障配对,但是他们又都能与其他类型的屏障配对(当然包括通用屏障)。

write屏障可以与数据依赖屏障、控制依赖屏障、acquire屏障、release屏障、read屏障或者通用屏障配对。

同样的read屏障、控制依赖屏障或数据依赖屏障与write屏障、acquire屏障、release屏障或者通用屏障配对。

CPU 1 CPU 2

=============== ===============

WRITE_ONCE(a, 1);

WRITE_ONCE(b, 2); x = READ_ONCE(b);

y = READ_ONCE(a);

Or:

CPU 1             CPU 2
===============       ===============================
a = 1;
<write barrier>
WRITE_ONCE(b, &a);    x = READ_ONCE(b);
<data dependency barrier>
y = *x;


Or even:

CPU 1             CPU 2
===============       ===============================
r1 = READ_ONCE(y);
<general barrier>
WRITE_ONCE(y, 1);     if (r2 = READ_ONCE(x)) {
<implicit control dependency>
WRITE_ONCE(y, 1);
}

assert(r1 == 0 || r2 == 0);


基本上,read屏障总是必须存在,尽管它可能是“较弱”的类型。

[!]注意,在write屏障之前出现的STORE操作通常总是期望匹配读屏障或数据依赖屏障之后出现的LOAD操作,反之亦然:

CPU 1 CPU 2

=================== ===================

WRITE_ONCE(a, 1); }—- —>{ v = READ_ONCE(c);

WRITE_ONCE(b, 2); } \ / { w = READ_ONCE(d);

\

WRITE_ONCE(c, 3); } / \ { x = READ_ONCE(a);

WRITE_ONCE(d, 4); }—- —>{ y = READ_ONCE(b);

5.6 内存屏障举例

第一,write屏障用作将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}集合之前,而集合内部可能乱序。

第二,数据依赖屏障对有数据依赖关系的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 (gets &B)

LOAD *C (reads B)

没有干预的话, CPU 1的操作被CPU 2感知到的顺序是随机的, 尽管CPU 1执行了写屏障:

在上面的例子中, CPU 2看到的B的值是7, 尽管对LOAD*C(值应该是B)发生在LOAD C之

后.

但是,如果在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 (gets &B)

LOAD *C (reads B)

那么下面的情况将会发生:

第三,读取屏障用作LOAD上的部分顺序。考虑如下事件序列:

CPU 1           CPU 2
======================= =======================
{ A = 0, B = 9 }
STORE A=1
<write barrier>
STORE B=2
LOAD B
LOAD A


没有干预的话, CPU 1的操作被CPU 2感知到的顺序是随机的, 尽管CPU 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所感知:

为了更全面地说明这一点, 考虑一下如果代码在读屏障的两边都有一个LOAD A的话, 会发生

什么:

CPU 1 CPU 2

======================= =======================

{ A = 0, B = 9 }

STORE A=1

STORE B=2

LOAD B

LOAD A [first load of A]

LOAD A [second load of A]

尽管两次LOAD A都发生在LOAD B之后, 它们也可能得到不同的值:

但是也可能CPU 2在读屏障结束之前就感知到CPU 1对A的更新:

这里保证, 如果LOAD B得到的值是2的话, 第二个LOAD A总是能得到的值是1.

但是对于第一个LOAD A的值是没有保证的,可能得到的值是0或者1.

5.7 读内存屏障与内存预取

许多CPU会对LOAD操作进行预取: 那就是CPU发现它可能需要从内存中LOAD一个数据,

同时CPU寻找一个不需要使用总线进行其他LOAD操作的时机,来进行这个LOAD操作

(虽然CPU的指令执行流程还没有执行到该LOAD指令)。

这可能使得某些LOAD指令执行时会立即完成,因为CPU已经预取到了所需要LOAD的值。

可能会出现因为一个分支语句导致CPU实际上并不需要执行该LOAD语句,在这种情况下CPU可以丢弃该值或者缓存该值供以后使用。

Consider:

考虑如下场景:

CPU 1 CPU 2

======================= =======================

LOAD B

DIVIDE } 除法指令通常消耗

DIVIDE } 很长的执行时间

LOAD A

Which might appear as this:

这可能将表现为如下情况:

如果在第二个LOAD之前放一个读屏障或数据依赖屏障:

CPU 1           CPU 2
======================= =======================
LOAD B
DIVIDE
DIVIDE
<read barrier>
LOAD A


这将迫使CPU对所推测的任何值进行更新检查,这取决于所使用的障碍物的类型。

如果没有对推测的内存位置进行更改,那么只会使用推测值:

但是如果有其他CPU更新或者删除该值,则内存预取将失效,CPU重新加载该值:

5.8 传递性

传递性是对真实计算机系统并不总是提供的排序的深刻直观的概念。 以下示例演示了传递性:

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之后,

CPU 2的LOAD Y发生在CPU 3的STORE Y之前,

那么问题是“CPU 3的LOAD X操作是否能返回0?”

因为CPU 2的LOAD X看起来是发生在CPU 1的STORE X之后,因此肯定期望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 2的通用屏障替换为读屏障,如下所示:

CPU 1 CPU 2 CPU 3

======================= ======================= =======================

{ X = 0, Y = 0 }

STORE X=1 LOAD X STORE Y=1

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共享存储缓冲区或高级缓存的系统上运行,则CPU 2可能会更早访问到CPU 1的写入。

因此需要通用障碍,以确保所有CPU对CPU 1和CPU 2的访问的顺序意见一致。?????????????????

通用屏障提供“全局传递性”,所以所有的CPU都会就操作顺序达成一致。

相比之下,一组release-acquire仅提供“局部传递性”,因此只有那些链上的CPU才能保证访问的组合顺序一致。

例如,切换到依赖于Herman Hollerith的C代码:

int u, v, x, y, z;

void cpu0(void)
{
r0 = smp_load_acquire(&x);
WRITE_ONCE(u, 1);
smp_store_release(&y, 1);
}

void cpu1(void)
{
r1 = smp_load_acquire(&y);
r4 = READ_ONCE(v);
r5 = READ_ONCE(u);
smp_store_release(&z, 1);
}

void cpu2(void)
{
r2 = smp_load_acquire(&z);
smp_store_release(&x, 1);
}

void cpu3(void)
{
WRITE_ONCE(v, 1);
smp_mb();
r3 = READ_ONCE(u);
}


因为cpu0(),cpu1()和cpu2()参与smp_store_release()/ smp_load_acquire()的本地依赖链,

所以以下结果不可能发生:

r0 == 1 && r1 == 1 && r2 == 1

此外,由于cpu0()和cpu1()之间的依赖关系,

r1等于1时cpu1()肯定看到cpu0()的写入,因此以下结果不可能发生:

r1 == 1 && r5 == 0

但是,release-acquisition的传递性只涉及到参与release-acquisition操作的CPU,不适用于cpu3()。

因此,以下结果是可能发生:

r0 == 0 && r1 == 1 && r2 == 1 && r3 == 0 && r4 == 0

As an aside, the following outcome is also possible:

除此之外,以下结果也是可能的:

r0 == 0 && r1 == 1 && r2 == 1 && r3 == 0 && r4 == 0 && r5 == 1

虽然cpu0(),cpu1()和cpu2()将按顺序看到它们各自的读写操作,

但是没有涉及release-acquisition链的CPU在顺序上可能不太符合要求。

这种分歧源于以下事实:

在所有情况下,用于实现smp_load_acquire()和smp_store_release()的

弱内存屏障指令不需要在先前存储之后对后续加载进行排序。 ????????????????????????????????????不理解

这意味着cpu3()可以看到cpu0()的STORE u发生在cpu1的LOAD v后面,尽管cpu0()和cpu1()都认为这两个操作按照预定的顺序发生。

但是,请记住,smp_load_acquire()不是魔术。特别是,它只是从其参数中读取。

它不能确保任何特定的值被读取。 因此,以下结果是可能的:

r0 == 0 && r1 == 0 && r2 == 0 && r5 == 0

请注意,这一结果甚至可能发生在一个神话般的顺序一致的系统,没有任何重新排序。

To reiterate, if your code requires global transitivity, use general

barriers throughout.

要重申,如果您的代码需要全局传递性,请在整个过程中使用一般障碍。

6 内核中的显式内存屏障

The Linux kernel has a variety of different barriers that act at different

levels:

Linux内核具有各种各样的屏障,在不同层次上起作用:

(*)编译优化屏障

(*)CPU内存屏障

(*)MMIO写屏障

6.1 编译优化屏障

Linux内核有一个明确的编译器屏障功能,可以防止编译器将屏障任意一侧的内存访问移动到另一侧:

barrier();

这是通用的障碍 - 没有read-read或write-write的屏障变体。

然而,READ_ONCE()和WRITE_ONCE()可以被认为是仅影响由READ_ONCE()或WRITE_ONCE()

标记的特定访问的barrier()的弱形式。

barrier()函数具有以下效果:

(*) 阻止编译器将barrier()之后的访问重新排序到barrier()之前的任何访问之前。

这个性质的一个示例使用是简化中断处理程序代码与被中断代码之间的通信。

(*)在一个循环中,强制编译器加载该循环中使用的变量,每个遍历该循环的条件是有条件的。

有许多优化使单线程代码可以安全执行而在多线程代码中可能是致命的,

READ_ONCE()和WRITE_ONCE()函数可以防止这些优化,

以下是这些优化的一些示例:

(*)编译器在其权限内对加载和存储重新排序到同一个变量,在某些情况下,CPU在其权限内可以将加载重新排序到同一个变量。

这意味着以下代码:

a[0] = x;

a[1] = x;

可能导致存储在a[1]中的x值比存储在a[0]中的x值要旧。预先编译器和CPU都不执行如下操作:

a[0] = READ_ONCE(x);

a[1] = READ_ONCE(x);

简而言之,READ_ONCE()和WRITE_ONCE()为从多个CPU到单个变量的访问提供高速缓存一致性。

(*) 编译器有权限合并来自相同变量的连续LOAD操作。 这种合并可能导致编译器“优化”以下代码:

while (tmp = a)

do_something_with(tmp);

优化成以下代码,尽管对于单线程代码来说合法,但几乎肯定不是开发者的意图:

if (tmp = a)

for (;;)

do_something_with(tmp);

Use READ_ONCE() to prevent the compiler from doing this to you:


使用READ_ONCE()来防止编译器这样做:

while (tmp = READ_ONCE(a))

do_something_with(tmp);

(*) 编译器有权限控制重新加载变量。

例如,在高寄存器压力阻止编译器将所有感兴趣的数据保存在寄存器中的情况下。

因此,编译器可能会优化我们前面示例中的变量“tmp”:

while (tmp = a)

do_something_with(tmp);

这可能导致以下代码,这在单线程代码中是完全安全的,但可能在并发代码中是致命的:

while (a)

do_something_with(a);

例如,在“while”语句和对do_something_with()的调用之间变量a被某个其他CPU修改的情况下,

该代码的优化版本可能导致将零传递给do_something_with()。

Again, use READ_ONCE() to prevent the compiler from doing this:

再次,使用READ_ONCE()来防止编译器这样做:

while (tmp = READ_ONCE(a))

do_something_with(tmp);

请注意,如果编译器运行的寄存器不足,则可能会将tmp保存到堆栈中。

这种节省和后期恢复的开销是为什么编译器重新加载变量。

这样做对于单线程代码是完全安全的,因此您需要告诉编译器有关不安全的情况。

(*)如果编译器知道该值是什么,则编译器有权限完全省略LOAD。

例如,如果编译器可以证明变量’a’的值始终为零,则可以优化此代码:

while (tmp = a)

do_something_with(tmp);

优化成这样:

do { } while (0);


这种转换是单线程代码的胜利,因为它摆脱了一个LOAD和一个分支语句。

问题是编译器进行了假设,假设当前的CPU是唯一一个更新变量’a’。

如果变量’a’被共享,则编译器的假设将是错误的。 使用READ_ONCE()来告诉编译器它不知道它认为是多少:

while (tmp = READ_ONCE(a))

do_something_with(tmp);

但请注意,编译器还会仔细观察您使用READ_ONCE()之后的值。

例如,假设您执行以下操作,MAX是一个值为1的预处理器宏:

while ((tmp = READ_ONCE(a)) % MAX)

do_something_with(tmp);

然后,编译器知道使用“%”运算符跟着MAX结果将始终为零,

这将再次允许编译器将代码优化。 (它仍将从变量’a’加载。)

(*) 类似地,如果编译器知道变量已经具有存储的值,则在编译器有权限省略STORE操作。

同样,编译器假定当前的CPU是唯一STORE该变量的CPU,这可能导致编译器对共享变量做错了事情。

例如,假设您有以下内容:

a = 0;

… Code that does not store to variable a …

没有STORE a操作的代码

a = 0;

编译器看到变量’a’的值已经为零,所以可能会省略第二个STORE操作。

如果其他CPU可能同时STORE“a”,这将是一个致命的错误。

使用WRITE_ONCE()来防止编译器发生这种错误的猜测:

WRITE_ONCE(a, 0);

没有STORE a操作的代码

WRITE_ONCE(a, 0);

(*) 编译器有权重新排序内存访问,除非你告诉它不应该这么做。

例如,考虑过程级代码和中断处理程序之间的以下交互:

void process_level(void)

{

msg = get_message();

flag = true;

}

void interrupt_handler(void)
{
if (flag)
process_message(msg);
}


没有什么可以阻止编译器将process_level()转换为以下内容,实际上这可能是单线程代码的胜利:

void process_level(void)

{

flag = true;

msg = get_message();

}

如果这两个语句之间发生中断,那么interrupt_handler()可能会传递一个乱码的msg。 使用WRITE_ONCE()来防止如下:

void process_level(void)

{

WRITE_ONCE(msg, get_message());

WRITE_ONCE(flag, true);

}

void interrupt_handler(void)
{
if (READ_ONCE(flag))
process_message(READ_ONCE(msg));
}


请注意,如果该中断处理程序本身可以被访问“flag”和“msg”的中断处理程序中断,

例如嵌套中断或NMI,则需要在interrupt_handler()中使用READ_ONCE()和WRITE_ONCE()。

否则,除了用于文档的目的,interrupt_handler()中不需要使用READ_ONCE()和WRITE_ONCE()。

(另请注意,嵌套中断通常不会在现代Linux内核中出现,实际上,如果中断处理程序返回中断使能,

您将获得一个WARN_ONCE()splat。)

您应该假设编译器移动READ_ONCE()和WRITE_ONCE()代码不能

越过包含READ_ONCE(),WRITE_ONCE(),barrier()或类似原语的代码。

这种效果也可以使用barrier()实现,但READ_ONCE()和WRITE_ONCE()更具选择性:

使用READ_ONCE()和WRITE_ONCE(),编译器只需要忘记指定的内存位置的内容,

而barrier() 编译器必须丢弃其在任何机器寄存器中缓存的所有存储器位置的值。

当然,编译器也必须遵守READ_ONCE()和WRITE_ONCE()发生的顺序,尽管CPU不需要这样做。

(*) 编译器有权产生STORE操作,如以下示例所示:

if (a)

b = a;

else

b = 42;

编译器可以通过如下优化来节省一个分支:

b = 42;

if (a)

b = a;

在单线程代码中,这不仅安全,而且还节省了一个分支。

不幸的是,在并发代码中这种优化可能会导致,当加载变量’b’时,即使变量’a’从不为零,一些其他CPU看到b等于值42。

使用WRITE_ONCE()来防止如下:

if (a)

WRITE_ONCE(b, a);

else

WRITE_ONCE(b, 42);

编译器也可以产生LOAD操作。 这些通常不那么有害,但是它们可能会导致高速缓存行弹跳,

从而导致性能和可扩展性的降低。 使用READ_ONCE()来防止发明的负载

(*) 对于对齐的存储器位置,其尺寸允许通过单个存储器引用指令访问它们,

防止“LOAD撕裂”和“STORE撕裂”,其中单个大内存块的访问被多个较小的内存访问代替。

例如,给定一个具有7位立即字段的16位存储指令的架构,

编译器可能会试图使用两个16位存储立即指令来实现以下32位存储:

p = 0x00010002;

请注意,GCC真的使用这种优化,这并不奇怪,因为它可能需要两个以上的指令来构建常量,然后存储它。

因此,这种优化可以在单线程代码中成功。 事实上,最近的错误(自从修复)导致GCC错误地在易失性存储中使用这种优化。

在没有此类错误的情况下,使用WRITE_ONCE()可以防止在以下示例中出现STORE撕裂:

WRITE_ONCE(p, 0x00010002);

:

使用结构也可能导致LOAD和STORE撕裂,如本例所示:

struct attribute((packed)) foo {

short a;

int b;

short c;

};

struct foo foo1, foo2;



foo2.a = foo1.a;
foo2.b = foo1.b;
foo2.c = foo1.c;


因为没有使用READ_ONCE()或WRITE_ONCE()也没有使用volatile标记,

编译器在他的权限内用一对32位加载,后跟一对32位存储来实现这三个赋值语句的行为。

这将导致对“b”的加载和存储操作分为两个指令。

在此示例中,READ_ONCE()和WRITE_ONCE()再次防止撕裂:

foo2.a = foo1.a;

WRITE_ONCE(foo2.b, READ_ONCE(foo1.b));

foo2.c = foo1.c;

除此之外,对已标记为volatile的变量,不必使用READ_ONCE()和WRITE_ONCE()。

例如,因为’jiffies’被标记为volatile,所以不需要说READ_ONCE(jiffies)。

这样做的原因是READ_ONCE()和WRITE_ONCE()使用volatile cast来实现的,这样对变量没有影响。

Please note that these compiler barriers have no direct effect on the CPU,

which may then reorder things however it wishes.

请注意,这些编译器屏障对CPU没有直接的影响,然后它可能会重新排序。

6.2 CPU内存屏障

Linux内核有八个基本的CPU内存障碍:

类型 强制屏障 SMP环境生效命令

=============== ======================= ===========================

GENERAL mb() smp_mb()

WRITE wmb() smp_wmb()

READ rmb() smp_rmb()

除数据依赖屏障之外的所有内存屏障隐含了编译器屏障。 数据依赖不会强加任何额外的编译器排序。

另外:在有数据相关性的情况下,编译器会将LOAD指令按正确的顺序输出

(例如, 在
a[b]
语句中, load b必须放在load a[b]之前),

但是在C规范中不保证编译器不预测b的值(例如等于1)于是先load a再load b

(例如tmp = a [1]; if(b!= 1)tmp = a [ b];)。

编译器在load a[b]之后又重新load b, 也可能会存在问题,因为b比a[b]更新。

对于这种一致性问题,应该先使用READ_ONCE()宏。

在UP系统中, SMP内存屏障将退化成编译器优化屏障, 因为它假定CPU能够保证自身的一致性

, 并能以正确的顺序处理重叠的内存访问.

但是,请参阅下面的“虚拟机访客”一节。

[!] 请注意,SMP内存障碍必须用于控制在SMP系统上引用共享内存的顺序,尽管使用锁定是足够的。

强制屏障不应该用于控制SMP的影响,因为强制性障碍可能对SMP和UP系统造成不必要的开销。

然而,使用MMIO来访问松散属性的IO内存窗口时, 强制屏障可以用来控制这些访存的影响。

强制屏障即使在非SMP系统上也可能需要,因为它们通过禁止编译器和CPU重新排序来影响设备

感知到的对存储器操作的顺序。

还有一些更高级的屏障函数

(*) smp_store_mb(var, value)

这将value赋值给var变量,然后插入一个完整的内存屏障。 在UP编译中不能保证会插入编译优化屏障以外其他东西。

(*) smp_mb__before_atomic();

(*) smp_mb__after_atomic();

这些用于原子操作(如加,减,递增和递减)不返回值的函数,特别是用于引用计数时。

这些函数并不意味着内存障碍。

也用于不返回值的原子位操作函数(如set_bit和clear_bit)。

例如,考虑将一个对象标记为死亡的代码,然后减少对象的引用计数:

obj->dead = 1;

smp_mb__before_atomic();

atomic_dec(&obj->ref_count);

这确保在感知到引用计数器递减之前感知到对象上的死亡标记被设置。

有关详细信息,请参阅Documentation / atomic_ops.txt。 有关在哪里使用这些信息,请参阅“原子操作”小节。

(*) lockless_dereference();

这可以被认为是围绕smp_read_barrier_depends()数据依赖性屏障的指针获取包装器。

这也类似于rcu_dereference(),但是在对象生命周期由除RCU之外的其他机制处理的情况下,

例如,仅当系统关闭时才删除对象。

另外,在一些数据结构中使用了lockless_dereference(),可以使用和不使用RCU。

(*) dma_wmb();

(*) dma_rmb();

这些用于一致的内存,以确保CPU和具有DMA能力的设备可访问的共享内存的写入或读取顺序。

例如,考虑与设备共享内存的设备驱动程序,并使用描述符状态值来指示描述符是否属于设备或CPU,

以及当新的描述符可用时的门铃机制来通知它:

if (desc->status != DEVICE_OWN) {

/* do not read data until we own descriptor */

dma_rmb();

/* read/modify data */
read_data = desc->data;
desc->data = write_data;

/* flush modifications before status update */
dma_wmb();

/* assign ownership */
desc->status = DEVICE_OWN;

/* force memory to sync before notifying device via MMIO */
wmb();

/* notify device of new descriptors */
writel(DESC_NOTIFY, doorbell);
}


在我们从描述符读取数据之前,dma_rmb()允许我们保证设备已经释放了所有权,

并且dma_wmb()允许我们保证将数据写入描述符,然后设备可以看到它现在拥有所有权。

在尝试写入高速缓存不相干的MMIO区域之前,需要wmb()来保证高速缓存一致存储器写入已经完成。

有关一致内存的更多信息,请参阅文档/ DMA-API.txt。

6.3 MMIO写屏障

对于内存映射IO的写操作, Linux内核还有一个特别的屏障:

mmiowb();

这是强制写入障碍的一个变化,导致对弱有序I / O区域的写入被部分排序。

其影响可能超出CPU->硬件接口,实际上会在某种程度上影响硬件。

有关详细信息,请参阅“获取与IO访问”章节。

7 内核中隐式的内存屏障

linux内核中的其他一些函数意味着内存屏障,其中包括锁函数和调度函数。

本规范是最低保证; 任何特定的架构可以提供更实质的保证,但是在特定体

系结构的代码之外, 不能依赖于这些额外保证.

7.1 锁定获取函数

Linux内核有很多锁结构:

(*)自旋锁

(*)读写自旋锁

(*)互斥体

(*)信号量

(*)读写信号量

在所有情况下, 它们都是”ACQUIRE”操作和”RELEASE”操作的变种. 这些操作都隐含一定的屏障:

(1) ACQUIRE操作所隐含的操作:

ACQUIRE之后发出的内存操作将在ACQUIRE操作完成后完成。

ACQUIRE之前发出的内存操作,可能会在ACQUIRE操作完成后完成。

将smp_mb__before_spinlock()与以下ACQUIRE操作相结合,可以根据对先STORE与随后的LOAD和STORE进行排序。

请注意,这比smp_mb()更弱! 许多架构上的smp_mb__before_spinlock()原语都是不受约束的。

(2) RELEASE操作所隐含的

在RELEASE操作之前出现的内存操作, 一定在RELEASE操作完成之前完成.

而在RELEASE操作之后出现的内存操作, 可能在RELEASE操作完成之前就完成了.

(3) ACQUIRE操作+ACQUIRE操作所隐含的:

在某个ACQUIRE操作之前出现的所有ACQUIRE操作都将在后面这个ACQUIRE之前完成.

(4) ACQUIRE操作+RELEASE操作所隐含的:

在RELEASE操作之前出现的所有ACQUIRE操作都将在这个RELEASE之前完成.

(5) ACQUIRE失败所隐含的

某些变种的ACQUIRE操作可能会失败,比如可能由于无法立即获得锁,或者由于在睡眠等待锁可用时收到未阻塞的信号。

失败的锁操作不隐含任何屏障.

[!] 注意:锁ACQUIRE和RELEASE只是单向屏障,其结果是, 临界区之外的指令可能会在临界区中产生影响.

一个ACQUIRE后跟RELEASE可能不会被认为是完全的内存障碍,因为在ACQUIRE之前的访问可能发生在

ACQUIRE之后,以及RELEASE之后的访问可能发生在RELEASE之前,并且这两次访问可以互相交叉

*A = a;

ACQUIRE M

RELEASE M

*B = b;

可能表现为:

ACQUIRE M, STORE *B, STORE *A, RELEASE M

当ACQUIRE和RELEASE分别是锁定获取和释放时,如果锁的ACQUIRE和RELEASE是相同的锁定变量,

但是仅从不具有该锁的另一个CPU的角度来看,则可能会发生相同的重新排序。

简而言之,接下来是RELEASE的ACQUIRE可能不被认为是一个完整的内存障碍。

类似地,RELEASE后跟ACQUIRE的情况并不意味着完整的内存屏障。

因此,CPU对与RELEASE和ACQUIRE相对应的关键部分的执行可能会交叉,以便:

*A = a;

RELEASE M

ACQUIRE N

*B = b;

可能表现为:

ACQUIRE N, STORE *B, STORE *A, RELEASE M

可能看来,这种重新排序可能会导致死锁。

但是,这不可能发生,因为如果有这样的一个死锁威胁,RELEASE就会完成,从而避免死锁。

为什么这个工作有用?

一个关键点是,我们只是在谈论CPU进行重新排序,而不是编译器。 如果编译器(或者开发者)切换操作,可能会发生死锁。

但是假设CPU重新排序操作。 在这种情况下,解锁先于汇编代码中的锁定。

CPU只是选择尝试先执行以后的锁定操作。

如果有一个死锁,这个锁定操作将简单地自旋(或者尝试睡觉,但稍后会更多)。

CPU将最终执行解锁操作(在汇编代码中的锁定操作之前),这将解开潜在的死锁,从而允许锁定操作成功。

但是如果锁是睡眠锁怎么办? 在这种情况下,代码将尝试进入调度程序,最终将遇到内存屏障,这将迫使较早的解锁操作完成,

再次解开死锁。 可能会有一个睡眠解锁的一类情况,但是锁定原语需要在任何情况下正确地解决这样的一类情况。

锁和信号量可能不会对UP编译系统提供任何排序的保证,

因此在这种情况下,根本不能指望实际执行任何操作,特别是在I / O访问方面,

除非与中断禁用操作相结合。

另请参阅“Intel-CPU获取屏障的影响”部分。

例如,考虑以下几点:

*A = a;

*B = b;

ACQUIRE

*C = c;

*D = d;

RELEASE

*E = e;

*F = f;

以下事件顺序是可以接受的:

ACQUIRE, {*F,*A}, *E, {*C,*D}, *B, RELEASE

[+] 注意, {*F,*A} 代表一次合并访问.

但是下面的执行顺序都不可接受:

{*F,*A}, *B, ACQUIRE, *C, *D, RELEASE, *E

*A, *B, *C, ACQUIRE, *D, RELEASE, *E, *F

*A, *B, ACQUIRE, *C, RELEASE, *D, *E, *F

*B, ACQUIRE, *C, *D, RELEASE, {*F,*A}, *E

7.2 中断函数

禁止中断(类似于ACQUIRE)和启用中断(类似于RELEASE)的函数只会起到编译优化屏障的作用. 所

以, 如果在这种情况下需要使用内存或IO屏障, 必须采取其他手段.

7.3 睡眠唤醒函数

在全局数据中标记的事件上睡眠和唤醒可以被视为两块数据之间的交互:

等待事件的任务的任务状态和用于指示事件的全局数据。

为了确保这些似乎以正确的顺序发生,开始进入睡眠过程的原语和启动唤醒的原语隐含着某些屏障。

首先,睡眠者通常遵循这样的事件序列:

for (;;) {

set_current_state(TASK_UNINTERRUPTIBLE);

if (event_indicated)

break;

schedule();

}

在更改任务状态后,set_current_state()会自动插入通用内存屏障:

CPU 1

===============================

set_current_state();

smp_store_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);

or:

event_indicated = 1;
wake_up_process(event_daemon);


类似wake_up()的函数会隐含一个写内存屏障。 当且仅当它们的确唤醒了某个进程时。

屏障出现在进程的睡眠状态被清除之前, 也就是在设置唤醒事件标记的STORE操作和将进程状态

修改为TASK_RUNNING的STORE操作之间:

CPU 1 CPU 2

=============================== ===============================

set_current_state(); STORE event_indicated

smp_store_mb(); wake_up();

STORE current->state

STORE current->state

LOAD event_indicated

要重复的是,当且仅当事物被实际唤醒时,这种写入记忆障碍才是存在的。

要看到这一点,请考虑以下事件序列,其中X和Y都初始为零:

CPU 1 CPU 2

=============================== ===============================

X = 1; STORE event_indicated

smp_mb(); wake_up();

Y = 1; wait_event(wq, Y == 1);

wake_up(); load from Y sees 1, no memory barrier

load from X might see 0

相反,如果确实发生了唤醒,则CPU 2从X的LOAD将被保证看到1。

可用的唤醒函数包括:

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进行排序。

例如,如果睡眠者:

set_current_state(TASK_INTERRUPTIBLE);

if (event_indicated)

break;

__set_current_state(TASK_RUNNING);

do_something(my_data);

and the waker does:

而唤醒函数这样做:

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);

7.4 其他函数

其他隐含了屏障的函数:

(*)schedule()和类似函数隐含了完整的内存屏障.

8 跨CPU的获取的屏障作用

在SMP系统中, 锁定原语给出了多种形式的屏障: 其中一种在一些特定的锁冲突的情况下,

会影响其他CPU上的内存访问顺序.

8.1 ACQUIRES与内存访问

考虑以下几点:系统有一对自旋锁(M)和(Q),三个CPU; 那么应该发生以下事件序列:

CPU 1 CPU 2

=============================== ===============================

WRITE_ONCE(*A, a); WRITE_ONCE(*E, e);

ACQUIRE M ACQUIRE Q

WRITE_ONCE(*B, b); WRITE_ONCE(*F, f);

WRITE_ONCE(*C, c); WRITE_ONCE(*G, g);

RELEASE M RELEASE Q

WRITE_ONCE(*D, d); WRITE_ONCE(*H, h);

那么对于CPU 3来说, 从*A到*H的访问顺序是没有保证的, 不像单独的锁对应单独的CPU有

那样的限制. 例如, CPU 3可能看到的顺序是:

*E, ACQUIRE M, ACQUIRE Q, *G, *C, *F, *A, *B, RELEASE Q, *D, *H, RELEASE M


但是它不会看到如下情况:

*B, *C or *D preceding ACQUIRE M

*A, *B or *C following RELEASE M

*F, *G or *H preceding ACQUIRE Q

*E, *F or *G following RELEASE Q

8.2 ACQUIRES与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”.

9 什么地方需要内存屏障?

在正常操作下, 内存操作的乱序一般并不会成为问题, 即使是在SMP内核中, 一段单线程的

线性代码也总是能够正确工作. 但是, 有四种情况, 乱序绝对可能是一个问题:

(*)处理器间交互.

(*)原子操作.

(*)访问设备.

(*) Interrupts.

中断.

INTERPROCESSOR INTERACTION

9.1 处理器间交互

当有一个具有多个处理器的系统时,系统中的多个CPU可能同时在同一个数据集上工作。

这可能会导致同步问题,而通常的处理方式是使用锁。

然而,锁是相当昂贵的,因此如果可能的话,可以可能的话最好使用操作而不使用锁。

在这种情况下,可能需要仔细安排那些影响两个CPU的操作,以防止故障。

例如,考虑R / W信号量慢速路径。 这里一个等待的进程在信号量上排队,

由于信号量的特点,进程的堆栈链接到信号量的等待进程列表:

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

而如果其中一些步骤发生了乱序, 那么整个过程可能会产生错误.

一旦waiter进程将自己挂入等待队列, 并释放了信号量里的锁, 这个等待进程就不会再获得这

个锁了; 它要做的事情就是在继续工作之前, 等待waiter结构中的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

Resume processing

down_xxx() returns

call foo()

foo() clobbers *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的数据依赖逻辑将

处理所有事情.

ATOMIC OPERATIONS

9.2 原子操作

虽然原子操作在技术上实现了处理器之间的交互, 然而特别注意一些原子操作隐含了完整的

内存屏障, 而另外一些则没有, 但是它们却作为一个被整个内核严重依赖群体.

许多原子操作修改内存中的一些状态, 并且返回该状态相关的信息(旧状态或新状态), 就在

其中实际操作内存的两边各隐含一个SMP环境下的通用内存屏障(smp_mb())(除显式的锁操作

之外, 稍后说明). 它们包括:

xchg();

atomic_xchg(); atomic_long_xchg();

atomic_inc_return(); atomic_long_inc_return();

atomic_dec_return(); atomic_long_dec_return();

atomic_add_return(); atomic_long_add_return();

atomic_sub_return(); atomic_long_sub_return();

atomic_inc_and_test(); atomic_long_inc_and_test();

atomic_dec_and_test(); atomic_long_dec_and_test();

atomic_sub_and_test(); atomic_long_sub_and_test();

atomic_add_negative(); atomic_long_add_negative();

test_and_set_bit();

test_and_clear_bit();

test_and_change_bit();

/* when succeeds */
cmpxchg();
atomic_cmpxchg();       atomic_long_cmpxchg();
atomic_add_unless();        atomic_long_add_unless();


它们被用于作为ACQUIRE类和RELEASE类操作的实现, 和用于控制对象删除的引用计数, 这些情况

下, 隐含内存屏障是有必要的.

以下操作由于没有隐含内存屏障, 会有潜在的问题, 但有可能被用于实现RELEASE类这样的操



atomic_set();

set_bit();

clear_bit();

change_bit();

如果需要, 对应于这些函数, 可以使用相应的显式内存屏障(比如

smp_mb__before_atomic()).

下面这些函数也不隐含内存屏障, 并且在一些情况下, 可能也需要用到显式内存屏障(比如

smp_mb__before_atomic()):

atomic_add();

atomic_sub();

atomic_inc();

atomic_dec();

如果它们用于产生一般统计, 那么他们可能就不需要内存屏障, 除非统计数据之间存在耦合.

如果它们被用作控制对象生命周期的引用计数,则它们可能不需要内存屏障,

因为要么引用计数需要在一个锁的临界区里面进行调整, 要么调用者已经持有足够的引用而相当于拥

有了锁(引用计数足够多,不可能在这种情况下析构), 因此不需要内存屏障。

如果它们用于构成锁的一些描述信息, 那么他们可能就需要内存屏障, 因为锁原语一般需要

按一定的顺序来操作.

基本上, 每个场景都需要仔细考虑是否需要使用内存屏障.

以下操作是特殊的锁原语:

test_and_set_bit_lock();

clear_bit_unlock();

__clear_bit_unlock();

它们都执行了ACQUIRE类和RELEASE类的操作. 相比其他操作, 它们应该优先被用于实现锁原语,

因为它们的实现可以在许多体系结构下得到优化.

[!] 注意, 这些特殊的内存屏障原语对一些情况也是有用的, 因为在一些体系结构的CPU上,

使用的原子操作本身就隐含了完整的内存屏障功能, 所以屏障指令在这里是多余的, 在

这样的情况下, 这些特殊的屏障原语将不使用额外的屏障操作.

更多信息请参阅Documentation/atomic_ops.txt.

9.3 访问设备

许多设备都可以被映射到内存, 因此在CPU看来, 它们只是一组内存地址. 为了控制这些设

备, 驱动程序通常需要确保正确的内存访问按正确的顺序来执行.

但是, 聪明的CPU或者聪明的编译器却导致了潜在的问题, 如果CPU或编译器认为乱序, 或合

并访问更有利于效率的话, 驱动程序代码中仔细安排的访存序列可能并不会按正确的顺序被

送到设备上 - 从而可能导致设备的错误.

在Linux内核里面, IO访问应该使用适当的访问函数 - 例如inb()或writel() - 它们知道如

何得到恰当的访问顺序. 大多数情况下, 在使用这些函数之后就不必再显式的使用内存屏障

, 但是在两种情况下, 内存屏障可能还是需要的:

(1) 在一些系统中, IO存储操作对于所有CPU来说并不是严格有序的, 所以对于所有的通用驱动程序(译注: 通用驱动程序需要适应各种体系结构的系统), 需要使用锁, 并且一定要在解锁临界区之前执行mmiowb()函数.

(2) 如果访存函数访问松散属性的IO内存窗口, 那么需要使用强制内存屏障来确保执行顺序.

更多信息请参阅Documentation/DocBook/deviceiobook.tmpl.

9.4 中断

驱动程序可能被它自己的中断处理程序所打断, 然后驱动程序中的这两个部分可能会相互干

扰对方控制或访问设备的意图.

通过禁用本地中断(一种形式的锁)可能至少部分缓解这种情况, 这样的话, 驱动程序中的关

键操作都将包含在禁用中断的区间中. 于是当驱动程序的中断处理程序正在执行时, 驱动程

序的核心代码不可能在相同的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这样的操作.)

10 内核中I/O屏障的作用

在对IO内存进行存取的时候, 驱动程序应该使用适当的存取函数:

(*) inX(), outX():

它们都是倾向于跟IO空间打交道, 而不是普通内存空间, 不过这主要取决于具体CPU的

逻辑. i386和x86_64处理器确实有特殊的IO空间存取周期和指令, 但是许多系统结构

的CPU却并没有这些概念.

包括PCI总线也可能会定义成IO空间 - 比如在i386和x86_64的CPU上 - 很容易将它映

射到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(), writeX_relaxed()

这些函数类似于readX()和writeX(), 但是他们提供更弱的内存有序保证.特别的,

他们不保证一般的内存访问顺序,也不保证LOCK和UNLOCK操作的顺序。

如果后者需要保证则应该使用mmiowb()内存屏障。注意松散的外部设备的访问顺序与其他设备看到的顺序相同。

(*) ioreadX(), iowriteX()

这些函数在进行访存的时候会根据访存类型选择适当的操作, inX()/outX()或

readX()/writeX().

11 最小限度有序的假想模型

从概念上说, 必须假定的CPU是弱有序的, 但是它会保持程序上下文逻辑关系的外观. 一些

CPU(比如i386或x86_64)比另一些(比如powerpc或frv)更具有约束力, 而在体系结构无关的

代码中, 必须假定为最松散的情况(也就是DEC Alpha).

也就是说, 必须考虑到CPU可能会按它喜欢的顺序来执行操作 - 甚至并行执行 - 只是当指

令流中的一条指令依赖于之前的一条指令时, 之前的这条指定才必须在后面这条指令可能被

处理之前完全结束; 换句话说: 保持程序的上下文逻辑关系.

[*] 一些指令会产生不止一处影响 - 比如会修改条件码, 修改寄存器或修改内存 - 不同

的指令可能依赖于不同的影响

CPU也可能丢弃那些最终不产生任何影响的操作序列. 比如, 如果两个相邻的指令都将一个

立即数LOAD到寄存器, 那么第一个LOAD指令可能被丢弃.

类似的, 也需要假设编译器可能按它觉得舒服的顺序来调整指令流, 但同样也会保持程序的

上下文逻辑关系

12 CPU cache的影响

操作cache中缓存的内存之后, 相应的影响会在整个系统间得到传播. 位于CPU和内存之间的

cache, 和保持系统状态一致的内存一致性机构, 在一定程度上影响了传播的方法.

自从通过使用cache来实现CPU与系统中其他部分的交互以来, 内存系统就包含了CPU的缓存,

而内存屏障基本上就工作在CPU和其cache之间的界面上(逻辑上说, 内存屏障工作在下图中

虚线所示的地方):

一些LOAD和STORE可能不会实际出现在发起操作的CPU之外, 因为在CPU自己的cache上就能满

足需要, 尽管如此, 如果其他CPU关心这些数据, 那么完整的内存访问还是会发生, 因为

在保持程序所期望的上下文逻辑的前提下, CPU核心可能会按它认为合适的顺序来执行指令.

一些指令会产生LOAD和STORE操作, 并且将它们放到内存请求队列中, 等待被执行. CPU核心

可能会按它喜欢的顺序来将这些操作放进队列, 然后继续运行, 直到它必须等待这些访存指

令完成的时候为止.

内存屏障所需要关心的是访存操作从CPU一侧穿越到内存一侧的顺序, 和系统中的其他部件

感知到的操作发生的顺序.

[!] 对于一个CPU自己的LOAD和STORE来说, 并不需要使用内存屏障, 因为CPU总是能按程序

执行顺序看到它们所执行的LOAD和STORE操作.

[!] MMIO或其他设备存取可能绕开cache系统. 这取决于访问设备所经过的内存窗口的属性

和/或是否使用了CPU所特有的与设备进行交互的指令.

12.1 CACHE一致性

但是, 事情并不是像上面所说的那样简单: 因为虽然可以期望cache是一致的, 但是一致性

传播的顺序却是没有保证的. 也就是说, 虽然一个CPU所做出的更新将最终被其它CPU都看到

, 但是却不保证其他CPU所看到的都是相同的顺序.

考虑这样一个系统, 它具有双CPU(1和2), 每个CPU有一对并行的数据cache(CPU 1对应A/B,

CPU 2对应C/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       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 cache的更新. 但是设想一下

第二个CPU要去读取这些值的情形:

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


上面这一对读操作可能不会在预期的顺序下执行, 比如持有p的cache行可能被更新到另一个

CPU的cache, 而持有v的cache行因为其他一些cache事件的影响而延迟了对那个CPU的cache

的更新:

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的两个cache行都将得到更新, 但是在没有干预的情况下, 并不能保

证更新的顺序跟CPU 1提交的顺序一致.

我们需要在两次LOAD之间插入一个数据依赖屏障或读屏障, 以作为干预. 这将强制cache在

处理后续的请求之前, 先让它的一致性队列得到提交:

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;

15 引用

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 内核 文档