一、内存屏障简介
现在大多数现代计算机为了提高性能而采取乱序执行,这可能会导致程序运行不符合我们预期,内存屏障就是一类同步屏障指令,是CPU或者编译器在对内存随机访问的操作中的一个同步点,只有在此点之前的所有读写操作都执行后才可以执行此点之后的操作。
内存屏障,也称内存栅栏,内存栅障,屏障指令等, 是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作。
大多数现代计算机为了提高性能而采取乱序执行,这使得内存屏障成为必须。
语义上,内存屏障之前的所有写操作都要写入内存;内存屏障之后的读操作都可以获得同步屏障之前的写操作的结果。因此,对于敏感的程序块,写操作之后、读操作之前可以插入内存屏障。
大多数处理器提供了内存屏障指令:
- 完全内存屏障(full memory barrier)保障了早于屏障的内存读写操作的结果提交到内存之后,再执行晚于屏障的读写操作。
- 内存读屏障(read memory barrier)仅确保了内存读操作;
- 内存写屏障(write memory barrier)仅保证了内存写操作。
内核代码里定义了这三种内存屏障,如x86平台:arch/x86/include/asm/barrier.h
#define mb() asm volatile("mfence":::"memory")
#define rmb() asm volatile("lfence":::"memory")
#define wmb() asm volatile("sfence" ::: "memory")
个人理解:就类似于我们喝茶的时候需要先把水煮开(限定条件),然后再切茶,而这一整套流程都是限定特定环节的先后顺序(内存屏障),保障切出来的茶可以更香。
1.1内存屏障是什么
- 硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。
- 内存屏障有两个作用:
阻止屏障两侧的指令重排序;
强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。
- 对于Load Barrier来说,在指令前插入Load Barrier,可以让高速缓存中的数据失效,强制从新从主内存加载数据;
- 对于Store Barrier来说,在指令后插入Store Barrier,能让写入缓存中的最新数据更新写入主内存,让其他线程可见。
1.2不同场景内存屏障
java内存屏障
- java的内存屏障通常所谓的四种即LoadLoad,StoreStore,LoadStore,StoreLoad实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。
- LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
- StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
- LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
- StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能
volatile语义中的内存屏障
volatile的内存屏障策略非常严格保守,非常悲观且毫无安全感的心态:
- 在每个volatile写操作前插入StoreStore屏障,在写操作后插入StoreLoad屏障;
- 在每个volatile读操作前插入LoadLoad屏障,在读操作后插入LoadStore屏障;
由于内存屏障的作用,避免了volatile变量和其它指令重排序、线程之间实现了通信,使得volatile表现出了锁的特性。
final语义中的内存屏障
对于final域,编译器和CPU会遵循两个排序规则:
新建对象过程中,构造体中对final域的初始化写入和这个对象赋值给其他引用变量,这两个操作不能重排序;
初次读包含final域的对象引用和读取这个final域,这两个操作不能重排序;(意思就是先赋值引用,再调用final值)
总之上面规则的意思可以这样理解,必需保证一个对象的所有final域被写入完毕后才能引用和读取。这也是内存屏障的起的作用:
写final域:在编译器写final域完毕,构造体结束之前,会插入一个StoreStore屏障,保证前面的对final写入对其他线程/CPU可见,并阻止重排序。
读final域:在上述规则2中,两步操作不能重排序的机理就是在读final域前插入了LoadLoad屏障。
X86处理器中,由于CPU不会对写-写操作进行重排序,所以StoreStore屏障会被省略;而X86也不会对逻辑上有先后依赖关系的操作进行重排序,所以LoadLoad也会变省略。
二、为什么会出现内存屏障
由于现在计算机存在多级缓存且多核场景,为了保证读取到的数据一致性以及并行运行时所计算出来的结果一致,在硬件层面实现一些指令,从而来保证指定执行的指令的先后顺序。比如上图:双核cpu,每个核心都拥有独立的一二级缓存,而缓存与缓存之间需要保证数据的一致性所以这里才需要加添屏障来确保数据的一致性。三级缓存为各CPU共享,最后都是主内存,所以这些存在交互的CPU都需要通过屏障手段来保证数据的唯一性。
内存屏障存在的意义就是为了解决程序在运行过程中出现的内存乱序访问问题,内存乱序访问行为出现的理由是为了提高程序运行时的性能,Memory Bariier能够让CPU或编译器在内存访问上有序。
在进一步剖析为什么会出现内存屏障之前,如果你对Cache原理还不了解,强烈建议先阅读一下这篇文章,对Cache有了一定的了解之后,再阅读下面的内容。
2.1、内存屏障出现的背景(内存乱序是怎么出现的?)
早期的处理器为有序处理器(In-order processors),有序处理器处理指令通常有以下几步:
- 指令获取
- 如果指令的输入操作对象(input operands)可用(例如已经在寄存器中了),则将此指令分发到适当的功能单元中。如果一个或者多个操 作对象不可用(通常是由于需要从内存中获取),则处理器会等待直到它们可用
- 指令被适当的功能单元执行
- 功能单元将结果写回寄存器堆(Register file,一个 CPU 中的一组寄存器)
相比之下,乱序处理器(Out-of-order processors)处理指令通常有以下几步:
- 指令获取
- 指令被分发到指令队列(Invalidate Queues,后面会讲到)
- 指令在指令队列中等待,直到输入操作对象可用(一旦输入操作对象可用,指令就可以离开队列,即便更早的指令未被执行)
- 指令被分配到适当的功能单元并执行
- 执行结果被放入队列(放入到store buffer中,而不是直接写到cache中,后面也会讲到)
- 只有所有更早请求执行的指令的执行结果被写入cache后,指令执行的结果才被写入cache(执行结果重排序,让执行看起来是有序的)
已经了解了cache的同学应该可以知道,如果CPU需要读取的地址中的数据已经已经缓存在了cache line中,即使是cpu需要对这个地址重复进行读写,对CPU性能影响也不大,但是一旦发生了cache miss(对这个地址进行第一次写操作),如果是有序处理器,CPU在从其他CPU获取数据或者直接与主存进行数据交互的时候需要等待不可用的操作对象,这样就会非常慢,非常影响性能。举个例子:
如果CPU0发起一次对某个地址的写操作,但是其local cache中没有数据,这个数据存放在CPU1的local cache中。为了完成这次操作,CPU0会发出一个invalidate的信号,使其他CPU的cache数据无效(因为CPU0需要重新写这个地址中的值,说明这个地址中的值将被改变,如果不把其他CPU中存放的该地址的值无效,那么就有可能会出现数据不一致的问题)。只有当其他之前就已经存放了改地址数据的CPU中的值都无效了后,CPU0才能真正发起写操作。需要等待非常长的时间,这就导致了性能上的损耗。
但是乱序处理器山就不需要等待不可用的操作对象,直接把invalidate message放到invalidate queues中,然后继续干其他事情,提高了CPU的性能,但也带来了一个问题,就是程序执行过程中,可能会由于乱序处理器的处理方式导致内存乱序,程序运行结果不符合我们预期的问题。
2.2理解内存屏障
不少开发者并不理解一个事实 — 程序实际运行时很可能并不完全按照开发者编写的顺序访问内存。例如:
x = r;
y = 1;
这里,y = 1很可能先于x = r执行。这就是内存乱序访问。内存乱序访问行为出现的理由是为了提升程序运行时的性能。编译器和CPU都可能引起内存乱序访问:
编译时,编译器优化进行指令重排而导致内存乱序访问;运行时,多CPU间交互引入内存乱序访问。
编译器和CPU引入内存乱序访问通常不会带来什么问题,但在一些特殊情况下(主要是多线程程序中),逻辑的正确性依赖于内存访问顺序,
这时,内存乱序访问会带来逻辑上的错误,例如:
// thread 1
while(!ok);
do(x);
// thread 2
x = 42;
ok = 1;
Ok初始化为0, 线程1等待ok被设置为1后执行do函数。
假如,线程2对内存的写操作乱序执行,也就是x赋值晚于ok赋值完成,那么do函数接受的实参很有可能出乎开发者的意料,不为42。
我们可以引入内存屏障来避免上述问题的出现。内存屏障能让CPU或者编译器在内存访问上有序。一个内存屏障之前的内存访问操作必定先于其之后的完成。
内存屏障包括两类:编译器屏障和CPU内存屏障。
编译时内存乱序访问
编译器对代码做出优化时,可能改变实际执行指令的顺序(例如g++下O2或者O3都会改变实际执行指令的顺序),看一个例子:
int x, y, r;
void f()
{
x = r;
y = 1;
}
首先直接编译次源文件:g++ -S test.cpp。我们得到相关的汇编代码如下:
movl r(%rip), %eax
movl %eax, x(%rip)
movl $1, y(%rip)
这里我们可以看到,x = r和y = 1并没有乱序执行。现使用优化选项O2(或O3)编译上面的代码(g++ -O2 –S test.cpp),生成汇编代码如下:
movl r(%rip), %eax
movl $1, y(%rip)
movl %eax, x(%rip)
我们可以清楚地看到经过编译器优化之后,movl $1, y(%rip)先于movl %eax, x(%rip)执行,这意味着,编译器优化导致了内存乱序访问。
避免次行为的办法就是使用编译器屏障(又叫优化屏障)。
Linux内核提供了函数barrier(),用于让编译器保证其之前的内存访问先于其之后的内存访问完成。
(这个强制保证顺序的需求在哪里?换句话说乱序会带来什么问题内?—
一个线程执行了 y =1 , 但实际上x=r还没有执行完成,此时被另一个线程抢占,另一个线程执行,发现y=1,以为此时x必定=r,执行相应逻辑,造成错误)
内核实现barrier()如下:
#define barrier() __asm__ __volatile__("": : :"memory")
现在把此编译器barrier加入代码中:
int x, y, r;
void f()
{
x = r;
__asm__ __volatile__("": : :"memory")
y = 1;
}
再编译,就会发现内存乱序访问已经不存在了。
除了barrier()函数外,本例还可以使用volatile这个关键字来避免编译时内存乱序访问
(且仅能避免编译时的乱序访问,为什么呢,可以参考前面部分的说明,编译器对于volatile声明究竟做了什么–
volatile关键字对于编译器而言,是开发者告诉编译器,这个变量内存的修改,可能不再你可视范围内,不要对这个变量相关的代码进行优化)。
(ps:不同语言的volatile语义是存在一定差异的,比如这里volatile和java中)
volatile关键字能让volatile变量之间的内存访问上有序,这里可以修改x和y的定义来解决问题:
volatile int x, y, r;
通过volatile关键字,使得x相对y、y相对x在内存访问上是有序的。
实际上,Linux内核中,宏ACCESS_ONCE能避免编译器对于连续的ACCESS_ONCE实例进行指令重排,其就是通过volatile实现的:
#define ACCESS_ONCE(x) (*(volatile typeof(x) *)&(x))
此代码只是将变量x转换为volatile的而已。现在我们就有了第三个修改方案:
int x, y, r;
void f()
{
ACCESS_ONCE(x) = r;
ACCESS_ONCE(y) = 1;
}
到此,基本上就阐述完成了编译时内存乱序访问的问题。下面看看CPU会有怎样的行为。
运行时内存乱序访问
运行时,CPU本身是会乱序执行指令的。早期的处理器为有序处理器(in-order processors),总是按开发者编写的顺序执行指令, 如果指令的输入操作对象(input operands)不可用(通常由于需要从内存中获取), 那么处理器不会转而执行那些输入操作对象可用的指令,而是等待当前输入操作对象可用。
相比之下,乱序处理器(out-of-order processors)会先处理那些有可用输入操作对象的指令(而非顺序执行) 从而避免了等待,提高了效率。现代计算机上,处理器运行的速度比内存快很多, 有序处理器花在等待可用数据的时间里已可处理大量指令了。即便现代处理器会乱序执行, 但在单个CPU上,指令能通过指令队列顺序获取并执行,结果利用队列顺序返回寄存器堆(详情可参考http:// http://en.wikipedia.org/wiki/Out-of-order_execution),这使得程序执行时所有的内存访问操作看起来像是按程序代码编写的顺序执行的, 因此内存屏障是没有必要使用的(前提是不考虑编译器优化的情况下)。
SMP架构需要内存屏障的进一步解释:
从体系结构上来看,首先在SMP架构下,每个CPU与内存之间,都配有自己的高速缓存(Cache),以减少访问内存时的冲突
采用高速缓存的写操作有两种模式:
(1). 穿透(Write through)模式,每次写时,都直接将数据写回内存中,效率相对较低;
(2). 回写(Write back)模式,写的时候先写回告诉缓存,然后由高速缓存的硬件再周转复用缓冲线(Cache Line)时自动将数据写回内存,
或者由软件主动地“冲刷”有关的缓冲线(Cache Line)。
出于性能的考虑,系统往往采用的是模式2来完成数据写入。
正是由于存在高速缓存这一层,正是由于采用了Write back模式的数据写入,才导致在SMP架构下,对高速缓存的运用可能改变对内存操作的顺序。
已上面的一个简短代码为例:
// thread 0 -- 在CPU0上运行
x = 42;
ok = 1;
// thread 1 – 在CPU1上运行
while(!ok);
print(x);
这里CPU1执行时, x一定是打印出42吗?让我们来看看以下图为例的说明:
假设,正好CPU0的高速缓存中有x,此时CPU0仅仅是将x=42写入到了高速缓存中,
另外一个ok也在高速缓存中,但由于周转复用高速缓冲线(Cache Line)而导致将ok=1刷会到了内存中,
此时CPU1首先执行对ok内存的读取操作,他读到了ok为1的结果,进而跳出循环,读取x的内容,
而此时,由于实际写入的x(42)还只在CPU0的高速缓存中,导致CPU1读到的数据为x(17)。
程序中编排好的内存访问顺序(指令序:program ordering)是先写入x,再写入y。
而实际上出现在该CPU外部,即系统总线上的次序(处理器序:processor ordering),却是先写入y,再写入x(这个例子中x还未写入)。
在SMP架构中,每个CPU都只知道自己何时会改变内存的内容,但是都不知道别的CPU会在什么时候改变内存的内容,也不知道自己本地的高速缓存中的内容是否与内存中的内容不一致。
反过来,每个CPU都可能因为改变了内存内容,而使得其他CPU的高速缓存变的不一致了。在SMP架构下,由于高速缓存的存在而导致的内存访问次序(读或写都有可能书序被改变)的改变很有可能影响到CPU间的同步与互斥。
因此需要有一种手段,使得在某些操作之前,把这种“欠下”的内存操作(本例中的x=42的内存写入)全都最终地、物理地完成,就好像把欠下的债都结清,
然后再开始新的(通常是比较重要的)活动一样。这种手段就是内存屏障,其本质原理就是对系统总线加锁。
回过头来,我们再来看看为什么非SMP架构(UP架构)下,运行时内存乱序访问不存在。
在单处理器架构下,各个进程在宏观上是并行的,但是在微观上却是串行的,因为在同一时间点上,只有一个进程真正在运行(系统中只有一个处理器)。
在这种情况下,我们再来看看上面提到的例子:
线程0和线程1的指令都将在CPU0上按照指令序执行。thread0通过CPU0完成x=42的高速缓存写入后,再将ok=1写入内存,此后串行的将thread0换出,thread1换入,及时此时x=42并未写入内存,但由于thread1的执行仍然是在CPU0上执行,他仍然访问的是CPU0的高速缓存,因此,及时x=42还未写回到内存中,thread1势必还是先从高速缓存中读到x=42,再从内存中读到ok=1。
综上所述,在单CPU上,多线程执行不存在运行时内存乱序访问,我们从内核源码也可得到类似结论(代码不完全摘录)
#define barrier() __asm__ __volatile__("": : :"memory")
#define mb() alternative("lock; addl $0,0(%%esp)", "mfence", X86_FEATURE_XMM2)
#define rmb() alternative("lock; addl $0,0(%%esp)", "lfence", X86_FEATURE_XMM2)
#ifdef CONFIG_SMP
#define smp_mb() mb()
#define smp_rmb() rmb()
#define smp_wmb() wmb()
#define smp_read_barrier_depends() read_barrier_depends()
#define set_mb(var, value) do { (void) xchg(&var, value); } while (0)
#else
#define smp_mb() barrier()
#define smp_rmb() barrier()
#define smp_wmb() barrier()
#define smp_read_barrier_depends() do { } while(0)
#define set_mb(var, value) do { var = value; barrier(); } while (0)
#endif
这里可看到对内存屏障的定义,如果是SMP架构,smp_mb定义为mb(),mb()为CPU内存屏障(接下来要谈的),而非SMP架构时(也就是UP架构),直接使用编译器屏障,运行时内存乱序访问并不存在。
为什么多CPU情况下会存在内存乱序访问?
我们知道每个CPU都存在Cache,当一个特定数据第一次被其他CPU获取时,此数据显然不在对应CPU的Cache中(这就是Cache Miss)。
这意味着CPU要从内存中获取数据(这个过程需要CPU等待数百个周期),此数据将被加载到CPU的Cache中,这样后续就能直接从Cache上快速访问。
当某个CPU进行写操作时,他必须确保其他CPU已将此数据从他们的Cache中移除(以便保证一致性),只有在移除操作完成后,此CPU才能安全地修改数据。
显然,存在多个Cache时,必须通过一个Cache一致性协议来避免数据不一致的问题,而这个通信的过程就可能导致乱序访问的出现,也就是运行时内存乱序访问。
受篇幅所限,这里不再深入讨论整个细节,有兴趣的读者可以研究《Memory Barriers: a Hardware View for Software Hackers》这篇文章,它详细地分析了整个过程。
现在通过一个例子来直观地说明多CPU下内存乱序访问的问题:
volatile int x, y, r1, r2;
//thread 1
void run1()
{
x = 1;
r1 = y;
}
//thread 2
void run2
{
y = 1;
r2 = x;
}
变量x、y、r1、r2均被初始化为0,run1和run2运行在不同的线程中。
如果run1和run2在同一个cpu下执行完成,那么就如我们所料,r1和r2的值不会同时为0,而假如run1和run2在不同的CPU下执行完成后,由于存在内存乱序访问的可能,这时r1和r2可能同时为0。我们可以使用CPU内存屏障来避免运行时内存乱序访问(x86_64):
void run1()
{
x = 1;
//CPU内存屏障,保证x=1在r1=y之前执行
__asm__ __volatile__("mfence":::"memory");
r1 = y;
}
//thread 2
void run2
{
y = 1;
//CPU内存屏障,保证y = 1在r2 = x之前执行
__asm__ __volatile__("mfence":::"memory");
r2 = x;
}
里mfence的含义是什么?
x86/64系统架构提供了三中内存屏障指令:
(1) sfence; (2) lfence; (3) mfence。
sfence我认为其动作,可以看做是一定将数据写回内存,而不是写到高速缓存中。
lfence的动作,可以看做是一定将数据从高速缓存中抹掉,从内存中读出来,而不是直接从高速缓存中读出来。
mfence则正好结合了两项操作。
sfence只确保写者在将数据(A->B)写入内存的顺序,并不确保其他人读(A,B)数据时,一定是按照先读A更新后的数据,再读B更新后的数据这样的顺序,
很有可能读者读到的顺序是A旧数据,B更新后的数据,A更新后的数据(只是这个更新后的数据出现在读者的后面,他并没有“实际”去读);
同理,lfence也就只能确保读者在读入顺序时,按照先读A最新的在内存中的数据,再读B最新的在内存中的数据的顺序,
但如果没有写者sfence的配合,显然,即使顺序一致,内容还是有可能乱序。
为什么仅通过保证了写者的写入顺序(sfence), 还是有可能有问题?还是之前的例子
void run1()
{
x = 1;
//CPU内存屏障,保证x=1在r1=y之前执行
__asm__ __volatile__("sfence":::"memory");
r1 = y;
}
//thread 2
void run2
{
y = 1;
//CPU内存屏障,保证y = 1在r2 = x之前执行
__asm__ __volatile__("sfence":::"memory");
r2 = x;
}
如果仅仅是对“写入”操作进行顺序化,实际上,还是有可能使的上面的代码出现r1,r2同时为0(初始值)的场景:
当CPU0上的thread0执行时,x被先行写回到内存中,但如果此时y在CPU0的高速缓存中,这时y从缓存中读出,并被赋予r1写回内存,此时r1为0。
同理,CPU1上的thread1执行时,y被先行写回到内存中,如果此时x在CPU1的高速缓存中存在,则此时r2被赋予了x的(过时)值0,同样存在了r1, r2同时为0。
这个现象实际上就是所谓的r1=y的读顺序与x=1的写顺序存在逻辑上的乱序所致(或者是r2 = x与y=1存在乱序) —
读操作与写操作之间存在乱序。而mfence就是将这类乱序也屏蔽掉。
如果是通过mfence,是怎样解决该问题的呢?
当thread1在CPU0上对x=1进行写入时,x=1被刷新到内存中,由于是mfence,他要求r1的读取操作从内存读取数据,而不是从缓存中读取数据,
因此,此时如果y更新为1,则r1 = 1; 【原文此处错误】.
同时此时由于x更新为1, r2必须从内存中读取数据,则此时r2 = 1。总而言之是r1, r2, 都为1。
Full Barrier
mfence指令实现了Full Barrier,相当于StoreLoad Barriers。mfence指令综合了sfence指令与lfence指令的作用,强制所有在mfence指令之前的store/load指令,都在该mfence指令执行之前被执行;所有在mfence指令之后的store/load指令,都在该mfence指令执行之后被执行。即,禁止对mfence指令前后store/load指令的重排序跨越mfence指令,使所有Full Barrier之前发生的操作,对所有Full Barrier之后的操作都是可见的。
mfence指令对所有CPU都可见。
-------------------------------感谢大家的支持------------------------------------
【文章福利】小编推荐自己的Linux内核技术交流群:【 865977150】整理了一些个人觉得比较好的学习书籍、视频资料共享在群文件里面,有需要的可以自行添加哦!!!前100名进群领取,额外赠送一份价值 699的内核资料包(含视频教程、电子书、实战项目及代码)
[内核资料领取,](https://docs.qq.com/doc/DTmFTc29xUGdNSnZ2) [Linux内核源码学习地址。](https://ke.qq.com/course/4032547?flowToken=1044435)
2.3内存屏障的分类
编译屏障
编译屏障只是告诉编译器,不要对当前代码进行过度的优化,保证生成的汇编代码的次序与当前高级语言的次序保持一致。编译屏障对CPU执行时产生的重排序没有任何作用。
写内存屏障
一个写内存屏障可以提供这样的保证,站在系统中的其它组件的角度来看,在屏障之前的写操作看起来将在屏障后的写操作之前发生。
如果映射到上面的例子来说,首先,写内存屏障会对处理器指令重排序做出一些限制,也就是在写内存屏障之前的写入指令一定不会被重排序到写内存屏障之后的写入指令之后。其次,在执行写内存屏障之后的写入指令之前,一定要保证清空当前CPU存储缓冲中的所有写操作,将它们全部“提交”到缓存中。这样的话系统中的其它组件(包括别的CPU),就可以保证在看到写内存屏障之后的写入数据之前先看到写内存屏障之前的写入数据。
写内存屏障仅仅限制了CPU对写操作的排序,对加载操作没有任何效果,对其它的指令也没有作用。而且,写内存屏障只是保证在写内存屏障之后的写入操作一定是在写内存屏障之前的写入操作之后被系统其它组件感知,它并不能保证在写内存屏障之前的所有写入操作的顺序,也不能保证在写内存屏障之后的所有写入操作的顺序。
写内存屏障只管自己CPU上的写入操作能够按照一定次序被系统中其它部件感知,但是如果其它部件有缓存将旧数据缓存下来了,这它管不着。这个是下面介绍的读内存屏障要管的事,因此一般写内存屏障要和读内存屏障配对使用。
读内存屏障
一个读内存屏障可以提供这样的保证,站在系统中其它组件的角度来看,所有在读内存屏障之前的加载操作将在读内存屏障之后的加载操作之前发生。
还是用上面的例子来说明,首先,读内存屏障也会对处理器指令重排做出一些限制,也就是在读内存屏障之前的读取指令一定不会被重排序到读内存屏障之后的读取指令之后。其次,在执行读内存屏障之后的读取指令之前,一定要保证处理完当前CPU的无效队列。这样的话,当前CPU的缓存状态将完全遵照MESI协议,可以保证缓存数据一致性。
读内存屏障仅仅限制了CPU对加载操作的排序,对存储操作没有任何效果,对其它指令也没有任何作用。而且,读内存屏障只是保证在读内存屏障之后的读取操作一定是在读内存屏障之前的读取操作之后才去感知内存数据变化的,它并不能保证读内存屏障之前的所有读取操作顺序,也不能保证读内存屏障之后的所有读取操作的顺序。
读内存屏障只管自己CPU上的读取操作能够按照一定次序去感知系统内存中的值,但是对于其它CPU写入系统内存的次序没有任何约束。这个是上面介绍的写内存屏障要管的事,因此一般读内存屏障要和写内存屏障配对使用。
通用内存屏障(读写内存屏障)
一个通用内存屏障可以提供这样的保证,站在系统中其它组件的角度来看,通用内存屏障之前的加载、存储操作都将在通用内存屏障之后的加载、存储操作之前发生。
还是用上面的例子来说明,首先,通用内存屏障也会对处理器指令重排做出一些限制,也就是在通用内存屏障之前的写入和读取指令一定不会被重排序到通用内存屏障之后的写入和读取指令之后。其次,在执行通用内存屏障之后的任何写入和读取取指令之前,一定要保证清空当前CPU存储缓冲中的所有写操作,并且还要处理完当前CPU的无效队列。
通用内存屏障等同于同时包含了读和写内存屏障的功能,因此也可以替换它们中的任何一个,只不过可能会一定程度上影响性能。
通用内存屏障同时限制了CPU对加载操作和存储操作的排序,但是对其它指令没有任何作用。而且,通用内存屏障只是保证在通用内存屏障之后的所有写入和读取操作一定是在通用内存屏障之前的写入和读取操作之后才执行,它并不能保证通用内存屏障之前的所有读取和写入操作的顺序,也不能保证通用内存屏障之后的所有读取和写入操作的顺序。
一般写内存屏障、读内存屏障和通用内存屏障都会默认包含编译屏障。
其实还有一种叫做数据依赖屏障的东西,但这个只是在Alpha架构下才有用,这里不做讨论了。