linux 6.0 内核内存屏障文档翻译

linux 内核内存屏障

By: David Howells dhowells@redhat.com
Paul E. McKenney paulmck@linux.ibm.com
Will Deacon will.deacon@arm.com
Peter Zijlstra peterz@infradead.org

翻译:反光 zdhzdh102417@163.com

免责声明

这个文档不是一个规范,这是为了让文档更加简洁,但不是为了文档的不完整。
这个文档为使用linux提供的各种内存屏障功能提供了指南,但是如果有任何疑问请询问。
参考tools/memory-model/中的形式化内存一致性模型和相关文档,可以解决一些疑问。尽管如此,即使是这种内存模型也应该被视为其维护者的集体意见,而不是绝对正确的预言。

强调,这个文档不是一个linux对硬件的期望的规范。
文档的目的有两个:
(1) 指定对于任何内存屏障,我们能够期望的最低功能。
(2) 提供使用内存屏障的指南
注意:一个体系结构可以提供比最低功能更多的功能,但是如果少于该功能,这个体系结构就是有错误的。
在某个体系结构下内存屏障可以是一个空的操作,因为在该体系结构的工作方式明确内存屏障是没必要的

目录

  1. 抽象内存访问模型。
  • 操作设备
  • CPU基本保证
  1. 什么是内存屏障?
  • 内存屏障的种类
  • 关于内存屏障, 不能保证什么?
  • 数据依赖屏障(历史上的)
  • 控制依赖
  • SMP屏障配对使用
  • 内存屏障举例
  • 读内存屏障与内存预取
  • 多副本原子性
  1. 内核中的显式内存屏障
  • 编译优化屏障
  • CPU内存屏障
  1. 内核中隐式的内存屏障
  • 获取锁的功能
  • 中断禁用功能
  • 睡眠和唤醒功能
  • 其他功能
  1. 跨CPU的ACQUIRING的屏障的效果
  • ACQUIRES与内存访问
  1. 什么地方需要内存屏障?
  • 处理器间交互
  • 原子操作
  • 访问设备
  • 中断
  1. 内核中I/O屏障的影响。
  2. 最小限度有序的假想模型
  3. CPU cache的影响
  • cache一致性与DMA
  • cache一致性与MMIO
  1. CPU能做到的
  • 特别值得一提的Alpha处理器
  • 虚拟机中的客户机
  1. 示例使用
  • 循环缓冲区
  1. 参考

1. 抽象内存访问模型

考虑如下抽象系统模型:

		            :                :
		            :                :
		            :                :
		+-------+   :   +--------+   :   +-------+
		|       |   :   |        |   :   |       |
		|       |   :   |        |   :   |       |
		| CPU 1 |<----->| Memory |<----->| CPU 2 |
		|       |   :   |        |   :   |       |
		|       |   :   |        |   :   |       |
		+-------+   :   +--------+   :   +-------+
		    ^       :       ^        :       ^
		    |       :       |        :       |
		    |       :       |        :       |
		    |       :       v        :       |
		    |       :   +--------+   :       |
		    |       :   |        |   :       |
		    |       :   |        |   :       |
		    +---------->| Device |<----------+
		            :   |        |   :
		            :   |        |   :
		            :   +--------+   :
		            :                :

假设每个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中。

1.1 操作设备

一些设备将其控制寄存器映射到一组内存地址集合上,但这些控制寄存器的被访问顺序非常
重要。 例如,想像一个带有一组内部的以太网卡
通过地址端口寄存器(A)访问的寄存器和数据
端口寄存器(D)。 要读取内部寄存器5,则可能会执行以下代码
使用:

	*A = 5;
	x = *D;

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

	STORE *A = 5, x = LOAD *D
	x = LOAD *D, STORE *A = 5

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

1.2 CPU基本保证

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

1.2.1 上下文依赖

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

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

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

	Q = LOAD P, D = LOAD *Q

而且总是按照这个顺序。但是,在DEC Alpha上,READ_ONCE()也会发出内存屏障指令,因此DEC Alpha CPU将发出以下内存操作:

Q = LOAD P, MEMORY_BARRIER, D = LOAD *Q, MEMORY_BARRIER

无论是否在DEC Alpha上,READ_ONCE()也可以防止编译器的破坏。

1.2.2 重叠区域LOAD STORE

在特定CPU内重叠的加载和存储看起来像是在该CPU内有序的。这意味着for:

	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的目标指向同一块内存地址, 则认为是重叠的操作)

有很多事情必须或者不能假设:

1.2.3 没有被READ_ONCE()和WRITE_ONCE()保护的内存

一定不能假设编译器会对没有被READ_ONCE()和WRITE_ONCE()保护的内存引用做你想做的事情。
如果没有它们,编译器就有权利进行各种“创造性”的转换,这些转换将在“编译屏障”章节说明

1.2.4 不能假定独立的加载和存储将按照给定的顺序发出

必须不能假定独立的加载和存储将按照给定的顺序发出。这意味着

	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

1.2.5 必须假定重叠的内存访问可能被合并或丢弃

这意味着:

	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)时要小心。包含这种保证的标准部分是第3.14节,其中对“内存位置”的定义如下::
    内存位置
    要么是标量类型的对象,要么是宽度都不为零的相邻位域的最大序列
    注1:两个执行线程可以更新和访问独立的内存位置,而不会相互干扰。
    注2:位域和相邻的非位域成员位于独立的内存位置。这同样适用于两个位字段,如果一个声明在嵌套结构声明中,而另一个不是,或者两个位字段之间用零长度的位字段声明分隔,或者用非位字段成员声明分隔。如果同一个结构中两个位字段之间声明的所有成员都是位字段,而不管中间的位字段长度是多少,那么并发地更新这两个位字段是不安全的。

2 什么是内存屏障?

如上所述,独立的内存操作实际上是随机执行的,但这对于CPU-CPU交互和I/O来说可能是一个问题。所需要的是某种干预方式,以指示编译器和CPU限制顺序。

记忆障碍就是这样的干预措施。它们对屏障两侧的内存操作施加了一种可感知的部分排序。

这样的干预是非常重要的,因为系统中的CPU和系统中的其他设备可以使用各种各样的优化策略来提高性能,包括内存操作重新排序,延迟和内存操作的合并执行; 预取、分支预测和各种类型的缓存。
内存屏障用于禁止或抑制这些策略,使代码正确的控制多个CPU或CPU与设备的交互。

2.1 内存屏障的种类

内存屏障有四种基本类型:

2.1.1 写(或存储)内存屏障。

写内存屏障保证了在屏障之前指定的所有存储操作在屏障之后指定的所有存储操作发生之前,被系统的其他组件所感知。

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

随着时间的推移,CPU可以被视为向内存系统提交一系列存储操作。写屏障之前的所有存储将发生在写屏障之后的所有存储之前。

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

2.1.2 数据依赖屏障

数据依赖屏障是读屏障的弱化版本。在执行两个加载的情况下,第二个加载依赖于第一个加载的结果(例如:第一个加载检索第二个加载将指向的地址),需要一个数据依赖屏障,以确保在第一个加载获得的地址被访问后,第二个加载的目标被更新。

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

如(1)中所述,系统中的CPU可以感知到其他CPU提交到存储器系统的STORE操作序列。
而在该CPU上触发的数据依赖屏障将保证, 对于在屏障之前发生的LOAD操作,
如果这个LOAD操作的目标被其他CPU的STORE操作所修改,那么在屏障完成的时候,
这个LOAD操作之前的所有STORE操作所产生的影响,将被数据依赖屏障之后执行的任何LOAD操作所感知.

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

  • 容易混淆的控制依赖
    [!]请注意,第一个LOAD实际上必须具有数据依赖关系,而不是控制依赖。
    如果第二个LOAD的地址依赖于第一个LOAD,但是依赖关系是通过一个条件语句而不是实际加载地址本身,
    那么它是一个控制依赖关系,最好需要一个完整的读屏障。 有关详细信息,请参阅“控制依赖关系”小节。
  • SMP屏障配对
    [!] 请注意,数据依赖屏障通常应与写屏障配对; 请参阅“SMP屏障配对”小节。

2.1.3 读取(或加载)内存屏障。

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

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

读内存屏障隐含了数据依赖屏障, 因此可以用于替代数据依赖屏障.
[!] 注意, 读屏障一般要跟写屏障配对使用; 参阅"SMP内存屏障的配对使用"章节.

2.1.4 通用内存屏障.

通用内存屏障保证所有出现在屏障之前的LOAD和STORE操作都将先于所有出现在屏障之后的LOAD和STORE操作被系统中的其他组件所感知.
通用内存屏障是针对LOAD和STORE操作的部分有序.
通用内存屏障隐含了读屏障和写屏障, 因此可以用于替代它们.

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

2.1.5 ACQUIRE操作

这是一个单向的可渗透的屏障。它保证所有出现在ACQUIRE之后的内存操作都将在ACQUIRE操作被系统中的其他组件所感知之后才能发生.ACQUIRE包括LOCK操作、
smp_load_acquire()和smp_cond_load_acquire()操作。
出现在ACQUIRE之前的内存操作可能在ACQUIRE之后才发生
ACQUIRE操作应该总是跟RELEASE操作成对出现的。

2.1.6 RELEASE操作

这是一个单向的可渗透的屏障。它保证所有出现在RELEASE之前的内存操作都将在
RELEASE操作被系统中的其他组件所感知之前发生.RELEASE操作包括UNLOCK操作和smp_store_release()操作。
出现在RELEASE之后的内存操作可能看起来是在RELEASE完成之前就发生了.
使用ACQUIRE和RELEASE操作通常不需要其他种类的内存屏障。
此外,RELEASE + ACQUIRE对不能保证能替代完整的内存屏障。
然而,在ACQUIRE后的给定的变量,ACQUIRE之前的任何RELEASE前对该变量的所有存储器访问都保证是可见的。

换句话说,在给定变量的临界区中,对该变量之前所有临界区的访问都保证已经完成。

使用获取和释放操作通常可以避免使用其他类型的内存屏障。此外,释放+获取对不能保证充当完全的内存屏障。但是,在获取给定变量之后,该变量在发布之前的所有内存访问都保证是可见的。换句话说,在给定变量的临界区中,对该变量之前所有临界区的访问都保证已经完成。

这意味着ACQUIRE操作是一个最小的“获取”操作(获取之前发布的内存访问状态),
RELEASE操作时一个最小的“释放”操作(发布当前内存状态)。

在atomic_t.txt中描述的原子操作的子集,除了完全有序和宽松(无语义障碍)定义之外,还有ACQUIRE和RELEASE变体。对于复合的原子操作LOAD和STORE,ACQUIRE语义仅应用于LOAD,RELEASE语义仅应用于操作的STORE部分。
只有在两个CPU之间或CPU和设备之间有可能交互时,才需要内存屏障。如果可以保证在任何特定的代码段中不会有任何这样的交互,那么在该代码段中就没有必要使用内存屏障。

注意:对于前边提到的都是最低限度的保证,不同的体系结构可能提供更多的保证,
但是在特定体系结构的代码之外,不能依赖于这些额外的保证。

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

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

  • 不能保证内存屏障之前出现的任何内存访问都会在内存屏障指令之前完成。内存屏障相当于在该CPU的访问队列中画一条线,使得相关访存类型的请求不能跨越内存屏障。

  • 不保证在一个CPU上执行的内存屏障会对其他系统中的CPU或硬件设备产生任何直接影响。
    间接影响就是第二个CPU感知到第一个CPU访问内存的顺序,不过请看下一点:

  • 不能保证CPU能够观察到第二个CPU的访问内存的正确顺序,即使第二个CPU使用内存屏障,除非第一个CPU也使用了与之匹配的内存屏障(参阅"SMP内存屏障的配对使用"部分)

  • 不能保证某些处于中间位置的非cpu硬件不会对内存访问进行重新排序。
    CPU cache一致性机制会在CPU间传播内存屏障所带来的间接影响,但是可能不是按照原顺序的。

  • 更多关于总线主控DMA和一致性的问题请参阅:
    Documentation/driver-api/pci/pci.rst
    Documentation/core-api/dma-api-howto.rst
    Documentation/core-api/dma-api.rst

2.3 数据依赖屏障(历史上的)

从Linux内核的v4.15开始,在DEC Alpha的READ_ONCE()中添加了一个smp_mb(),这意味着需要注意本节的人只有那些工作于DEC Alpha特定于体系结构的代码和那些工作于READ_ONCE()本身的人。对于那些需要它的人,以及那些对历史感兴趣的人,这里有一个关于数据依赖障碍的故事。

2.3.1 通常情况下有数据依赖的LOAD-LOAD是保序的

数据依赖屏障的使用要求有点微妙, 并不总是很明显就能看出是否需要他们。
为了说明这点,考虑如下的操作队列:

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)

2.3.2 DEC Alpha上的特殊情况

但是! 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 barrier>
WRITE_ONCE(P, &B);
		            Q = READ_ONCE(P);
		            <data dependency barrier>
		            D = *Q;

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

[!请注意,这种极端违反直觉的情况最容易发生在分离缓存的机器上,例如,一个缓存组处理偶数号的缓存行,而另一个缓存组处理奇数号的缓存行。指针P可能存储在奇数的缓存行中,变量B可能存储在偶数的缓存行中。然后,如果正在读取的CPU的偶数银行的缓存非常繁忙,而奇数银行是空闲的,可以看到指针P (&B)的新值,但变量B(2)的旧值。

2.3.3 有数据依赖的LOAD-STORE是保序的

对于依赖顺序的写操作,不需要数据依赖屏障,因为Linux内核支持的cpu在确定下面三项之前不会写操作
(1)写操作确实会发生,
(2)确定写操作的位置,
(3)确定要写的值
但请仔细阅读“CONTROL DEPENDENCIES”一节和文档/RCU/rcu_dereference.rst文件
编译器可以并且确实以许多极具创造性的方式打破依赖关系。

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);
		                WRITE_ONCE(*Q, 5);

因此,将读取到Q的操作与存储到*Q的操作进行排序时,不需要任何数据依赖障碍。换句话说,即使没有数据依赖障碍,这种结果也是被禁止的:

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

请注意,这种模式应该很少见。毕竟,依赖排序的主要目的是防止产生对数据结构的写入,以及这些写入导致的高速缓存未命中的昂贵开销。
该模式可用于记录罕见的错误条件等,而cpu自然发生的顺序可防止此类记录丢失。

请注意,数据依赖项提供的排序对于包含它的CPU是本地的。有关更多信息,请参阅“多副本原子性”一节。

例如,数据依赖障碍对RCU系统非常重要。请参阅include/linux/rcupdate.h中的rcu_assign_pointer()和rcu_dereference()。这允许将RCU'd指针的当前目标替换为一个新的修改后的目标,而不会使替换的目标看起来不完全初始化。

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

2.4 控制依赖

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

2.4.1 控制依赖可能被CPU短路导致重排(被误认为是数据依赖的控制依赖)

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

q = READ_ONCE(a);
if (q) {
	<data dependency barrier>  /* BUG: 没有数据依赖!!! */
	p = READ_ONCE(b);
}

这段代码可能达不到预期的效果因为这里其实并不是数据依赖, 而是控制依赖,CPU可能
试图通过提前预测结果而对"if (p)"进行短路,其他cpu也可以看到b的LOAD发生在a的load之前。
在这样的情况下, 需要的是:

q = READ_ONCE(a);
if (q) {
	<read barrier>
	p = READ_ONCE(b);
}

2.4.2 控制依赖的LOAD-STORE保证顺序

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

	q = READ_ONCE(a);
	if (q) {
		WRITE_ONCE(b, 1);
	}
例子中的READ_ONCE()WRITE_ONCE()不是可选的
控制依赖关系通常与其他类型的屏障配对。也就是说,请注意,READ_ONCE()和WRITE_ONCE()都不是可选的
没有READ_ONCE(),编译器可能将'a'的LOAD与其他LOAD操作合并。
没有WRITE_ONCE(),编译器可能将‘b’STORE与其他STORE操作合并。
这可能会对排序的特别违反直觉的影响。

更糟糕的是,如果编译器能够证明变量'a'的值总是非零值,
编译器将在它的权利范围内通过删除“if”条件判断语句对原示例进行优化,结果如下:

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

所以不要丢弃READ_ONCE()。

2.4.3 控制依赖可能被编译器优化掉》CPU对没有控制依赖的指令重排

在“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();
}

2.4.4 显式内存屏障防止问题

现在从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();
}

2.4.5 分支操作不同防止问题

相比之下,如果没有明确的内存屏障,只有当条件语句的两条腿中的STORE操作不同时,
控制排序才能有效。例如:

q = READ_ONCE(a);
if (q) {
	WRITE_ONCE(b, 1);
	do_something();
} else {
	WRITE_ONCE(b, 2);
	do_something_else();
}

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

2.4.6 编译器优化掉控制依赖

另外,你需要注意的是用局部变量'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, 2);
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代码,但它不会强制编译器使用返回的结果。

2.4.7 条件语句之后的语句没有控制依赖关系

另外,控制依赖只适用于所讨论的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,$0
	cmov,ne r4,$1
	cmov,eq r4,$2
	st r4,b
	st $1,c

一个弱排序的CPU认为STORE'a'和LOAD'c'之间没有任何依赖关系。
控制依赖关系只会展开成一对cmov指令和依赖这两个指令的存储操作。
简而言之,控制依赖仅适用于所讨论的if语句的then分支和else分支中的STORE操作(包括这两个分支所包含的函数调用),
但不包括if语句后面的代码。

请注意,控制依赖项提供的顺序对于包含它的CPU来说是本地的。更多信息请参阅“多副本原子性”一节。

2.4.8 总结:

综上所述:

  • 控制依赖可以对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{,64}_read()可以保护控制依赖关系。有关的更多信息,请参阅编译屏障章节。
  • 控制依赖仅适用于包含控制依赖关系的if语句的then分支和else分支(包括这两个分支所包含的函数调用)。控制依赖关系不适用于包含控制依赖关系的if语句之后的代码
  • 控制依赖关系通常与其他类型的屏障配对使用。
  • 控制依赖不提供多副本原子性。如果需要所有cpu同时查看一个给定的存储,可以使用smp_mb()。
  • 编译器不理解控制依赖。 因此,您的工作是确保编译器不会破坏您的代码。

2.5 SMP屏障配对使用

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

通用的屏障是成对的,尽管它们也会与大多数其他类型的屏障配对,尽管没有多拷贝的原子性。
acquire屏障与release屏障配对,但是他们又都能与其他类型的屏障配对(当然包括通用屏障)。
write屏障可以与数据依赖屏障、控制依赖屏障、acquire屏障、release屏障、read屏障或者通用屏障配对。
同样的read屏障、控制依赖屏障或数据依赖屏障与write屏障、acquire屏障、release屏障或者通用屏障配对。

CPU 1		            CPU 2
===============	        ===============
WRITE_ONCE(a, 1);
<write barrier>
WRITE_ONCE(b, 2);       x = READ_ONCE(b);
		                <read barrier>
		                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(x, 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 barrier>            \        <read barrier>
WRITE_ONCE(c, 3);    }    / \    {  x = READ_ONCE(a);
WRITE_ONCE(d, 4);    }----   --->{  y = READ_ONCE(b);

2.6 内存屏障举例

第一,write屏障用作将STORE操作部分有序。请考虑以下操作顺序:

CPU 1
=======================
STORE A = 1
STORE B = 2
STORE C = 3
<write barrier>
STORE D = 4
STORE E = 5

这个操作序列会按照顺序被提交到内存一致性系统,而系统中的其他组件可以看到
{STORE A,STORE B,STORE C}集合都发生在{STORE D,STORE E}集合之前,而集合内部可能乱序。

+-------+       :      :
|       |       +------+
|       |------>| C=3  |     }     /\
|       |  :    +------+     }-----  \  -----> Events perceptible to
|       |  :    | A=1  |     }        \/       the rest of the system
|       |  :    +------+     }
| CPU 1 |  :    | B=2  |     }
|       |       +------+     }
|       |   wwwwwwwwwwwwwwww }   <--- At this point the write barrier
|       |       +------+     }        requires all stores prior to the
|       |  :    | E=5  |     }        barrier to be committed before
|       |  :    +------+     }        further stores may take place
|       |------>| D=4  |     }
|       |       +------+
+-------+       :      :
                   |
                   | Sequence in which stores are committed to the
                   | memory system by CPU 1
                   V

第二,数据依赖屏障对有数据依赖关系的LOAD操作进行部分有序的限制。 考虑以下事件序列:

CPU 1			        CPU 2
=======================	=======================
	{ B = 7; X = 9; Y = 8; C = &Y }
STORE A = 1
STORE B = 2
<write barrier>
STORE C = &B		    LOAD X
STORE D = 4		        LOAD C (gets &B)
			            LOAD *C (reads B)

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

+-------+       :      :                :       :
|       |       +------+                +-------+  | Sequence of update
|       |------>| B=2  |-----       --->| Y->8  |  | of perception on
|       |  :    +------+     \          +-------+  | CPU 2
| CPU 1 |  :    | A=1  |      \     --->| C->&Y |  V
|       |       +------+       |        +-------+
|       |   wwwwwwwwwwwwwwww   |        :       :
|       |       +------+       |        :       :
|       |  :    | C=&B |---    |        :       :       +-------+
|       |  :    +------+   \   |        +-------+       |       |
|       |------>| D=4  |    ----------->| C->&B |------>|       |
|       |       +------+       |        +-------+       |       |
+-------+       :      :       |        :       :       |       |
                               |        :       :       |       |
                               |        :       :       | CPU 2 |
                               |        +-------+       |       |
    Apparently incorrect --->  |        | B->7  |------>|       |
    perception of B (!)        |        +-------+       |       |
                               |        :       :       |       |
                               |        +-------+       |       |
    The load of X holds --->    \       | X->9  |------>|       |
    up the maintenance           \      +-------+       |       |
    of coherence of B             ----->| B->2  |       +-------+
                                        +-------+
                                        :       :

在上面的例子中, CPU 2看到的B的值是7, 尽管对LOADC(值应该是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
<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  |
                                        +-------+
                                        :       :

但是, 如果在CPU 2的LOAD B和LOAD A之间增加一个读屏障:

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

那么CPU 1的部分有序将正确的被CPU 2所感知:

+-------+       :      :                :       :
|       |       +------+                +-------+
|       |------>| 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都发生在LOAD B之后, 它们也可能得到不同的值:

+-------+       :      :                :       :
|       |       +------+                +-------+
|       |------>| 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            +-------+       |       |
                                        :       :       +-------+

但是也可能CPU 2在读屏障结束之前就感知到CPU 1对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的话, 第二个LOAD A总是能得到的值是1.
但是对于第一个LOAD A的值是没有保证的,可能得到的值是0或者1.

2.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
这可能将表现为如下情况:
  :       +-------+
                                        +-------+       |       |
                                    --->| 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

这将迫使CPU对所推测的任何值进行更新检查,这取决于所使用的屏障的类型。
如果没有对推测的内存位置进行更改,那么只会使用推测值:

                                        :       :       +-------+
                                        +-------+       |       |
                                    --->| B->2  |------>|       |
                                        +-------+       | CPU 2 |
                                        :       :DIVIDE |       |
                                        +-------+       |       |
The CPU being busy doing a --->     --->| A->0  |~~~~   |       |
division speculates on the              +-------+   ~   |       |
LOAD of A                               :       :   ~   |       |
                                        :       :DIVIDE |       |
                                        :       :   ~   |       |
                                        :       :   ~   |       |
                                    rrrrrrrrrrrrrrrr~   |       |
                                        :       :   ~   |       |
                                        :       :   ~-->|       |
                                        :       :       |       |
                                        :       :       +-------+

但是如果有其他CPU更新或者删除该值,则内存预取将失效,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                               :       :       +-------+

2.8 多副本原子性

多副本原子性是一个非常直观的关于排序的概念,但实际的计算机系统并不总是提供这种特性。也就是说,一个给定的数据存储在同一时间对所有cpu可见,或者,所有cpu对所有数据存储可见的顺序达成一致。然而,对完全多副本原子性的支持将拒绝有价值的硬件优化,因此一种称为“其他多副本原子性”的较弱形式只保证给定的存储在同一时间对所有其他cpu可见。本文档的其余部分将讨论这种较弱的形式,但为了简洁起见,我们将其简单称为“多副本原子性”。

下面的例子演示了多副本的原子性:

CPU 1			        CPU 2			        CPU 3
=======================	=======================	=======================
	{ X = 0, Y = 0 }
STORE X=1		        r1=LOAD X (reads 1)	    LOAD Y (reads 1)
			            <general barrier>	    <read barrier>
			            STORE Y=r1		        LOAD X

假设CPU 2的LOAD X返回1,并将其STORE到Y,而CPU 3的LOAD Y返回1。这表明CPU 1的STORE X先于CPU 2的LOAD X ,CPU 2的STORE Y先于CPU 3的LOAD Y。此外,内存屏障保证CPU 2在STORE Y之前执行它的LOAD X,CPU 3在LOAD X之前从LOAD Y 。那么问题是“CPU 3的LOAD X可以返回0吗?”

因为CPU 3的LOAD X在某种意义上是在CPU 2的LOAD X之后,所以很自然地认为CPU 3的LOAD X必然返回1。这个期望来自于多副本的原子性:如果CPU B上执行的load语句跟CPU a上执行的load语句是同一个变量(而CPU a一开始并没有存储它读取的值),那么在多副本原子系统上,CPU B的load语句必须返回与CPU a的load语句相同的值,或者稍后的某个值。但Linux内核并不要求系统是多副本原子性的。

上面例子中使用的通用内存屏障弥补了多副本原子性的不足。在这个例子中,如果CPU 2的LOAD X返回1,CPU 3的LOAD Y返回1,那么CPU 3的LOAD X肯定也返回1。

然而,依赖关系、读屏障和写屏障并不总是能够补偿非多副本原子性。例如,假设上面的例子去掉了CPU 2的一般屏障,只留下如下所示的数据依赖:

CPU 1			        CPU 2			        CPU 3
=======================	=======================	=======================
	{ X = 0, Y = 0 }
STORE X=1		        r1=LOAD X (reads 1)	    LOAD Y (reads 1)
			            <data dependency>	    <read barrier>
			            STORE Y=r1		        LOAD X (reads 0)

这种替换会破坏多副本原子性:在这个例子中,CPU 2的LOAD X返回1、CPU 3的LOAD Y返回1、CPU 3的LOAD X返回0都是完全合法的。

关键在于,尽管CPU 2的数据依赖会对其LOAD和STRORE进行排序,但它并不保证CPU 1的STRORE也会排序。因此,如果这个例子运行在非多副本原子系统上,CPU 1和CPU 2共享一个存储缓冲区或某个缓存级别,那么CPU 2可能会提前访问CPU 1的STORE操作。因而需要通用屏障来确保所有cpu对多次访问的组合顺序达成一致。

通用屏障不仅可以弥补非多副本的原子性,还可以生成额外的顺序,以确保所有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()之间的release-acquire关系,cpu1()必须看到cpu0()的写操作,因此禁止出现以下结果:

r1 == 1 && r5 == 0

然而,release-acquire链提供的顺序对于参与该链的cpu来说是本地的,并且不适用于cpu3(),至少除了stores。因此,可能产生以下结果:
不理解这种情况发生的原因

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

除此之外,以下结果也是可能的:
不理解这种情况发生的原因

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

尽管cpu0()、cpu1()和cpu2()会按顺序查看它们各自的读写操作,但未参与release-acquire链的cpu可能不同意这种顺序。这种分歧源于这样一个事实:在所有情况下,用于实现smp_load_acquire()和smp_store_release()的弱内存壁垒指令都不需要对之前的STORE和之后的LOAD进行排序。这意味着cpu3()可以将cpu0()的WRITE_ONCE(u, 1);视为发生在cpu1()的 READ_ONCE(v);之后,即使cpu0()和cpu1()都认为这两个操作是按照预期的顺序进行的。

但请记住,smp_load_acquire()不是魔法。特别是,它只是从它的排序参数中读取。它不会确保读取任何特定的值。因此,可能产生以下结果:

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

请注意,这种结果甚至可能发生在神话中的顺序一致系统中,其中没有任何东西是重新排序的。

重申一下,如果你的代码需要对所有操作进行完全排序,请始终使用通用屏障。

3 内核中的显式内存屏障

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

  • 编译优化屏障
  • CPU内存屏障

3.1 编译优化屏障

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

barrier();

这是通用的屏障 - 没有read-read或write-write的屏障变体。然而,READ_ONCE()和WRITE_ONCE()可以被认为是仅影响由READ_ONCE()或WRITE_ONCE()标记的特定访问的barrier()的弱形式。

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

  • 阻止编译器将barrier()之后的内存访问重新排序到barrier()之前的任何内存访问之前。这个性质的一个示例用途是简化中断处理程序代码与被中断代码之间的通信。
  • 在循环内部,强制编译器每次执行循环时都加载循环条件语句中使用的变量。

READ_ONCE()和WRITE_ONCE()函数可以防止任何优化,尽管这些优化在单线程代码中是完全安全的,但在并发代码中可能是致命的。下面是这类优化的一些例子:

3.1.1 重新排序LOAD和STORE

编译器有权利对同一个变量重新排序LOAD和STORE,在某些情况下,CPU也有权利对同一个变量重新排序加载。这意味着下面的代码:

    a[0] = x;
    a[1] = x;

可能导致存储在[1]中的x值比存储在[0]中的x值更旧。防止编译器和CPU这样做,如下所示:

    a[0] = READ_ONCE(x);
	a[1] = READ_ONCE(x);

简而言之,READ_ONCE()和WRITE_ONCE()为多个cpu对同一个变量的访问提供了缓存一致性。

3.1.2 并来自同一个变量的连续LOAD

编译器有权合并来自同一个变量的连续LOAD。这样的合并会导致编译器“优化”以下代码:

	while (tmp = a)
		do_something_with(tmp);

下面这段代码虽然在某种意义上适合单线程代码,但几乎肯定不是开发人员想要的:

if (tmp = a)
	for (;;)
		do_something_with(tmp);

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

while (tmp = READ_ONCE(a))
	do_something_with(tmp);

3.1.3 重新LOAD变量

编译器有重新加载变量的权利,例如,在高寄存器压力导致编译器无法将所有感兴趣的数据保存在寄存器中时。因此,编译器可能会根据我们之前的例子优化变量tmp:

	while (tmp = a)
		do_something_with(tmp);

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

while (a)
	do_something_with(a);

例如,这段代码的优化版本在执行“while”语句和调用do_something_with()之间修改了变量a的情况下,可能会向do_something_with()传递一个0。

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

while (tmp = READ_ONCE(a))
	do_something_with(tmp);

注意,如果编译器运行时缺少寄存器,它可能会将tmp保存到堆栈上。这种保存和稍后恢复的开销是编译器重新加载变量的原因。这样做对于单线程代码是完全安全的,所以您需要告诉编译器在哪些情况下不安全。

3.1.4 省略LOAD

如果编译器知道装载的值是多少,它就有权利完全忽略装载。例如,如果编译器可以证明变量'a'的值总是0,它可以优化这段代码:

while (tmp = a)
	do_something_with(tmp);

优化成这样:

do { } while (0);

这种转换是单线程代码的胜利,因为它摆脱了一个LOAD和一个分支语句。
问题是编译器进行了假设,假设当前的CPU是唯一一个更新变量'a'的CPU。
如果变量'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'加载。)

3.1.5 省略STORE

类似地,如果编译器知道变量已经具有存储的值,则在编译器有权限省略STORE操作。
同样,编译器假定当前的CPU是唯一STORE该变量的CPU,这可能导致编译器对共享变量做错了事情。
例如,假设您有以下内容:

a = 0;
... Code that does not store to variable a ...
a = 0;

编译器看到变量'a'的值已经为零,所以可能会省略第二个STORE操作。
如果其他CPU可能同时STORE“a”,这将是一个致命的错误。

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

WRITE_ONCE(a, 0);
... Code that does not store to variable a ...
WRITE_ONCE(a, 0);

3.1.6 重排内存访问

编译器有权重新排序内存访问,除非你告诉它不应该这么做。
例如,考虑过程级代码和中断处理程序之间的以下交互:

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不需要这样做。

3.1.7 创建STORE操作

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

if (a)
	b = a;
else
	b = 42;

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

b = 42;
if (a)
	b = a;

在单线程代码中,这不仅是安全的,而且还节省了一个分支。不幸的是,在并发代码中,这种优化可能会导致其他CPU在加载变量'b'时看到一个伪值42——即使变量'a'从不为零。
使用WRITE_ONCE()来防止这种情况,如下所示:

if (a)
	WRITE_ONCE(b, a);
else
	WRITE_ONCE(b, 42);

编译器也可以产生LOAD操作。 这些通常不那么有害,但是它们可能会导致高速缓存行弹跳,
从而导致性能和可扩展性的降低。 使用READ_ONCE()来防止创建的LOAD

3.1.8 STORE指令的拆分

对于对齐的存储器位置,其尺寸允许通过单个存储器指令访问它们,防止“LOAD撕裂”和“STORE撕裂”,其中单个大内存块的访问被多个较小的内存访问代替。
例如,给定一个具有7位立即字段的16位存储指令的架构,
编译器可能会试图使用两个16位存储立即指令来实现以下32位存储:

p = 0x00010002;

请注意,GCC确实使用了这种优化,这并不奇怪,因为它可能需要两个以上的指令来构建常量,然后存储它。
因此,这种优化在单线程代码中是成功。
事实上,最近的一个bug(已经修复)导致GCC在一个不稳定的存储中错误地使用了这种优化。
为了防止这种情况,在下面的例子使用WRITE_ONCE()防止存储拆分:

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位LOAD,后跟一对32位STORE来实现这三个赋值语句的行为。
这将导致对“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类型转换,当它的参数已经被标记为volatile时,这种类型转换不起作用。

请注意,这些编译器屏障对CPU没有直接的影响,CPU可能会按照自己的意愿重新排序。

3.2 CPU内存屏障

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

类型		    强制屏障		            SMP环境生效命令
===============	=======================	===========================
===============	=======================	===========================
GENERAL		    mb()			        smp_mb()
WRITE		    wmb()		        	smp_wmb()
READ		    rmb()			        smp_rmb()
DATA DEPENDENCY				            READ_ONCE()

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

题外话:在对于数据依赖关系,编译器应该按照正确的顺序发出LOAD
(例如, 在a[b]语句中, load b必须放在load a[b]之前),
但是在C规范中并不能保证编译器不会推测出b的值(例如:b=1),在LOAD b之前LOAD a[b]。
(例如:tmp = a[1]; if (b != 1) tmp = a[b])
还有一个问题是,编译器在LOAD a[b]之后重新LOAD b,从而拥有了一个比a[b]更新的b副本。对于这些问题还没有达成共识,但是READ_ONCE()宏是一个很好的开始。

在单处理器编译系统上,SMP内存屏障被简化为编译器屏障,因为它假定CPU能够保证自身的一致性,并且会正确地对重叠访问进行排序。

但是,请参阅下面关于“虚拟机来宾”的小节。

[!] 请注意,在SMP系统上,必须使用SMP内存屏障来控制对共享内存的引用顺序,而使用锁就足够了。

强制屏障不应该用于控制SMP的影响,因为强制屏障会给SMP和UP系统带来不必要的开销。然而,使用MMIO来访问松散属性的IO内存窗口时, 强制屏障可以用来控制这些访存的影响。即使在非smp系统上也需要这些屏障,因为它们会禁止编译器和CPU重排内存操作,从而影响内存操作在设备上出现的顺序。

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

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

  • smp_mb__before_atomic();

  • smp_mb__after_atomic();

    这些函数用于原子RMW函数,这些函数并不意味着内存屏障,但在代码需要内存屏障的地方。不意味着内存障碍的原子RMW函数的例子包括:add、subtract、(失败的)条件操作、_relaxed函数,但不是atomic_read或atomic_set。在使用原子操作进行引用计数时,可能需要设置内存屏障。

    这些函数也用于原子的RMW bitop函数,不涉及内存屏障(如set_bit和clear_bit)。

    举个例子,假设有一段代码将一个对象标记为死亡,然后减少该对象的引用计数:

	obj->dead = 1;
	smp_mb__before_atomic();
	atomic_dec(&obj->ref_count);
这可以确保在引用计数器递减之前,对象上的死亡标记被感知到已经设置。
请参阅Documentation/atomic_{t,bitops}.txt获取更多信息。
  • dma_wmb();

  • dma_rmb();

  • dma_mb();
    这些用于一致性内存,以保证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;

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

dma_rmb()允许我们保证在从描述符读取数据之前,设备已经释放了所有权,而dma_wmb()允许我们保证在设备看到它现在拥有所有权之前,数据已经被写入描述符。dma_mb()包含dma_rmb()和dma_wmb()。注意,当使用writel()时,不需要使用先前的wmb()来确保在写入MMIO区域之前已经完成缓存一致性的内存写入。更便宜的writel_relax()不能提供这种保证,所以不能在这里使用。

有关relaxed I/O访问的更多信息请参考"Kernel I/O barrier effects"部分,
有关一致性内存的更多信息请参阅“Documentation/core-api/dma-api.rst”文件。

  • pmem_wmb();
    这是用于持久内存的,确保将修改写入持久存储的SOTORE操作达到了平台持久性域。
    例如,在对pmem区域进行非暂时写操作之后,我们使用pmem_wmb()来确保存储已经达到了平台持久性域。这确保了在后续指令发起任何数据访问或数据传输之前,STORE操作已经更新了持久存储。这是对wmb()方法排序的补充。
    对于来自持久内存的负载,现有的读内存屏障足以确保读顺序。

  • io_stop_wc();
    对于带有写合并属性(例如ioremap_wc()返回的属性)的内存访问,CPU可能会等待之前的访问与后续的访问合并。当等待影响性能时,Io_stop_wc()可用于防止将该宏前后的写合并内存访问合并起来。

4 内核中隐式的内存屏障

linux内核中的其他一些函数意味着内存屏障,其中包括锁函数和调度函数。
本规范是最低保证; 任何特定的架构可以提供更实质的保证,但是在特定体
系结构的代码之外, 不能依赖于这些额外保证.

4.1 获取锁的功能

Linux内核有很多锁结构:

  • 自旋锁
  • 读写自旋锁
  • 互斥体
  • 信号量
  • 读写信号量

在所有情况下, 它们都是"ACQUIRE"操作和"RELEASE"操作的变体. 这些操作都隐含一定的屏障:
(1) ACQUIRE操作所隐含的操作:
在ACQUIRE之后发出的内存操作将在ACQUIRE操作完成后完成。
在ACQUIRE之前发出的内存操作,可能会在ACQUIRE操作完成后完成。

(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

特别是,当获取和释放是锁的获取和释放时,如果锁的获取和释放是对同一个锁变量,但只从另一个不持有该锁的CPU的角度来看,就会发生相同的重排序。简而言之,ACQUIRE-RELEASE不能被认为是完全的内存屏障。

类似地,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最终将执行unlock操作(在汇编代码中锁操作之前),这将解开潜在的死锁,使锁操作成功。

    但如果锁是一个睡眠锁呢?在这种情况下,代码将尝试进入调度器,最终会遇到内存障碍,这将迫使之前的解锁操作完成,再次解开死锁。可能存在睡眠解锁竞争,但锁定原语在任何情况下都需要正确地解决此类竞争。

在UP编译系统上,锁和信号量不能提供任何顺序保证,因此在这种情况下不能指望实现任何实际操作(特别是I/O访问),除非与中断禁用操作结合使用。

参见“cpu间ACQUIRE屏障的影响”一节。

作为一个例子,考虑如下:

*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

4.2 中断禁用功能

禁止中断(相当于ACQUIRE)和启用中断(相当于RELEASE)的函数只会起到编译优化屏障的作用.因此,如果在这种情况下需要使用内存或I/O屏障, 必须采取其他手段.

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

or:

event_indicated = 1;
wake_up_process(event_daemon);

如果wake_up()唤醒了某些东西,那么它会执行一个通用的内存屏障。如果它没有唤醒任何东西,那么可能会执行内存屏障,也可能不会执行;你不能依赖它。屏障发生在访问进程状态之前,特别是,它位于表示事件的存储和设置TASK_RUNNING的存储之间:

CPU 1 (Sleeper)			        CPU 2 (Waker)
===============================	===============================
set_current_state();		    STORE event_indicated
  smp_store_mb();		        wake_up();
    STORE current->state	    ...
    <general barrier>		    <general barrier>
LOAD event_indicated		    if ((LOAD task->state) & TASK_NORMAL)
				                    STORE task->state

其中的“task”是被唤醒的线程,它等于CPU 1的“current”。

重复一遍,如果实际唤醒了某个对象,wake_up()保证会执行一个通用的内存屏障,否则就没有这样的保证。
为了看到这一点,考虑下面的事件序列,其中X和Y最初都是0:

CPU 1				            CPU 2
===============================	===============================
X = 1;				            Y = 1;
smp_mb();			            wake_up();
LOAD Y				            LOAD X

如果确实发生唤醒,则两个负载中的一个(至少)必须看到1。另一方面,如果没有发生唤醒,则两个负载都可能看到0。
Wake_up_process()总是执行一个通用的内存屏障。屏障再次发生在访问任务状态之前。特别是,如果前面代码片段中的wake_up()被wake_up_process()调用替换,那么两个负载中的一个就可以保证看到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();

就内存排序而言,这些函数都提供了与wake_up()相同的(或更强)保证。

[!请注意,睡眠线程和唤醒线程所隐含的内存屏障不会在唤醒之前对存储值进行排序,而在睡眠线程调用set_current_state()之后才会加载这些存储值。
例如,如果睡眠者这样做:

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

4.4 其他功能

其他隐含了屏障的函数:

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

5 跨CPU的ACQUIRING的屏障效果

在SMP系统上,锁定原语提供了一种更牢固的屏障形式:在任何特定锁的冲突上下文中,这种屏障确实会影响其他cpu上的内存访问顺序。

5.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

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

在正常操作下, 内存操作的乱序一般并不会成为问题, 即使是在SMP内核中, 一段单线程的
线性代码也总是能够正确工作. 但是, 有四种情况, 乱序绝对肯定会成为问题:

  • 处理器间交互.
  • 原子操作.
  • 访问设备.
  • 中断.

6.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
<preempt>
			                	Resume processing
				                down_xxx() returns
				                call foo()
				                foo() clobbers *waiter
</preempt>
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的数据依赖逻辑将
处理所有事情.

6.2 原子操作

虽然原子操作在技术上实现了处理器之间的交互, 然而特别注意一些原子操作隐含了完整的
内存屏障, 而另外一些则没有, 但是它们却被整个内核严重依赖.

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

6.3 访问设备

许多设备都可以被映射到内存, 因此在CPU看来, 它们只是一组内存地址. 为了控制这些设
备, 驱动程序通常需要确保正确的内存访问按正确的顺序来执行.

但是, 聪明的CPU或者聪明的编译器却导致了潜在的问题, 如果CPU或编译器认为乱序, 或合
并访问更有利于效率的话, 驱动程序代码中仔细安排的访存序列可能并不会按正确的顺序被
送到设备上 - 这将导致设备故障。

在Linux内核内部,I/O应该通过适当的访问器函数(如inb()或writel())来完成,这些例程知道如何以适当的顺序进行此类访问。大多数情况下, 在使用这些函数之后就不必再显式的使用内存屏障,但如果访问器函数用于引用具有宽松内存访问属性的I/O内存窗口,那么就需要强制的内存屏障来强制排序。

更多信息请参阅Documentation/driver-api/device-io.rst。

6.4 中断

驱动程序可能被它自己的中断处理程序所打断, 然后驱动程序中的这两个部分可能会相互干
扰对方控制或访问设备的意图.

通过禁用本地中断(一种形式的锁)可能至少部分缓解这种情况, 这样的话, 驱动程序中的关
键操作都将包含在禁用中断的区间中. 于是当驱动程序的中断处理程序正在执行时, 驱动程
序的核心代码不可能在相同的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

如果执行顺序像这样松散, 就需要假定在禁用中断区间内应该完成的访问可能泄漏到区间之
外, 并且可能漏到中断过程中进行访问 - 反之亦然 - 除非使用隐式或显式的屏障
通常这并不是一个问题, 因为禁用中断区间内完成的IO访存将会包含严格有序的同步LOAD操
作, 形成隐式的IO屏障.

在一个中断服务程序与两个运行在不同CPU的程序相互通信的情况下, 类似的情况也可能发
生. 如果出现这样的情况, 那么禁用中断的锁操作需要用于确保执行顺序. (译注: 也就是
类似于spinlock_irq这样的操作.)

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

通过I/O访问与外设的接口是深度架构和设备特定的。因此,本质上不可移植的驱动程序可能依赖于其目标系统的特定行为,以便以最轻量级的方式实现同步。对于希望在多个架构和总线实现之间可移植的驱动程序,内核提供了一系列的访问函数,这些访问函数提供了不同程度的排序保证:

  • readX(), writeX():
    readX()和writeX() MMIO访问函数接受一个指向被访问外围设备的指针作为__iomem *参数。对于使用默认I/O属性映射的指针(例如ioremap()返回的指针),顺序保证如下:
    1. 对同一外设的所有readX()和writeX()访问都是相互排序的。这确保了同一CPU线程对特定设备的MMIO寄存器访问将按程序顺序到达。
    2. 持有自旋锁的CPU线程发出的writeX()会在另一个CPU线程在获取了自旋锁后发出的writeX()之前被排序。这确保了在持有自旋锁时发出的对特定设备的MMIO寄存器写操作将按照获得锁的顺序到达。
    3. CPU线程使用writeX()写入外设的操作将等待同一线程发出或传播到同一线程的所有内存写入操作完成。这确保当CPU写入它的MMIO控制寄存器以触发传输时,CPU对dma_alloc_coherent()分配的输出DMA缓冲区的写操作对DMA引擎是可见的。
    4. CPU线程使用readX()从外设的读取的操作将在该线程开始任何后续的内存读取之前完成。在从DMA引擎的MMIO状态寄存器中读取数据确定DMA传输已经完成之后,可以确保CPU从dma_alloc_coherent()分配的DMA缓冲区中读取数据时不会看到陈旧的数据。
    5. CPU线程使用readX()从外设的读取的操作将在同一个线程上开始的后续的delay()循环之前完成。如果第一个写操作被readX()立即读回,并且在第二个writeX()之前调用udelay(1),这确保了CPU对外设的两个MMIO寄存器的写操作之间至少相隔1个us:
    	writel(42, DEVICE_REGISTER_0); // Arrives at the device...
		readl(DEVICE_REGISTER_0);
		udelay(1);
		writel(42, DEVICE_REGISTER_1); // ...at least 1us before this.

使用没有默认排序属性函数(例如ioremap_wc()返回的那些)获得的__iomem指针的排序属性是特定于底层架构的,因此上面列出的保证通常不能用于访问这些类型的映射。

  • readX_relaxed(), writeX_relaxed()
    这些函数类似于readX()和writeX(), 但是他们提供更弱的内存有序保证.具体来说,它们不保证与锁、普通内存访问或delay()循环相关的顺序。(如上文2-5所示)
    但在操作默认I/O属性映射的__iomem指针时,相对于从同一CPU线程到同一外设的其他访问,它们仍然保证是有序的。

  • readsX(), writesX():

readsX()和writesX() MMIO访问函数设计用于访问驻留在无法执行DMA的外设上的基于寄存器的、内存映射的fifo。因此,它们只保证了readX_relaxed()和writeX_relaxed()的顺序,如上文所述。

  • inX(), outX():
    inX()和outX()用于访问I/O空间外设,这可能需要在某些体系结构(特别是x86)上的特殊指令。被访问的外设的端口号作为一个参数传递。
    由于许多CPU体系结构最终都是通过内部的虚拟内存映射来访问这些外设的,因此在访问具有默认I/O属性的映射时,inX()和outX()提供的可移植排序保证与readX()和writeX()提供的相同。
    设备驱动程序可能希望outX()发出一个非提交的写事务,该事务在返回之前等待I/O外设的完成响应。这不是所有体系结构都能保证的,因此不是可移植排序语义的一部分。

  • ioreadX(), iowriteX():

    无论是inX()/outX()还是readX()/writeX(),它们都会根据实际进行的访问类型适当地执行。

除了串行访问函数(insX()、outsX()、readsX()和writesX()),上述所有方法都假定底层外设是小端序的,因此会在大端序的体系结构上执行字节交换操作。

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

从概念上说, 必须假定的CPU是弱有序的, 但它会保持程序本身的因果关系。 一些
CPU(比如i386或x86_64)比另一些(比如powerpc或frv)更具有约束力, 而在体系结构无关的
代码中, 必须假定为最松散的情况(也就是DEC Alpha).
也就是说, 必须考虑到CPU可能会按它喜欢的顺序来执行操作 - 甚至并行执行 - 只是当指
令流中的一条指令依赖于之前的一条指令时, 之前的这条指定才必须在后面这条指令可能被
处理之前完全结束; 换句话说: 保持程序的上下文逻辑关系.
[*] 一些指令会产生不止一处影响 - 比如会修改条件码, 修改寄存器或修改内存 - 不同
的指令可能依赖于不同的影响
CPU也可能丢弃那些最终不产生任何影响的操作序列. 比如, 如果两个相邻的指令都将一个
立即数LOAD到寄存器, 那么第一个LOAD指令可能被丢弃.

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

9 CPU cache的影响

在一定程度上,位于cpu和内存之间的缓存,以及维护系统状态一致性的内存一致性系统,会影响整个系统感知缓存操作的方式。
只要CPU通过高速缓存与系统其他部分交互,内存系统就必须包括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实际上可能不会出现在发出LOAD或STORE的CPU之外,因为它可能已经在CPU自己的缓存中得到了满足,但就其他CPU而言,它仍然会看起来像是完全的内存访问已经发生了,因为缓存一致性机制会将缓存行迁移到正在访问的CPU,并在冲突时传播影响。

CPU核心可以以它认为合适的任何顺序执行指令,前提是程序的因果关系似乎保持不变。一些指令生成加载和存储操作,然后进入要执行的内存访问队列。内核可以按照自己希望的任何顺序将这些指令放入队列中,并继续执行,直到被迫等待一条指令完成。
内存屏障所关注的是控制从CPU端到内存端的访问顺序,以及系统中其他观察者感知到的效果发生的顺序。

[!] 在给定的CPU中,内存屏障是不需要的,因为CPU总是认为它们自己的负载和存储是按程序顺序发生的。
[!] MMIO或其他设备访问可能会绕过缓存系统。这取决于访问设备所通过的内存窗口的属性和/或CPU可能使用的任何特殊设备通信指令。

9.1 cache一致性与DMA

并非所有系统都与执行DMA的设备保持缓存一致性。在这种情况下,尝试DMA的设备可能会从物理内存获得过时的数据,因为脏的高速缓存行可能驻留在各种cpu的高速缓存中,而且可能还没有写回物理内存。为处理这种情况,内核的适当部分必须将脏数据刷写(将缓存写入内存中)或使其失效(无效化缓存)。

此外,设备通过DMA访问内存的数据可能会被从CPU缓存写回内存的脏缓存线覆盖,或者CPU缓存中的缓存线可能会掩盖内存已经更新的事实,直到缓存线从CPU缓存中被丢弃并重新加载。为处理该问题,内核的适当部分必须使每个CPU上对应内存的缓存失效。

更多关于cache管理的信息请参阅: Documentation/core-api/cachetlb.rst

9.2 cache一致性与MMIO

内存映射I/O通常通过CPU内存空间中窗口的内存位置进行,该窗口具有与通常的RAM定向窗口不同的属性。

这些属性通常包含这样的情况: 访存会完全绕过cache, 而直接到达设备总线. 这意味着MMIO访问实际上可能超过之前对缓存内存的访问。(译注: 意思是, MMIO后执行, 但是比带cache的内存访问
先到达IO; 而先执行的写内存操作则可能被缓存在cache上, 之后才能冲刷到内存). 这种
情况下, 如果这两者有某种依赖的话, 仅使用内存屏障是不够的,而需要在写被缓存内存
和MMIO访存之间刷新缓存。

10 CPU所能做到的

程序员可能会想当然地认为CPU将完全按照指定的顺序执行内存操作.
例如,给定要执行的以下代码段,CPU会按照指定的顺序执行:

a = READ_ONCE(*A);
WRITE_ONCE(*B, b);
c = READ_ONCE(*C);
d = READ_ONCE(*D);
WRITE_ONCE(*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
	(Where "LOAD {*C,*D}" is a combined load)

但是, CPU将保证自身的一致性: 它将按正确的顺序看到自己的内存操作, 而不需要使用内
存屏障. 以下面的代码为例:

U = READ_ONCE(*A);
WRITE_ONCE(*A, V);
WRITE_ONCE(*A, W);
X = READ_ONCE(*A);
WRITE_ONCE(*A, Y);
Z = READ_ONCE(*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

但是,在没有干预的情况下,在保证程序视角的一致性的前提下,序列几乎可以有任何元素的组合组合或丢弃。请注意,在上面的例子中,READ_ONCE()和WRITE_ONCE()不是可选的,因为在某些体系结构中,给定的CPU可能会将连续的加载重新排序到相同的位置。

在这样的体系结构上,READ_ONCE()和WRITE_ONCE()会做任何必要的工作来防止这种情况发生。例如,在Itanium上,READ_ONCE()和WRITE_ONCE()使用的volatile类型转换会导致GCC发出专门的ld.acq和st.rel指令来防止这种重排序。

在CPU看到这些操作之前, 编译器也可能会合并, 丢弃或推迟序列中的一些操作.

例如:
*A = V;
*A = W;

可简化为:
*A = W;

于是, 在没有使用写屏障的情况下, 可以认为将V写入*A的STORE操作丢失了. 类似的:

*A = Y;
Z = *A;

在没有内存屏障或READ_ONCE()和WRITE_ONCE()的情况下,可以被简化为:
*A = Y;
Z = Y;

LOAD操作永远不会出现在CPU外部。

10.1 特别值得一提的Alpha处理器

DEC Alpha CPU是目前最宽松的CPU之一。不仅如此,一些版本的Alpha CPU具有拆分数据缓存,允许它们在不同的时间更新两个语义相关的缓存行。这就是数据依赖障碍真正必要的地方,因为这将使两个缓存与内存一致性系统同步,从而使指针更改与新数据出现的顺序看起来是正确的。
Alpha定义了Linux内核的内存模型,尽管从v4.15开始,Linux内核将smp_mb()添加到Alpha上的READ_ONCE(),大大降低了它对内存模型的影响。

10.2 虚拟机中的客户机

运行在虚拟机中的客户机可能会受到SMP效应的影响,即使客户机本身是在没有SMP支持的情况下编译的。这是一个在运行UP内核时与SMP主机进行交互的产物。在这个用例中使用强制屏障是可能的,但通常不是最佳选择。
为了最优地处理这种情况,可以使用底层的virt_mb()等宏。
在启用SMP时,它们与smp_mb()等具有相同的效果,但对SMP和非SMP系统生成相同的代码。
例如,虚拟机guest在与(可能是SMP)主机进行同步时应该使用virt_mb()而不是smp_mb()。
这些与smp_mb()等在其他方面是等价的,特别是它们不能控制MMIO效应:为了控制MMIO效应,需要使用强制性屏障。

11 使用示例

11.1 环型缓冲区

内存屏障可以用于实现环型缓冲区, 不需要使用锁, 就能使生产者和消费者串行化. 参阅:
Documentation/core-api/circular-buffers.rst
以获得更多细节.

==========

12 引用

==========

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

posted @ 2022-08-31 16:01  反光  阅读(367)  评论(0编辑  收藏  举报