LINUX内核内存屏障

Posted xuhao_xuhao

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了LINUX内核内存屏障相关的知识,希望对你有一定的参考价值。

=================                          LINUX内核内存屏障                          =================
By: David Howells <dhowells@redhat.com>     Paul E. McKenney <paulmck@linux.vnet.ibm.com> 译: kouu <kouucocu@126.com> 出处: Linux内核文档 -- Documentation/memory-barriers.txt
目录:
 (*) 内存访问抽象模型.
     - 操作设备.      - 保证.
 (*) 什么是内存屏障?
     - 各式各样的内存屏障.      - 关于内存屏障, 不能假定什么?      - 数据依赖屏障.      - 控制依赖.      - SMP内存屏障的配对使用.      - 内存屏障举例.      - 读内存屏障与内存预取.
 (*) 内核中显式的内存屏障.
     - 编译优化屏障.      - CPU内存屏障.      - MMIO写屏障.
 (*) 内核中隐式的内存屏障.
     - 锁相关函数.      - 禁止中断函数.      - 睡眠唤醒函数.      - 其他函数.
 (*) 跨CPU的锁的屏障作用.
     - 锁与内存访问.      - 锁与IO访问.
 (*) 什么地方需要内存屏障?
     - 处理器间交互.      - 原子操作.      - 访问设备.      - 中断.
 (*) 内核中I/O屏障的作用.
 (*) 最小限度有序的假想模型.
 (*) CPU cache的影响.
     - Cache一致性.      - Cache一致性与DMA.      - Cache一致性与MMIO.
 (*) CPU所能做到的.
     - 特别值得一提的Alpha处理器.
 (*) 使用示例.
     - 环型缓冲区.
 (*) 引用.

================ 内存访问抽象模型 ================
考虑如下抽象系统模型:
                            :                :                             :                :                             :                :                 +-------+   :   +--------+   :   +-------+                 |       |   :   |        |   :   |       |                 |       |   :   |        |   :   |       |                 | CPU 1 |<----->|  内存  |<----->| CPU 2 |                 |       |   :   |        |   :   |       |                 |       |   :   |        |   :   |       |                 +-------+   :   +--------+   :   +-------+                     ^       :       ^        :       ^                     |       :       |        :       |                     |       :       |        :       |                     |       :       v        :       |                     |       :   +--------+   :       |                     |       :   |        |   :       |                     |       :   |        |   :       |                     +---------->|  设备  |<----------+                             :   |        |   :                             :   |        |   :                             :   +--------+   :                             :                :
假设每个CPU都分别运行着一个会触发内存访问操作的程序. 对于这样一个CPU, 其内存访问 顺序是非常松散的, 在保证程序上下文逻辑关系的前提下, CPU可以按它所喜欢的顺序来执 行内存操作. 类似的, 编译器也可以将它输出的指令安排成任何它喜欢的顺序, 只要保证不 影响程序表面的执行逻辑.
(译注: 
内存屏障是为应付内存访问操作的乱序执行而生的. 那么, 内存访问为什么会乱序呢? 这里 先简要介绍一下:
现在的CPU一般采用流水线来执行指令. 一个指令的执行被分成: 取指, 译码, 访存, 执行, 写回, 等若干个阶段. 
指令流水线并不是串行化的, 并不会因为一个耗时很长的指令在"执行"阶段呆很长时间, 而 导致后续的指令都卡在"执行"之前的阶段上. 
相反, 流水线中的多个指令是可以同时处于一个阶段的, 只要CPU内部相应的处理部件未被 占满. 比如说CPU有一个加法器和一个除法器, 那么一条加法指令和一条除法指令就可能同 时处于"执行"阶段, 而两条加法指令在"执行"阶段就只能串行工作.
这样一来, 乱序可能就产生了. 比如一条加法指令出现在一条除法指令的后面, 但是由于除 法的执行时间很长, 在它执行完之前, 加法可能先执行完了. 再比如两条访存指令, 可能由 于第二条指令命中了cache(或其他原因)而导致它先于第一条指令完成.
一般情况下, 指令乱序并不是CPU在执行指令之前刻意去调整顺序. CPU总是顺序的去内存里 面取指令, 然后将其顺序的放入指令流水线. 但是指令执行时的各种条件, 指令与指令之间 的相互影响, 可能导致顺序放入流水线的指令, 最终乱序执行完成. 这就是所谓的" 顺序流 入, 乱序流出".
指令流水线除了在资源不足的情况下会卡住之外(如前所述的一个加法器应付两条加法指令) , 指令之间的相关性才是导致流水线阻塞的主要原因.
下文中也会多次提到, CPU的乱序执行并不是任意的乱序, 而必须保证上下文依赖逻辑的正 确性. 比如: a++; b=f(a); 由于b=f(a)这条指令依赖于第一条指令(a++)的执行结果, 所以 b=f(a)将在"执行"阶段之前被阻塞, 直到a++的执行结果被生成出来.
如果两条像这样有依赖关系的指令挨得很近, 后一条指令必定会因为等待前一条执行的结果 , 而在流水线中阻塞很久. 而编译器的乱序, 作为编译优化的一种手段, 则试图通过指令重 排将这样的两条指令拉开距离, 以至于后一条指令执行的时候前一条指令结果已经得到了,  那么也就不再需要阻塞等待了.
相比于CPU的乱序, 编译器的乱序才是真正对指令顺序做了调整. 但是编译器的乱序也必须 保证程序上下文的依赖逻辑.
由于指令执行存在这样的乱序, 那么自然, 由指令执行而引发的内存访问势必也可能乱序. )
在上面的图示中, 一个CPU执行内存操作所产生的影响, 一直要到该操作穿越该CPU与系统中 其他部分的界面(见图中的虚线)之后, 才能被其他部分所感知.
举例来说, 考虑如下的操作序列:
        CPU 1           CPU 2         =============== ===============          A == 1; B == 2         A = 3;          x = A;         B = 4;          y = B;
这一组访问指令在内存系统(见上图的中间部分)上生效的顺序, 可以有24种不同的组合:
        STORE A=3,   STORE B=4,     x=LOAD A->3,   y=LOAD B->4         STORE A=3,   STORE B=4,     y=LOAD B->4,   x=LOAD A->3         STORE A=3,   x=LOAD A->3,   STORE B=4,     y=LOAD B->4         STORE A=3,   x=LOAD A->3,   y=LOAD B->2,   STORE B=4         STORE A=3,   y=LOAD B->2,   STORE B=4,     x=LOAD A->3         STORE A=3,   y=LOAD B->2,   x=LOAD A->3,   STORE B=4         STORE B=4,   STORE A=3,     x=LOAD A->3,   y=LOAD B->4         STORE B=4, ...         ...
然后这就产生四种不同组合的结果值:
        x == 1, y == 2         x == 1, y == 4         x == 3, y == 2         x == 3, y == 4
甚至于, 一个CPU在内存系统上提交的STORE操作还可能不会以相同的顺序被其他CPU所执行 的LOAD操作所感知.
进一步举例说明, 考虑如下的操作序列:
        CPU 1           CPU 2         =============== ===============          A == 1, B == 2, C = 3, P == &A, Q == &C         B = 4;          Q = P;         P = &B          D = *Q;
这里有一处明显的数据依赖, 因为在CPU2上, LOAD到D里面的值依赖于从P获取到的地址. 在 操作序列的最后, 下面的几种结果都是有可能出现的:
        (Q == &A) 且 (D == 1)         (Q == &B) 且 (D == 2)         (Q == &B) 且 (D == 4)
注意, CPU2决不会将C的值LOAD到D, 因为CPU保证在将P的值装载到Q之后才会执行对*Q的 LOAD操作(译注: 因为存在数据依赖).
操作设备 --------
对于一些设备, 其控制寄存器被映射到一组内存地址集合上, 而这些控制寄存器被访问的顺 序是至关重要的. 假设, 一个以太网卡拥有一些内部寄存器, 通过一个地址端口寄存器(A) 和一个数据端口寄存器(D)来访问它们. 要读取编号为5的内部寄存器, 可能使用如下代码:         *A = 5;         x = *D;
但是这可能会表现为以下两个序列之一(译注: 因为从程序表面看, A和D是不存在依赖的):
        STORE *A = 5, x = LOAD *D         x = LOAD *D, STORE *A = 5
其中的第二种几乎肯定会导致错误, 因为它在读取寄存器之后才设置寄存器的编号.
保证 ----
对于一个CPU, 它最低限度会提供如下的保证:
 (*) 对于一个CPU, 在它上面出现的有上下文依赖关系的内存访问将被按顺序执行. 这意味      着:
        Q = P; D = *Q;
     CPU会顺序执行以下访存:
        Q = LOAD P, D = LOAD *Q
     并且总是按这样的顺序.
 (*) 对于一个CPU, 重叠的LOAD和STORE操作将被按顺序执行. 这意味着:
        a = *X; *X = b;
     CPU只会按以下顺序执行访存:
        a = LOAD *X, STORE *X = b
     同样, 对于:
        *X = c; d = *X;
     CPU只会按以下顺序执行访存:
        STORE *X = c, d = LOAD *X
     (如果LOAD和STORE的目标指向同一块内存地址, 则认为是重叠).
还有一些事情是必须被假定或者必须不被假定的:
 (*) 必须不能假定无关的LOAD和STORE会按给定的顺序被执行. 这意味着:
        X = *A; Y = *B; *D = Z;
     可能会得到如下几种执行序列之一:
        X = LOAD *A,  Y = LOAD *B,  STORE *D = Z         X = LOAD *A,  STORE *D = Z, Y = LOAD *B         Y = LOAD *B,  X = LOAD *A,  STORE *D = Z         Y = LOAD *B,  STORE *D = Z, X = LOAD *A         STORE *D = Z, X = LOAD *A,  Y = LOAD *B         STORE *D = Z, Y = LOAD *B,  X = LOAD *A
 (*) 必须假定重叠内存访问可能被合并或丢弃. 这意味着:
        X = *A; Y = *(A + 4);
     可能会得到如下几种执行序列之一:
        X = LOAD *A; Y = LOAD *(A + 4);         Y = LOAD *(A + 4); X = LOAD *A;         X, Y = LOAD *A, *(A + 4) ;
     同样, 对于:
        *A = X; Y = *A;
     可能会得到如下几种执行序列之一:
        STORE *A = X; Y = LOAD *A;         STORE *A = Y = X;

=============== 什么是内存屏障? ===============
正如上面所说, 无关的内存操作会被按随机顺序有效的得到执行, 但是在CPU与CPU交互时或 CPU与IO设备交互时, 这可能会成为问题. 我们需要一些手段来干预编译器和CPU, 使其限制 指令顺序.
内存屏障就是这样的干预手段. 他们能保证处于内存屏障两边的内存操作满足部分有序. ( 译注: 这里"部分有序"的意思是, 内存屏障之前的操作都会先于屏障之后的操作, 但是如果 几个操作出现在屏障的同一边, 则不保证它们的顺序. 这一点下文将多次提到.)
这样的强制措施是非常重要的, 因为系统中的CPU和其他设备可以使用各种各样的策略来提 高性能, 包括对内存操作的乱序, 延迟和合并执行; 预取; 投机性的分支预测和各种缓存.  内存屏障用于禁用或抑制这些策略, 使代码能够清楚的控制多个CPU和/或设备的交互.
各式各样的内存屏障 ------------------
内存屏障有四种基本类型:
 (1) 写(STORE)内存屏障.
     写内存屏障提供这样的保证: 所有出现在屏障之前的STORE操作都将先于所有出现在屏      障之后的STORE操作被系统中的其他组件所感知.
     写屏障仅保证针对STORE操作的部分有序; 不要求对LOAD操作产生影响.
     随着时间的推移, 一个CPU提交的STORE操作序列将被存储系统所感知. 所有在写屏障      之前的STORE操作将先于所有在写屏障之后的STORE操作出现在被感知的序列中.
     [!] 注意, 写屏障一般需要与读屏障或数据依赖屏障配对使用; 参阅"SMP内存屏障配          对"章节. (译注: 因为写屏障只保证自己提交的顺序, 而无法干预其他代码读内          存的顺序. 所以配对使用很重要. 其他类型的屏障亦是同理.)
 (2) 数据依赖屏障.
     数据依赖屏障是读屏障的弱化版本. 假设有两个LOAD操作的场景, 其中第二个LOAD操      作的结果依赖于第一个操作(比如, 第一个LOAD获取地址, 而第二个LOAD使用该地址去      取数据), 数据依赖屏障确保在第一个LOAD获取的地址被用于访问之前, 第二个LOAD的      目标内存已经更新.      (译注: 因为第二个LOAD要使用第一个LOAD的结果来作为LOAD的目标, 这里存在着数      据依赖. 由前面的"保证"章节可知, 第一个LOAD必定会在第二个LOAD之前执行, 不需      要使用读屏障来保证顺序, 只需要使用数据依赖屏障来保证内存已刷新.)
     数据依赖屏障仅保证针对相互依赖的LOAD操作的部分有序; 不要求对STORE操作,       独立的LOAD操作, 或重叠的LOAD操作产生影响.
     正如(1)中所提到的, 在一个CPU看来, 系统中的其他CPU提交到内存系统的STORE操作      序列在某一时刻可以被其感知到. 而在该CPU上触发的数据依赖屏障将保证, 对于在屏      障之前发生的LOAD操作, 如果一个LOAD操作的目标被其他CPU的STORE操作所修改, 那      么在屏障完成之时, 这个对应的STORE操作之前的所有STORE操作所产生的影响, 将被      数据依赖屏障之后执行的LOAD操作所感知.
     参阅"内存屏障举例"章节所描述的时序图.
     [!] 注意, 对第一个LOAD的依赖的确是一个数据依赖而不是控制依赖. 而如果第二个          LOAD的地址依赖于第一个LOAD, 但并不是通过实际加载的地址本身这样的依赖条          件, 那么这就是控制依赖, 需要一个完整的读屏障或更强的屏障. 参阅"控制依          赖"相关章节.
     [!] 注意, 数据依赖屏障一般要跟写屏障配对使用; 参阅"SMP内存屏障的配对使用"章          节.
 (3) 读(LOAD)内存屏障.
     读屏障包含数据依赖屏障的功能, 并且保证所有出现在屏障之前的LOAD操作都将先于      所有出现在屏障之后的LOAD操作被系统中的其他组件所感知.
     读屏障仅保证针对LOAD操作的部分有序; 不要求对STORE操作产生影响.
     读内存屏障隐含了数据依赖屏障, 因此可以用于替代它们.
     [!] 注意, 读屏障一般要跟写屏障配对使用; 参阅"SMP内存屏障的配对使用"章节.
 (4) 通用内存屏障.
     通用内存屏障保证所有出现在屏障之前的LOAD和STORE操作都将先于所有出现在屏障      之后的LOAD和STORE操作被系统中的其他组件所感知.
     通用内存屏障是针对LOAD和STORE操作的部分有序.
     通用内存屏障隐含了读屏障和写屏障, 因此可以用于替代它们.
内存屏障还有两种隐式类型:
 (5) LOCK操作.
     它的作用相当于一个单向渗透屏障. 它保证所有出现在LOCK之后的内存操作都将在      LOCK操作被系统中的其他组件所感知之后才能发生.
     出现在LOCK之前的内存操作可能在LOCK完成之后才发生.
     LOCK操作总是跟UNLOCK操作配对出现的.
 (6) UNLOCK操作.
     它的作用也相当于一个单向渗透屏障. 它保证所有出现在UNLOCK之前的内存操作都将      在UNLOCK操作被系统中的其他组件所感知之前发生.
     出现在UNLOCK之后的内存操作可能在UNLOCK完成之前就发生了.
     需要保证LOCK和UNLOCK操作严格按照相互影响的正确顺序出现.
     (译注: LOCK和UNLOCK的这种单向屏障作用, 确保临界区内的访存操作不能跑到临界区      外, 否则就起不到"保护"作用了.)      使用LOCK和UNLOCK之后, 一般就不再需要其他内存屏障了(但是注意"MMIO写屏障"章节      中所提到的例外).
只有在存在多CPU交互或CPU与设备交互的情况下才可能需要用到内存屏障. 如果可以确保某 段代码中不存在这样的交互, 那么这段代码就不需要使用内存屏障. (译注: CPU乱序执行指 令, 同样会导致寄存器的存取顺序被打乱, 但是为什么不需要寄存器屏障呢? 就是因为寄存 器是CPU私有的, 不存在跟其他CPU或设备的交互.)
注意, 对于前面提到的最低限度保证. 不同的体系结构可能提供更多的保证, 但是在特定体 系结构的代码之外, 不能依赖于这些额外的保证.
关于内存屏障, 不能假定什么? ---------------------------
Linux内核的内存屏障不保证下面这些事情:
 (*) 在内存屏障之前出现的内存访问不保证在内存屏障指令完成之前完成; 内存屏障相当      于在该CPU的访问队列中画一条线, 使得相关访存类型的请求不能相互跨越. (译注:      用于实现内存屏障的指令, 其本身并不作为参考对象, 其两边的访存操作才被当作参      考对象. 所以屏障指令执行完成并不表示出现在屏障之前的访存操作已经完成. 而如      果屏障之后的某一个访存操作已经完成, 则屏障之前的所有访存操作必定都已经完成      了.)
 (*) 在一个CPU上执行的内存屏障不保证会直接影响其他系统中的CPU或硬件设备. 只会间      接影响到第二个CPU感知第一个CPU产生访存效果的顺序, 不过请看下一点:
 (*) 不能保证一个CPU能够按顺序看到另一个CPU的访存效果, 即使另一个CPU使用了内存屏      障, 除非这个CPU也使用了与之配对的内存屏障(参阅"SMP内存屏障的配对使用"章节).
 (*) 不保证一些与CPU相关的硬件不会乱序访存. CPU cache一致性机构会在CPU之间传播内      存屏障所带来的间接影响, 但是可能不是按顺序的.
        [*] 更多关于总线主控DMA和一致性的问题请参阅:
            Documentation/PCI/pci.txt             Documentation/PCI/PCI-DMA-mapping.txt             Documentation/DMA-API.txt
数据依赖屏障 ------------
数据依赖屏障的使用需求有点微妙, 并不总是很明显就能看出需要他们. 为了说明这一点,  考虑如下的操作序列:
        CPU 1           CPU 2         =============== ===============          A == 1, B == 2, C = 3, P == &A, Q == &C         B = 4;         <写屏障>         P = &B                         Q = P;                         D = *Q;
这里有明显的数据依赖, 在序列执行完之后, Q的值一定是&A和&B之一, 也就是:
        (Q == &A) 那么 (D == 1)         (Q == &B) 那么 (D == 4)
但是! CPU 2可能在看到P被更新之后, 才看到B被更新, 这就导致下面的情况:
        (Q == &B) 且 (D == 2) ????
虽然这看起来似乎是一个一致性错误或逻辑关系错误, 但其实不是, 并且在一些真实的CPU 中就能看到这样的行为(就比如DEC Alpha).
为了解决这个问题, 必须在取地址和取数据之间插入一个数据依赖或更强的屏障:
        CPU 1           CPU 2         =============== ===============          A == 1, B == 2, C = 3, P == &A, Q == &C         B = 4;         <写屏障>         P = &B                         Q = P;                         <数据依赖屏障>                         D = *Q;
这将强制最终结果是前两种情况之一, 而避免出现第三种情况.
[!] 注意, 这种非常违反直觉的情况最容易出现在cache分列的机器上, 比如, 一个cache组     处理偶数号的cache行, 另一个cache组处理奇数号的cache行. P指针可能存储在奇数号     的cache行中, 而B的值可能存储在偶数号的cache行中. 这样一来, 如果执行读操作的     CPU的偶数号cache组非常繁忙, 而奇数号cache组空闲, 它就可能看到P已被更新成新值     (&B), 而B还是旧值(2).
另一个可能需要数据依赖屏障的例子是, 从内存读取一个数值, 用于计算数组的访问偏移:
        CPU 1           CPU 2         =============== ===============          M[0] == 1, M[1] == 2, M[3] = 3, P == 0, Q == 3         M[1] = 4;         <写屏障>         P = 1                         Q = P;                         <数据依赖屏障>                         D = M[Q];
数据依赖屏障对于RCU非常重要, 举例来说. 参阅include/linux/rcupdate.h文件中的 rcu_dereference()函数. 这个函数使得当前RCU指针指向的对象被替换成新的对象时, 不会 发生新对象尚未初始化完成的情况. (译注: 更新RCU对象时, 一般步骤是: 1-为新对象分配 空间; 2-初始化新对象; 3-调用rcu_dereference()函数, 将对象指针指到新的对象上, 这 就意味着新的对象已生效. 这个过程中如果出现乱序访存, 可能导致对象指针的更新发生在 新对象初始化完成之前. 也就是说, 新对象尚未初始化完成就已经生效了. 那么别的CPU就 可能引用到一个尚未初始化完成的新对象, 从而出现错误.)
更详尽的例子请参阅"Cache一致性"章节.
控制依赖 --------
控制依赖需要使用一个完整的读内存屏障, 简单的数据依赖屏障不能使其正确工作. 考虑 下面的代码:
        q = &a;         if (p)                 q = &b;         <数据依赖屏障>         x = *q;
这段代码可能达不到预期的效果, 因为这里其实并不是数据依赖, 而是控制依赖, CPU可能 试图通过提前预测结果而对"if (p)"进行短路. 在这样的情况下, 需要的是:
        q = &a;         if (p)                 q = &b;         <读屏障>         x = *q;
(译注: 例如:         CPU 1           CPU 2         =============== ===============          a == 1, b == 2, p == 0         a = 3;         b = 4;         <写屏障>         p = 1;                         q = &a;                         if (p)                                 q = &b;                         <数据依赖屏障>                         x = *q;
CPU 1上的写屏障是为了保证这样的逻辑: 如果p == 1, 那么必定有a == 3 && b == 4. 但是到了CPU 2, 可能p的值已更新(==1), 而a和b的值未更新, 那么这时数据依赖屏障可以 起作用, 确保x = *q时a和b的值更新. 因为从代码逻辑上说, q跟a或b是有所依赖的, 数据 依赖屏障能保证这些有依赖关系的值都已更新.
然而, 换一个写法:         CPU 1           CPU 2         =============== ===============          a == 1, b == 2, p == 0         p = 1;         <写屏障>         a = 3;         b = 4;                         q = &a;                         if (p)                                 q = &b;                         <读屏障>                         x = *q;
CPU 1上的写屏障是为了保证这样的逻辑: 如果a == 3 || b == 4, 那么必定有p == 1. 但是到了CPU 2, 可能a或b的值已更新, 而p的值未更新. 那么这时使用数据依赖屏障就不能 保证p的更新. 因为从代码逻辑上说, p跟任何人都没有依赖关系. 这时必须使用读屏障, 以 确保x = *q之前, p被更新. 原文中"短路"的意思就是, 由于p没有数据依赖关系, CPU可以早早获得它的值, 而不必考虑 更新.)
SMP内存屏障的配对使用 ---------------------
在处理CPU与CPU的交互时, 对应类型的内存屏障总是应该配对使用. 缺乏适当配对的使用基 本上可以肯定是错误的.
一个写屏障总是与一个数据依赖屏障或读屏障相配对, 虽然通用屏障也可行. 类似的, 一个 读屏障或数据依赖屏障也总是与一个写屏障相配对, 尽管一个通用屏障也同样可行:
        CPU 1           CPU 2         =============== ===============         a = 1;         <写屏障>         b = 2;          x = b;                         <读屏障>                         y = a;
或:
        CPU 1           CPU 2         =============== ===============         a = 1;         <写屏障>         b = &a;         x = b;                         <数据依赖屏障>                         y = *x;
基本上, 读屏障总是需要用在这些地方的, 尽管可以使用"弱"类型.
[!] 注意, 在写屏障之前出现的STORE操作通常总是期望匹配读屏障或数据依赖屏障之后出 现的LOAD操作, 反之亦然:
        CPU 1                       CPU 2         ===============             ===============         a = 1;       ----   --->  v = c         b = 2;          \\ /      w = d         <写屏障>           \\        <读屏障>         c = 3;          / \\      x = a;         d = 4;       ----   --->  y = b;
内存屏障举例 ------------
首先, 写屏障用作部分有序的STORE操作. 考虑如下的操作序列:
        CPU 1         =======================         STORE A = 1         STORE B = 2         STORE C = 3         <写屏障>         STORE D = 4         STORE E = 5
这个操作序列会按顺序被提交到内存一致性系统, 而系统中的其他组件可能看到 STORE A, STORE B, STORE C 的组合出现在 STORE D, STORE E 的组合之前, 而组合 内部可能乱序:
        +-------+       :      :         |       |       +------+         |       |------>| C=3  |         /\\         |       |  :    +------+     -----  \\  -----> 操作被系统中的其他         |       |  :    | A=1  |            \\/       组件所感知         |       |  :    +------+             | CPU 1 |  :    | B=2  |             |       |       +------+             |       |   wwwwwwwwwwwwwwww   <--- 在这一时刻, 写屏障要求在它之         |       |       +------+            前出现的STORE操作都先于在它         |       |  :    | E=5  |            之后出现的STORE操作被提交         |       |  :    +------+             |       |------>| D=4  |             |       |       +------+         +-------+       :      :                            |                            | CPU 1发起的STORE操作被提交到内存系统的顺序                            |                            V
其次, 数据依赖屏障用作部分有序的数据依赖LOAD操作. 考虑如下的操作序列:
        CPU 1                        CPU 2         =======================      =======================          B = 7; X = 9; Y = 8; C = &Y         STORE A = 1         STORE B = 2         <写屏障>         STORE C = &B                 LOAD X         STORE D = 4                  LOAD C (得到&B)                                      LOAD *C (读取B)
没有干预的话, CPU 1的操作被CPU 2感知到的顺序是随机的, 尽管CPU 1执行了写屏障:
        +-------+       :      :                :       :         |       |       +------+                +-------+  | CPU 2所看到的         |       |------>| B=2  |-----       --->| Y->8  |  | 更新序列         |       |  :    +------+     \\          +-------+  |         | CPU 1 |  :    | A=1  |      \\     --->| C->&Y |  V         |       |       +------+       |        +-------+         |       |   wwwwwwwwwwwwwwww   |        :       :         |       |       +------+       |        :       :         |       |  :    | C=&B |---    |        :       :       +-------+         |       |  :    +------+   \\   |        +-------+       |       |         |       |------>| D=4  |    ----------->| C->&B |------>|       |         |       |       +------+       |        +-------+       |       |         +-------+       :      :       |        :       :       |       |                                        |        :       :       |       |                                        |        :       :       | CPU 2 |                                        |        +-------+       |       |             对B的取值显然不正确  --->  |        | B->7  |------>|       |                                        |        +-------+       |       |                                        |        :       :       |       |                                        |        +-------+       |       |             对X的LOAD延误了B的  --->    \\       | X->9  |------>|       |             一致性更新                   \\      +-------+       |       |                                           ----->| B->2  |       +-------+                                                 +-------+                                                 :       :
在上面的例子中, CPU 2看到的B的值是7, 尽管对*C(值应该是B)的LOAD发生在对C的LOAD之 后.
但是, 如果一个数据依赖屏障被放到CPU 2的LOAD C和LOAD *C(假设值是B)之间:
        CPU 1                        CPU 2         =======================      =======================          B = 7; X = 9; Y = 8; C = &Y         STORE A = 1         STORE B = 2         <写屏障>         STORE C = &B                 LOAD X         STORE D = 4                  LOAD C (获得&B)                                      <数据依赖屏障>                                      LOAD *C (读取B)
那么下面的情况将会发生:
        +-------+       :      :                :       :         |       |       +------+                +-------+         |       |------>| B=2  |-----       --->| Y->8  |         |       |  :    +------+     \\          +-------+         | CPU 1 |  :    | A=1  |      \\     --->| C->&Y |         |       |       +------+       |        +-------+         |       |   wwwwwwwwwwwwwwww   |        :       :         |       |       +------+       |        :       :         |       |  :    | C=&B |---    |        :       :       +-------+         |       |  :    +------+   \\   |        +-------+       |       |         |       |------>| D=4  |    ----------->| C->&B |------>|       |         |       |       +------+       |        +-------+       |       |         +-------+       :      :       |        :       :       |       |                                        |        :       :       |       |                                        |        :       :       | CPU 2 |                                        |        +-------+       |       |                                        |        | X->9  |------>|       |                                        |        +-------+       |       |           确保STORE C之前的影响  --->   \\   ddddddddddddddddd   |       |           都被后续的LOAD操作感           \\      +-------+       |       |           知到                            ----->| B->2  |------>|       |                                                 +-------+       |       |                                                 :       :       +-------+
第三, 读屏障用作部分有序的LOAD操作. 考虑如下事件序列:
        CPU 1                        CPU 2         =======================      =======================          A = 0, B = 9         STORE A=1         <写屏障>         STORE B=2                                      LOAD B                                      LOAD A
没有干预的话, CPU 1的操作被CPU 2感知到的顺序是随机的, 尽管CPU 1执行了写屏障:
        +-------+       :      :                :       :         |       |       +------+                +-------+         |       |------>| A=1  |------      --->| A->0  |         |       |       +------+      \\         +-------+         | CPU 1 |   wwwwwwwwwwwwwwww   \\    --->| B->9  |         |       |       +------+        |       +-------+         |       |------>| B=2  |---     |       :       :         |       |       +------+   \\    |       :       :       +-------+         +-------+       :      :    \\   |       +-------+       |       |                                      ---------->| B->2  |------>|       |                                         |       +-------+       | CPU 2 |                                         |       | A->0  |------>|       |                                         |       +-------+       |       |                                         |       :       :       +-------+                                          \\      :       :                                           \\     +-------+                                            ---->| A->1  |                                                 +-------+                                                 :       :
但是, 如果一个读屏障被放到CPU 2的LOAD B和LOAD A之间:
        CPU 1                        CPU 2         =======================      =======================          A = 0, B = 9         STORE A=1         <写屏障>         STORE B=2                                      LOAD B                                      <读屏障>                                      LOAD A
那么CPU 1所施加的部分有序将正确的被CPU 2所感知:
        +-------+       :      :                :       :         |       |       +------+                +-------+         |       |------>| A=1  |------      --->| A->0  |         |       |       +------+      \\         +-------+         | CPU 1 |   wwwwwwwwwwwwwwww   \\    --->| B->9  |         |       |       +------+        |       +-------+         |       |------>| B=2  |---     |       :       :         |       |       +------+   \\    |       :       :       +-------+         +-------+       :      :    \\   |       +-------+       |       |                                      ---------->| B->2  |------>|       |                                         |       +-------+       | CPU 2 |                                         |       :       :       |       |                                         |       :       :       |       |           在这一时刻, 读屏障导致 ---->   \\  rrrrrrrrrrrrrrrrr   |       |           STORE B之前的影响都被           \\     +-------+       |       |           CPU 2所感知                      ---->| A->1  |------>|       |                                                 +-------+       |       |                                                 :       :       +-------+
为了更全面地说明这一点, 考虑一下如果代码在读屏障的两边都有一个LOAD A的话, 会发生 什么:
        CPU 1                        CPU 2         =======================      =======================          A = 0, B = 9         STORE A=1         <写屏障>         STORE B=2                                      LOAD B                                      LOAD A [第一次LOAD A]                                      <读屏障>                                      LOAD A [第二次LOAD A]
尽管两次LOAD A都发生在LOAD B之后, 它们也可能得到不同的值:
        +-------+       :      :                :       :         |       |       +------+                +-------+         |       |------>| A=1  |------      --->| A->0  |         |       |       +------+      \\         +-------+         | CPU 1 |   wwwwwwwwwwwwwwww   \\    --->| B->9  |         |       |       +------+        |       +-------+         |       |------>| B=2  |---     |       :       :         |       |       +------+   \\    |       :       :       +-------+         +-------+       :      :    \\   |       +-------+       |       |                                      ---------->| B->2  |------>|       |                                         |       +-------+       | CPU 2 |                                         |       :       :       |       |                                         |       :       :       |       |                                         |       +-------+       |       |                                         |       | A->0  |------>| 一次  |                                         |       +-------+       |       |           在这一时刻, 读屏障导致 ---->   \\  rrrrrrrrrrrrrrrrr   |       |           STORE B之前的影响都被           \\     +-------+       |       |           CPU 2所感知                      ---->| A->1  |------>| 二次  |                                                 +-------+       |       |                                                 :       :       +-------+
但是也可能CPU 2在读屏障结束之前就感知到CPU 1对A的更新:
        +-------+       :      :                :       :         |       |       +------+                +-------+         |       |------>| A=1  |------      --->| A->0  |         |       |       +------+      \\         +-------+         | CPU 1 |   wwwwwwwwwwwwwwww   \\    --->| B->9  |         |       |       +------+        |       +-------+         |       |------>| B=2  |---     |       :       :         |       |       +------+   \\    |       :       :       +-------+         +-------+       :      :    \\   |       +-------+       |       |                                      ---------->| B->2  |------>|       |                                         |       +-------+       | CPU 2 |                                         |       :       :       |       |                                          \\      :       :       |       |                                           \\     +-------+       |       |                                            ---->| A->1  |------>| 一次  |                                                 +-------+       |       |                                             rrrrrrrrrrrrrrrrr   |       |                                                 +-------+       |       |                                                 | A->1  |------>| 二次  |                                                 +-------+       |       |                                                 :       :       +-------+
这里只保证, 如果LOAD B得到的值是2的话, 第二个LOAD A能得到的值是1. 对于第一个 LOAD A是不存在这样的保证的; 它可能得到A的值是0或是1.
读内存屏障与内存预取 --------------------
许多CPU会对LOAD操作进行预取: 作为性能优化的一种手段, 当CPU发现它们将要从内存LOAD 一个数据时, 它们会寻找一个不需要使用总线来进行其他LOAD操作的时机, 用于LOAD这个数 据 - 尽管他们的指令执行流程实际上还没有到达该处LOAD的地方. 实际上, 这可能使得某 些LOAD指令能够立即完成, 因为CPU已经预取到了所需要LOAD的值.
这也可能出现CPU实际上用不到这个预取的值的情况 - 可能因为一个分支而避开了这次LOAD  - 在这样的情况下, CPU可以丢弃这个值或者干脆就缓存它以备后续使用.
考虑如下场景:
        CPU 1                   CPU 2         ======================= =======================                                 LOAD B                                 DIVIDE   除法指令通常消耗                                 DIVIDE   很长的执行时间                                 LOAD A
这可能将表现为如下情况:
                                  :       :       +-------+                                   +-------+       |       |                               --->| B->2  |------>|       |                                   +-------+       | CPU 2 |                                   :       :DIVIDE |       |                                   +-------+       |       |         CPU在执行除法指令 ---> -->| A->0  |~~~~   |       |         的同时, 预取A             +-------+   ~   |       |         (译注: 此时总线空闲)      :       :   ~   |       |                                   :       :DIVIDE |       |                                   :       :   ~   |       |            一旦除法结束,  -->     :       :   ~-->|       |            CPU能马上使            :       :       |       |            LOAD指令生效           :       :       +-------+
如果在第二个LOAD之前放一个读屏障或数据依赖屏障:
        CPU 1                   CPU 2         ======================= =======================                                 LOAD B                                 DIVIDE                                 DIVIDE                                 <读屏障>                                 LOAD A
这在一定程度上将迫使预取所获得的值, 根据屏障的类型而被重新考虑. 如果没有新的更新 操作作用到已经被预取的内存地址, 则预取到的值就会被使用:
                                  :       :       +-------+                                   +-------+       |       |                               --->| B->2  |------>|       |                                   +-------+       | CPU 2 |                                   :       :DIVIDE |       |                                   +-------+       |       |         CPU在执行除法指令 ---> -->| A->0  |~~~~   |       |         的同时, 预取A             +-------+   ~   |       |                                   :       :   ~   |       |                                   :       :DIVIDE |       |                                   :       :   ~   |       |                                   :       :   ~   |       |                               rrrrrrrrrrrrrrrr~   |       |                                   :       :   ~   |       |                                   :       :   ~-->|       |                                   :       :       |       |                                   :       :       +-------+
但是, 如果存在一个来自于其他CPU的更新或失效, 那么预取将被取消, 并且重新载入值:
                                  :       :       +-------+                                   +-------+       |       |                               --->| B->2  |------>|       |                                   +-------+       | CPU 2 |                                   :       :DIVIDE |       |                                   +-------+       |       |         CPU在执行除法指令 ---> -->| A->0  |~~~~   |       |         的同时, 预取A             +-------+   ~   |       |                                   :       :   ~   |       |                                   :       :DIVIDE |       |                                   :       :   ~   |       |                                   :       :   ~   |       |                               rrrrrrrrrrrrrrrrr   |       |                                   +-------+       |       |         预取被丢弃, 并且更 --> -->| A->1  |------>|       |         新后的值被重新获取        +-------+       |       |                                   :       :       +-------+

==================== 内核中显式的内存屏障 ====================
linux内核拥有各式各样的屏障, 作用在不同层次上:
  (*) 编译优化屏障.
  (*) CPU内存屏障.
  (*) MMIO写屏障.
编译优化屏障 ------------
Linux内核有一个显式的编译器屏障函数, 能够防止编译器优化将访存操作从它的任一侧移 到另一侧:
        barrier();
这是一个通用屏障 - 弱类型的编译优化屏障并不存在.
编译优化屏障并不直接作用到CPU, CPU依然可以按其意愿乱序执行代码.
(译注: 
既然编译优化屏障并不能限制CPU的乱序访存, 那么单纯的编译优化屏障能起到什么作用呢? 以内核中的preempt_disable宏为例: 
        #define preempt_disable() \\         do \\                 inc_preempt_count(); \\                 barrier(); \\          while (0)
preempt_disable()和对应的preempt_enable()之间的代码是禁止内核抢占的, 通过对当前 进程的preempt_count进行++, 以标识进入禁止抢占状态(preempt_count==0时可抢占). 这 里在对preempt_count自增之后, 使用了

以上是关于LINUX内核内存屏障的主要内容,如果未能解决你的问题,请参考以下文章

Linux 内核 内存管理优化内存屏障 ② ( 内存屏障 | 编译器屏障 | 处理器内存屏障 | 内存映射 I/O 写屏障 )

LINUX内核内存屏障

LINUX内核内存屏障

LINUX内核内存屏障

Linux 内核 内存管理优化内存屏障 ① ( barrier 优化屏障 | 编译器优化 | CPU 执行优化 | 优化屏障源码 barrier 宏 )

Linux 内核 内存管理优化内存屏障 ① ( barrier 优化屏障 | 编译器优化 | CPU 执行优化 | 优化屏障源码 barrier 宏 )