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

这份文件不是说明书,它是为了让文件更简洁,而不是为了让文件不完整。

[En]

This document is not a specification, it is to make the document more concise, but not to make the document incomplete.

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

强调,这个文档不是一个linux对硬件的期望的规范。
文档的目的有两个:
(1) 指定对于任何内存屏障,我们能够期望的最低功能。
(2) 提供使用内存屏障的指南
注:体系结构可以提供比最低功能更多的功能,但如果少于最低功能,则体系结构是错误的。

[En]

Note: an architecture can provide more functionality than the minimum functionality, but if it is less than that, the architecture is wrong.

在体系结构下,内存障碍可以是空操作,因为在体系结构的工作方式中没有必要澄清内存障碍

[En]

The memory barrier can be an empty operation under an architecture, because it is not necessary to clarify the memory barrier in the way the architecture works

  • 操作设备
  • CPU基本保证

  • 内存屏障的种类

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

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

  • 控制依赖
  • SMP屏障配对使用
  • 内存屏障举例
  • 读内存屏障与内存预取
  • 多副本原子性

  • 编译优化屏障

  • CPU内存屏障

  • 获取锁的功能

  • 中断禁用功能
  • 睡眠和唤醒功能
  • 其他功能

  • ACQUIRES与内存访问

  • 处理器间交互

  • 原子操作
  • 访问设备
  • 中断

  • cache一致性与DMA

  • cache一致性与MMIO

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

  • 虚拟机中的客户机

  • 循环缓冲区

考虑如下抽象系统模型:

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

假设每个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, ...
    ...

这将生成四种不同组合的结果值:

[En]

This then produces the result values of four different combinations:

    x == 2, y == 1
    x == 2, y == 3
    x == 4, y == 1
    x == 4, y == 3

此外,一个CPU向内存系统提交的STORE操作还可能不会以相同的顺序被其他CPU所执行的LOAD操作所感知。

为了进一步说明Son,请考虑以下事件序列:

[En]

To further illustrate the son, consider the following event sequence:

    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中获取的地址。
在操作序列结束时,可能会得到以下结果:

[En]

At the end of the operation sequence, the following results may be obtained:

    (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 操作设备

一些设备将它们的控制寄存器映射到一组内存地址,但这些控制寄存器是按非常顺序访问的

[En]

Some devices map their control registers to a set of memory addresses, but these control registers are accessed in a very order

重要。 例如,想像一个带有一组内部的以太网卡
通过地址端口寄存器(A)访问的寄存器和数据
端口寄存器(D)。 要读取内部寄存器5,则可能会执行以下代码
使用:

    *A = 5;
    x = *D;

但这可以作为以下两个序列之一来执行:

[En]

But this may be performed as one of the following two sequences:

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

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

[En]

The second of these will almost certainly cause an error because it sets the address after trying to read the register.

1.2 CPU基本保证

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

对于一个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()也可以防止编译器的破坏。

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

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

[En]

There are many things that must or cannot be assumed:

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

[En]

Without them, the compiler has the right to make various “creative” conversions, which are described in the “compilation barrier” section.

不能假设独立的加载和存储将以给定的顺序发布。这意味着

[En]

It must not be assumed that independent loads and stores will be issued in a given order. Which means

    X = *A; Y = *B; *D = Z;

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

[En]

We may get any of the following execution order:

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

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

[En]

We may get any of the following execution order:

    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;

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

[En]

We may get any of the following execution order:

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

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

[En]

The warranties described in this section are invalid in some cases (no guarantee of order of operation, atomicity, etc.):

  • 这些保证不适用于位字段,因为编译器生成的代码通常使用非原子读-改-写序列来修改位字段。不要试图使用位域来同步并行算法。
    [En]

    these guarantees do not apply to bit fields because code generated by the compiler usually uses non-atomic read-change-write sequences to modify bit fields. Do not try to use bit fields to synchronize parallel algorithms.*

  • 即使位字段受锁定保护,给定位置字段中的所有字段也必须受锁定保护。如果位字段中的两个字段由不同的锁保护,编译器的非原子读-修改-写序列可能会导致对一个字段的更新,从而破坏相邻字段的值。
    [En]

    even if a bit field is protected by a lock, all fields in a given location field must be protected by a lock. If two fields in a bit field are protected by different locks, the compiler’s non-atomic read-modify-write sequence may cause updates to one field to destroy the values of adjacent fields.*

  • 这些保证只适用于对齐和大小正确的标量变量.”正确大小”在这里是指与”char”,”short”,”int”和”long”大小相同的变量。”正确对齐”是指自然对齐,因此”char”没有对齐限制,”short”是两字节对齐,”int”是四字节对齐,”long”在32位和64位系统上分别是四字节或八字节对齐。请注意,这些保证是在C11标准中引入的,因此在使用较旧的前C11编译器(例如gcc 4.6)时要小心。包含这种保证的标准部分是第3.14节,其中对”内存位置”的定义如下::
    &#x5185;&#x5B58;&#x4F4D;&#x7F6E;    &#x8981;&#x4E48;&#x662F;&#x6807;&#x91CF;&#x7C7B;&#x578B;&#x7684;&#x5BF9;&#x8C61;&#xFF0C;&#x8981;&#x4E48;&#x662F;&#x5BBD;&#x5EA6;&#x90FD;&#x4E0D;&#x4E3A;&#x96F6;&#x7684;&#x76F8;&#x90BB;&#x4F4D;&#x57DF;&#x7684;&#x6700;&#x5927;&#x5E8F;&#x5217;    &#x6CE8;1&#xFF1A;&#x4E24;&#x4E2A;&#x6267;&#x884C;&#x7EBF;&#x7A0B;&#x53EF;&#x4EE5;&#x66F4;&#x65B0;&#x548C;&#x8BBF;&#x95EE;&#x72EC;&#x7ACB;&#x7684;&#x5185;&#x5B58;&#x4F4D;&#x7F6E;&#xFF0C;&#x800C;&#x4E0D;&#x4F1A;&#x76F8;&#x4E92;&#x5E72;&#x6270;&#x3002;    &#x6CE8;2&#xFF1A;&#x4F4D;&#x57DF;&#x548C;&#x76F8;&#x90BB;&#x7684;&#x975E;&#x4F4D;&#x57DF;&#x6210;&#x5458;&#x4F4D;&#x4E8E;&#x72EC;&#x7ACB;&#x7684;&#x5185;&#x5B58;&#x4F4D;&#x7F6E;&#x3002;&#x8FD9;&#x540C;&#x6837;&#x9002;&#x7528;&#x4E8E;&#x4E24;&#x4E2A;&#x4F4D;&#x5B57;&#x6BB5;&#xFF0C;&#x5982;&#x679C;&#x4E00;&#x4E2A;&#x58F0;&#x660E;&#x5728;&#x5D4C;&#x5957;&#x7ED3;&#x6784;&#x58F0;&#x660E;&#x4E2D;&#xFF0C;&#x800C;&#x53E6;&#x4E00;&#x4E2A;&#x4E0D;&#x662F;&#xFF0C;&#x6216;&#x8005;&#x4E24;&#x4E2A;&#x4F4D;&#x5B57;&#x6BB5;&#x4E4B;&#x95F4;&#x7528;&#x96F6;&#x957F;&#x5EA6;&#x7684;&#x4F4D;&#x5B57;&#x6BB5;&#x58F0;&#x660E;&#x5206;&#x9694;&#xFF0C;&#x6216;&#x8005;&#x7528;&#x975E;&#x4F4D;&#x5B57;&#x6BB5;&#x6210;&#x5458;&#x58F0;&#x660E;&#x5206;&#x9694;&#x3002;&#x5982;&#x679C;&#x540C;&#x4E00;&#x4E2A;&#x7ED3;&#x6784;&#x4E2D;&#x4E24;&#x4E2A;&#x4F4D;&#x5B57;&#x6BB5;&#x4E4B;&#x95F4;&#x58F0;&#x660E;&#x7684;&#x6240;&#x6709;&#x6210;&#x5458;&#x90FD;&#x662F;&#x4F4D;&#x5B57;&#x6BB5;&#xFF0C;&#x800C;&#x4E0D;&#x7BA1;&#x4E2D;&#x95F4;&#x7684;&#x4F4D;&#x5B57;&#x6BB5;&#x957F;&#x5EA6;&#x662F;&#x591A;&#x5C11;&#xFF0C;&#x90A3;&#x4E48;&#x5E76;&#x53D1;&#x5730;&#x66F4;&#x65B0;&#x8FD9;&#x4E24;&#x4E2A;&#x4F4D;&#x5B57;&#x6BB5;&#x662F;&#x4E0D;&#x5B89;&#x5168;&#x7684;&#x3002;

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

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

[En]

Memory disorder is such an intervention. They impose a perceptible partial sort on memory operations on both sides of the barrier.

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

2.1 内存屏障的种类

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

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

[En]

The write memory barrier ensures that all storage operations specified before the barrier are perceived by other components of the system before all storage operations specified behind the barrier occur.

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

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

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

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

[En]

The data dependency barrier is a weakened version of the read barrier. In the case of two loads, the second load depends on the result of the first load (for example, the first load retrieves the address that the second load will point to), and a data dependency barrier is needed to ensure that the second loaded target is updated after the address obtained by the first load is accessed.

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

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

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

[En]

For a chart of sorting constraints, see “memory Barrier sequence example”.

  • 容易混淆的控制依赖
    [!]请注意,第一个LOAD实际上必须具有数据依赖关系,而不是控制依赖。
    如果第二个LOAD的地址依赖于第一个LOAD,但是依赖关系是通过一个条件语句而不是实际加载地址本身,
    则它是控制依赖性,优选地需要完全的读障碍。有关更多信息,请参阅“控制依赖项”部分。
    [En]

    Then it is a control dependency, preferably requiring a complete read barrier. For more information, see the “controlling dependencies” section.

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

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

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

读取内存障碍意味着数据依赖关系障碍,因此可以使用它来取代数据依赖关系障碍。

[En]

The read memory barrier implies the data dependency barrier, so it can be used to replace the data dependency barrier.

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

通用内存屏障保证所有出现在屏障之前的LOAD和STORE操作都将先于所有出现在屏障之后的LOAD和STORE操作被系统中的其他组件所感知.

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

通用内存屏障意味着一个读屏障和一个写屏障,因此可以用它来取代它们。

[En]

The universal memory barrier implies a read barrier and a write barrier, so it can be used to replace them.

还有两种隐性的记忆障碍:

[En]

There are also two implicit types of memory barriers:

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

这是一个单向的可渗透的屏障。它保证所有出现在RELEASE之前的内存操作都将在
RELEASE操作被系统中的其他组件所感知之前发生.RELEASE操作包括UNLOCK操作和smp_store_release()操作。
出现在RELEASE之后的内存操作可能看起来是在RELEASE完成之前就发生了.

使用ACQUIRE和RELEASE操作通常不需要其他种类的内存屏障。
此外,RELEASE + ACQUIRE对不能保证能替代完整的内存屏障。
然而,在ACQUIRE后的给定的变量,ACQUIRE之前的任何RELEASE前对该变量的所有存储器访问都保证是可见的。

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

[En]

In other words, in the critical section of a given variable, access to all previous critical sections of the variable is guaranteed to have been completed.

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

[En]

Other types of memory barriers can usually be avoided by using get and release operations. In addition, the release + get pair is not guaranteed to act as a complete memory barrier. However, after getting a given variable, all memory access to the variable is guaranteed to be visible before it is published. In other words, in the critical section of a given variable, access to all previous critical sections of the variable is guaranteed to have been completed.

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

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

注:对于上面提到的所有最低保证,不同的架构可能提供更多的保证

[En]

Note: for all the minimum guarantees mentioned above, different architectures may provide more assurance

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

[En]

But outside of the code for a particular architecture, you cannot rely on these additional guarantees.

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()本身的人。对于那些需要它的人,以及那些对历史感兴趣的人,这里有一个关于数据依赖障碍的故事。

数据依赖关系障碍的使用有点微妙,而且是否需要它们并不总是显而易见的。

[En]

The use of data dependency barriers is a bit subtle, and it’s not always obvious whether they are needed.

为了说明这一点,请考虑以下操作队列:

[En]

To illustrate this, consider the following operation queue:

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;
</write>

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

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

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

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

虽然这看起来似乎是一致性错误或逻辑关系错误,但其实不是,这种现象可以在特定的cpu上观察到(比如DEC Alpha)。
为了解决这个问题, 必须在取地址和取数据之间插入一个数据依赖或更强的屏障:

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

这强制执行结果是前两个结果之一,并避免生成第三个结果。

[En]

This forces the execution result to be one of the first two results and avoids the generation of the third result.

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

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

[En]

Compilers can and do break dependencies in many creative ways.

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

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

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

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

[En]

Please note that this mode should be rare. After all, the main purpose of dependent sorting is to prevent writes to data structures and the expensive overhead of cache misses caused by these writes.

该模式可用于记录罕见的错误条件等,而cpu自然发生的顺序可防止此类记录丢失。

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

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

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

2.4 控制依赖

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

[En]

Controlling dependencies can be a bit tricky because current compilers don’t understand them. The purpose of this section is to help you prevent the ignorance of the compiler from destroying your code.

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

q = READ_ONCE(a);
if (q) {
    <data dependency barrier>  /* BUG: &#x6CA1;&#x6709;&#x6570;&#x636E;&#x4F9D;&#x8D56;!!! */
    p = READ_ONCE(b);
}
</data>

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

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

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

    q = READ_ONCE(a);
    if (q) {
        WRITE_ONCE(b, 1);
    }
&#x4F8B;&#x5B50;&#x4E2D;&#x7684;READ_ONCE&#xFF08;&#xFF09;WRITE_ONCE&#xFF08;&#xFF09;&#x4E0D;&#x662F;&#x53EF;&#x9009;&#x7684;
&#x63A7;&#x5236;&#x4F9D;&#x8D56;&#x5173;&#x7CFB;&#x901A;&#x5E38;&#x4E0E;&#x5176;&#x4ED6;&#x7C7B;&#x578B;&#x7684;&#x5C4F;&#x969C;&#x914D;&#x5BF9;&#x3002;&#x4E5F;&#x5C31;&#x662F;&#x8BF4;&#xFF0C;&#x8BF7;&#x6CE8;&#x610F;&#xFF0C;READ_ONCE&#xFF08;&#xFF09;&#x548C;WRITE_ONCE&#xFF08;&#xFF09;&#x90FD;&#x4E0D;&#x662F;&#x53EF;&#x9009;&#x7684;
&#x6CA1;&#x6709;READ_ONCE&#xFF08;&#xFF09;&#xFF0C;&#x7F16;&#x8BD1;&#x5668;&#x53EF;&#x80FD;&#x5C06;'a'&#x7684;LOAD&#x4E0E;&#x5176;&#x4ED6;LOAD&#x64CD;&#x4F5C;&#x5408;&#x5E76;&#x3002;
&#x6CA1;&#x6709;WRITE_ONCE&#xFF08;&#xFF09;&#xFF0C;&#x7F16;&#x8BD1;&#x5668;&#x53EF;&#x80FD;&#x5C06;&#x2018;b&#x2019;STORE&#x4E0E;&#x5176;&#x4ED6;STORE&#x64CD;&#x4F5C;&#x5408;&#x5E76;&#x3002;
&#x8FD9;&#x53EF;&#x80FD;&#x4F1A;&#x5BF9;&#x6392;&#x5E8F;&#x7684;&#x7279;&#x522B;&#x8FDD;&#x53CD;&#x76F4;&#x89C9;&#x7684;&#x5F71;&#x54CD;&#x3002;

&#x66F4;&#x7CDF;&#x7CD5;&#x7684;&#x662F;&#xFF0C;&#x5982;&#x679C;&#x7F16;&#x8BD1;&#x5668;&#x80FD;&#x591F;&#x8BC1;&#x660E;&#x53D8;&#x91CF;'a'&#x7684;&#x503C;&#x603B;&#x662F;&#x975E;&#x96F6;&#x503C;&#xFF0C;
&#x7F16;&#x8BD1;&#x5668;&#x5C06;&#x5728;&#x5B83;&#x7684;&#x6743;&#x5229;&#x8303;&#x56F4;&#x5185;&#x901A;&#x8FC7;&#x5220;&#x9664;&#x201C;if&#x201D;&#x6761;&#x4EF6;&#x5224;&#x65AD;&#x8BED;&#x53E5;&#x5BF9;&#x539F;&#x793A;&#x4F8B;&#x8FDB;&#x884C;&#x4F18;&#x5316;&#xFF0C;&#x7ED3;&#x679C;&#x5982;&#x4E0B;&#xFF1A;

    q = a;
    b = 1;  /* BUG: &#x7F16;&#x8BD1;&#x5668;&#x548C;CPU&#x90FD;&#x80FD;&#x5BF9;&#x6307;&#x4EE4;&#x8FDB;&#x884C;&#x91CD;&#x6392;!!! */

&#x6240;&#x4EE5;&#x4E0D;&#x8981;&#x4E22;&#x5F03;READ_ONCE&#xFF08;&#xFF09;&#x3002;

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

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

不幸的是,当今的编译器在高优化级别上执行以下优化:

[En]

Unfortunately, today’s compilers do the following optimizations at high optimization levels:

q = READ_ONCE(a);
barrier();
WRITE_ONCE(b, 1);  /* BUG: No ordering vs. load from a!!! */
if (q) {
    /* WRITE_ONCE(b, 1); -- moved up, BUG!!! */
    do_something();
} else {
    /* WRITE_ONCE(b, 1); -- moved up, BUG!!! */
    do_something_else();
}

现在从LOAD”A”和STORE”b”之间没有条件语句,这意味着CPU有权限对他们进行重新排序:
条件语句是绝对必要的,即使在使用了所有编译器优化之后,它也必须存在于汇编代码中。

[En]

A conditional statement is absolutely necessary and must exist in assembly code even after using all compiler optimizations.

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

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

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

[En]

Control sorting is effective. For example:

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

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

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

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

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

q = READ_ONCE(a);
WRITE_ONCE(b, 2);
do_something_else();

这页,CPU就不再需要保证LOAD’a’和STORE’b’的顺序。
添加一个barrier()是很有吸引力的,但这没有帮助。
如果条件语句消失了,控制障碍将不会再出现。

[En]

If the conditional statement is gone, the control barrier will not come back.

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

q = READ_ONCE(a);
BUILD_BUG_ON(MAX <= 1); * 顺序执行从a load和store到b. if (q % max) { write_once(b, do_something(); } else 2); do_something_else(); < code></=>

请再次注意,STORE”b”的两个参数不同。如果他们相同的,正像前面提到的,
编译器可以将这个STORE操作移动到if语句外。

您还必须注意不要过于依赖布尔短路评估(或者仅在第一个条件为假的情况下才计算第二个条件)。

[En]

You must also be careful not to rely too much on Boolean short-circuit assessment (or calculate the second condition only if the first condition is false).

考虑如下例子:

q = READ_ONCE(a);
if (q || 1 > 0)
    WRITE_ONCE(b, 1);

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

[En]

Because the first condition cannot be wrong and the second condition is always true, the compiler can convert this example to the following:

q = READ_ONCE(a);
WRITE_ONCE(b, 1);

此示例强调编译器不能猜测您的代码的必要性。

[En]

This example emphasizes the need that the compiler cannot guess your code.

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

另外,控制依赖只适用于所讨论的if语句的then分支和else分支。
特别地,控制依赖不一定适用于if语句后面的代码。

q = READ_ONCE(a);
if (q) {
    WRITE_ONCE(b, 1);
} else {
    WRITE_ONCE(b, 2);
}
WRITE_ONCE(c, 1);  /* BUG: No ordering against the read from 'a'. */

人们很容易认为这个代码实际上是有序的,因为编译器不能对volatile修饰的操作(READ_ONCE、WRITE_ONCE操作)重新排序
也不能对条件语句中的WRITE操作排序。
不幸的是,对于这种推理,编译器可能将两个写入”b”编译为条件移动指令,就像在这个奇怪的伪汇编代码:

    ld r1,a
    cmp r1,$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来说是本地的。更多信息请参阅”多副本原子性”一节。

综上所述:

  • 控制依赖可以对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语句之后的代码
  • 控制依赖通常与其他类型的障碍配对。
    [En]

    Control dependencies are usually paired with other types of barriers.*

  • 控制依赖不提供多副本原子性。如果需要所有cpu同时查看一个给定的存储,可以使用smp_mb()。
  • 编译器不理解控制依赖。 因此,您的工作是确保编译器不会破坏您的代码。

2.5 SMP屏障配对使用

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

通用障碍是成对的,尽管它们也与大多数其他类型的障碍成对,尽管没有多拷贝原子性。

[En]

Universal barriers are paired, although they are also paired with most other types of barriers, although there is no multi-copy atomicity.

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);
</read></write>

Or:

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

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);
</implicit></general>

基本上,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);
</read></write>

2.6 内存屏障举例

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

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

该操作序列被顺序地提交给内存一致性系统,并且系统的其他组件可以看到

[En]

This sequence of operations is sequentially submitted to the memory consistency system, and other components of the system can see

{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 : e="5" be committed before further 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)
</write>

没有干预的话, 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, 尽管对LOAD C(值应该是B)发生在LOAD C之后.
但是,如果在CPU 2的LOAD C 和LOAD
C(即:B)之间放置数据依赖障碍的话:

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

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

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

在没有干预的情况下,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
</read></write>

那么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]
</read></write>

尽管两次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      } &#x9664;&#x6CD5;&#x6307;&#x4EE4;&#x901A;&#x5E38;&#x6D88;&#x8017;
            DIVIDE      } &#x5F88;&#x957F;&#x7684;&#x6267;&#x884C;&#x65F6;&#x95F4;
            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
</read>

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

[En]

If no change is made to the presumed memory location, only the inferred value will be used:

                                        :       :       +-------+
                                        +-------+       |       |
                                    --->| 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可见。本文档的其余部分将讨论这种较弱的形式,但为了简洁起见,我们将其简单称为”多副本原子性”。

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

[En]

The following example demonstrates the atomicity of multiple copies:

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
</read></general>

假设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)
</read></data>

这种替换会破坏多副本原子性:在这个例子中,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);
}
r0 == 1 && r1 == 1 && r2 == 1

此外,由于cpu0()和cpu1()之间的release-acquire关系,cpu1()必须看到cpu0()的写操作,因此禁止出现以下结果:

r1 == 1 && r5 == 0

然而,release-acquire链提供的顺序对于参与该链的cpu来说是本地的,并且不适用于cpu3(),至少除了stores。因此,可能产生以下结果:
我不明白为什么会发生这种事。

[En]

Don’t understand why this happened.

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

此外,还可能出现以下结果:

[En]

In addition, the following results are also possible:

我不明白为什么会发生这种事。

[En]

Don’t understand why this happened.

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

请注意,这一结果甚至可能出现在神话中的顺序一致性系统中,在该系统中,没有任何东西被重新排序。

[En]

Note that this result may even occur in a mythical sequential consistency system, in which nothing is reordered.

同样,如果您的代码需要对所有操作进行完全排序,请始终使用公共屏障。

[En]

Again, if your code needs to fully sort all operations, always use a common barrier.

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

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

3.1 编译优化屏障

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

barrier();

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

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

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

    inside the loop, forces the compiler to load the variables used in the loop condition statement each time the loop is executed.*

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

编译器有权利对同一个变量重新排序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对同一个变量的访问提供了缓存一致性。

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

    while (tmp = a)
        do_something_with(tmp);

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

[En]

While the following code is suitable for single-threaded code in a sense, it is almost certainly not what developers want:

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

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

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

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

    while (tmp = a)
        do_something_with(tmp);

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

[En]

This can result in the following code, which is completely safe in single-threaded code, but can be fatal in concurrent code:

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保存到堆栈上。这种保存和稍后恢复的开销是编译器重新加载变量的原因。这样做对于单线程代码是完全安全的,所以您需要告诉编译器在哪些情况下不安全。

如果编译器知道装载的值是多少,它就有权利完全忽略装载。例如,如果编译器可以证明变量’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’加载。)

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

[En]

For example, suppose you have the following:

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

编译器有权对内存访问重新排序,除非您告诉它不要这样做。

[En]

The compiler has the right to reorder memory access unless you tell it not to do so.

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

[En]

For example, consider the following interaction between procedure-level code and interrupt handlers:

void process_level(void)
{
    msg = get_message();
    flag = true;
}

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

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

void process_level(void)
{
    flag = true;
    msg = get_message();
}

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

void process_level(void)
{
    WRITE_ONCE(msg, get_message());
    WRITE_ONCE(flag, true);
}

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

请注意,如果该中断处理程序本身可以被访问”flag”和”msg”的中断处理程序中断,
例如嵌套中断或NMI,则需要在interrupt_handler()中使用READ_ONCE()和WRITE_ONCE()。
否则,除了用于文档的目的,interrupt_handler()中不需要使用READ_ONCE()和WRITE_ONCE()。
(另请注意,嵌套中断通常不会在现代Linux内核中出现,实际上,如果中断处理程序返回中断使能,您将获得一个WARN_ONCE()splat。)

您应该假设编译器移动READ_ONCE()和WRITE_ONCE()代码不能越过包含READ_ONCE(),WRITE_ONCE(),barrier()或类似原语的代码。

使用barrier()也可以达到这种效果,但是READ_ONCE()和WRITE_ONCE()更有选择性:使用READ_ONCE()和WRITE_ONCE(),编译器只需要忘记指定内存位置的内容,而使用barrier(),编译器必须丢弃它当前缓存在任何机器寄存器中的所有内存位置的值。当然,编译器也必须遵守READ_ONCE()和WRITE_ONCE()发生的顺序,尽管CPU不需要这样做。

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

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

编译器可以通过如下优化来保存分支:

[En]

The compiler can save a branch by optimizing as follows:

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

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

[En]

The compiler may attempt to use two 16-bit storage immediate instructions to implement the following 32-bit storage:

p = 0x00010002;

请注意,GCC确实使用了这种优化,这并不奇怪,因为它可能需要两个以上的指令来构建常量,然后存储它。
因此,这种优化在单线程代码中是成功的。

[En]

Therefore, this optimization is successful in single-threaded code.

事实上,最近的一个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内存障碍:

&#x7C7B;&#x578B;            &#x5F3A;&#x5236;&#x5C4F;&#x969C;                    SMP&#x73AF;&#x5883;&#x751F;&#x6548;&#x547D;&#x4EE4;
=============== ======================= ===========================
=============== ======================= ===========================
GENERAL         mb()                    smp_mb()
WRITE           wmb()                   smp_wmb()
READ            rmb()                   smp_rmb()
DATA DEPENDENCY                         READ_ONCE()

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

[En]

With the exception of the data dependency barrier, all memory barriers imply compiler barriers. Data dependencies do not force any additional compiler sorting.

题外话:在对于数据依赖关系,编译器应该按照正确的顺序发出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重排内存操作,从而影响内存操作在设备上出现的顺序。

还有一些更高级的屏障功能:

[En]

There are also some more advanced barrier functions:

  • 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);
  • 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()方法排序的补充。
    对于来自永久内存的加载,现有的读取内存屏障足以确保读取顺序。
    [En]

    For loads from persistent memory, the existing read memory barrier is sufficient to ensure read order.

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

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

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操作可能会失败,原因可能是无法立即获得锁,或者由于在睡眠等待锁可用时收到未阻塞的信号。
失败的锁并不意味着任何形式的障碍。

[En]

A failed lock does not mean any kind of barrier.

[!] 注意:锁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

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

[En]

This reordering can lead to deadlocks.

但是,这是不可能发生的,因为如果出现这样的死锁威胁,则RELEASE将简单地完成,从而避免死锁。

    &#x4E3A;&#x4EC0;&#x4E48;&#x4F1A;&#x8FD9;&#x6837;&#x5462;?

    &#x4E00;&#x4E2A;&#x5173;&#x952E;&#x70B9;&#x662F;&#xFF0C;&#x6211;&#x4EEC;&#x53EA;&#x8BA8;&#x8BBA;&#x8FDB;&#x884C;&#x91CD;&#x6392;&#x5E8F;&#x7684;CPU&#xFF0C;&#x800C;&#x4E0D;&#x662F;&#x7F16;&#x8BD1;&#x5668;&#x3002;&#x5982;&#x679C;&#x7F16;&#x8BD1;&#x5668;(&#x6216;&#x8005;&#x5F00;&#x53D1;&#x4EBA;&#x5458;)&#x5207;&#x6362;&#x4E86;&#x64CD;&#x4F5C;&#xFF0C;&#x5C31;&#x53EF;&#x80FD;&#x53D1;&#x751F;&#x6B7B;&#x9501;&#x3002;

    &#x4F46;&#x5047;&#x8BBE;CPU&#x91CD;&#x65B0;&#x6392;&#x5E8F;&#x4E86;&#x8FD9;&#x4E9B;&#x64CD;&#x4F5C;&#x3002;&#x5728;&#x8FD9;&#x79CD;&#x60C5;&#x51B5;&#x4E0B;&#xFF0C;&#x7A0B;&#x5E8F;&#x96C6;&#x4EE3;&#x7801;&#x4E2D;&#x89E3;&#x9501;&#x5728;&#x7684;&#x9501;&#x4E4B;&#x524D;&#x3002;CPU&#x53EA;&#x662F;&#x9009;&#x62E9;&#x5148;&#x5C1D;&#x8BD5;&#x6267;&#x884C;&#x540E;&#x9762;&#x7684;&#x9501;&#x64CD;&#x4F5C;&#x3002;&#x5982;&#x679C;&#x53D1;&#x751F;&#x6B7B;&#x9501;&#xFF0C;&#x8BE5;&#x9501;&#x64CD;&#x4F5C;&#x5C06;&#x7B80;&#x5355;&#x5730;&#x65CB;&#x8F6C;(&#x6216;&#x5C1D;&#x8BD5;&#x7761;&#x7720;&#xFF0C;&#x7A0D;&#x540E;&#x4F1A;&#x8BE6;&#x7EC6;&#x4ECB;&#x7ECD;)&#x3002;CPU&#x6700;&#x7EC8;&#x5C06;&#x6267;&#x884C;unlock&#x64CD;&#x4F5C;(&#x5728;&#x6C47;&#x7F16;&#x4EE3;&#x7801;&#x4E2D;&#x9501;&#x64CD;&#x4F5C;&#x4E4B;&#x524D;)&#xFF0C;&#x8FD9;&#x5C06;&#x89E3;&#x5F00;&#x6F5C;&#x5728;&#x7684;&#x6B7B;&#x9501;&#xFF0C;&#x4F7F;&#x9501;&#x64CD;&#x4F5C;&#x6210;&#x529F;&#x3002;

    &#x4F46;&#x5982;&#x679C;&#x9501;&#x662F;&#x4E00;&#x4E2A;&#x7761;&#x7720;&#x9501;&#x5462;?&#x5728;&#x8FD9;&#x79CD;&#x60C5;&#x51B5;&#x4E0B;&#xFF0C;&#x4EE3;&#x7801;&#x5C06;&#x5C1D;&#x8BD5;&#x8FDB;&#x5165;&#x8C03;&#x5EA6;&#x5668;&#xFF0C;&#x6700;&#x7EC8;&#x4F1A;&#x9047;&#x5230;&#x5185;&#x5B58;&#x969C;&#x788D;&#xFF0C;&#x8FD9;&#x5C06;&#x8FEB;&#x4F7F;&#x4E4B;&#x524D;&#x7684;&#x89E3;&#x9501;&#x64CD;&#x4F5C;&#x5B8C;&#x6210;&#xFF0C;&#x518D;&#x6B21;&#x89E3;&#x5F00;&#x6B7B;&#x9501;&#x3002;&#x53EF;&#x80FD;&#x5B58;&#x5728;&#x7761;&#x7720;&#x89E3;&#x9501;&#x7ADE;&#x4E89;&#xFF0C;&#x4F46;&#x9501;&#x5B9A;&#x539F;&#x8BED;&#x5728;&#x4EFB;&#x4F55;&#x60C5;&#x51B5;&#x4E0B;&#x90FD;&#x9700;&#x8981;&#x6B63;&#x786E;&#x5730;&#x89E3;&#x51B3;&#x6B64;&#x7C7B;&#x7ADE;&#x4E89;&#x3002;

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

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

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

*A = a;
*B = b;
ACQUIRE
*C = c;
*D = d;
RELEASE
*E = e;
*F = f;

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

[En]

The following sequence of events is acceptable:

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

[+] 注意, { _F,_A} 代表一次合并访问.

但下列执行顺序均不能接受:

[En]

But none of the following execution order is acceptable:

{ _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 睡眠和唤醒功能

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

[En]

Sleeping and waking up events marked in global data can be seen as an interaction between two pieces of data:

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

[En]

The task status of the task waiting for the event and the global data used to indicate the event.

为了确保这以正确的顺序发生,启动休眠过程的原语和启动唤醒的原语都有一些障碍。

[En]

To make sure that this happens in the right order, there are some barriers to the primitive that starts the sleep process and the primitive that initiates awakening.

首先,睡眠者通常遵循以下事件顺序:

[En]

First of all, sleepers usually follow this sequence of events:

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
</general>

set_current_state()可能包含在以下函数中:

prepare_to_wait();
prepare_to_wait_exclusive();

因此,这也意味着在状态之后设置记忆屏障。将上述函数重新打包。

[En]

Therefore, this also means setting the memory barrier after the state. The above functions are packaged again.

装在其他一些函数中, 所有这些包装函数都相当于在对应的位置插入了内存屏障:

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

其次,执行唤醒的代码通常如下所示:

[En]

Second, the code that performs wake-up usually looks like this:

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
</general></general>

其中的”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。

[En]

If wake-up does occur, one of the two loads (at least) must see 1. On the other hand, if wake-up does not occur, both loads may see 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()和类似函数隐含了完整的内存屏障.

在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内核中, 一段单线程的
线性码也总是正确工作。然而,在四种情况下,混乱肯定是一个问题:

[En]

Linear code also always works correctly. However, there are four situations in which disorder is definitely a problem:

  • 处理器间交互.

  • 原子操作.

  • 访问设备.

  • 中断.

6.1 处理器间交互

当有一个具有多个处理器的系统时,系统中的多个CPU可能同时在同一个数据集上工作。
这可能会导致同步问题,通常的处理方法是使用锁。

[En]

This can cause synchronization problems, and the usual way to handle it is to use locks.

然而,锁是相当昂贵的,所以如果可能的话,最好不要使用锁。

[En]

However, locks are quite expensive, so it is best not to use locks if possible.

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

例如,考虑R / W信号量慢速路径。 这里一个等待的进程在信号量上排队,
由于信号量的特点,进程的堆栈链接到信号量的等待进程列表:

[En]

Due to the characteristics of the semaphore, the stack of the process is linked to the waiting process list of the semaphore:

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()函数需要这样做:

换句话说, 这个过程会执行如下事件序列:

LOAD waiter->list.next;
LOAD waiter->task;
STORE waiter->task;
CALL wakeup
RELEASE task

如果这些步骤中的任何一个出现故障,整个过程都可能导致错误。

[En]

If any of these steps are out of order, the whole process may lead to errors.

一旦waiter进程将自己挂入等待队列, 并释放了信号量里的锁, 这个等待进程就不会再获得这
个锁了; 它要做的事情就是在继续工作之前, 等待waiter结构中的task指针被清空。
而既然waiter结构存在于等待进程的栈上, 这就意味着, 如果在waiter结构中的next指针被读
取之前, task指针先被清空了的话,那么, 这个等待进程可能已经在另一个CPU上开始运行了
并且在up*()函数有机会读取到next指针之前, 栈空间上对应的waiter结构可能已经被复用了.

看看上面的事件顺序可能会发生什么:

[En]

See what might happen to the above sequence of events:

`
CPU 1 CPU 2
=============================== ===============================
down_xxx()
Queue waiter
Sleep
up_yyy()
LOAD waiter->task;
STORE waiter->task;
Woken up by other event Resume processing
down_xxx() returns
call foo()
foo() clobbers *waiter
LOAD waiter->list.next;

Original: https://www.cnblogs.com/fanguang/p/16643434.html
Author: 反光
Title: linux 6.0 内核内存屏障文档翻译

原创文章受到原创版权保护。转载请注明出处:https://www.johngo689.com/522779/

转载文章受原作者版权保护。转载请注明原作者出处!

(0)

大家都在看

亲爱的 Coder【最近整理,可免费获取】👉 最新必读书单  | 👏 面试题下载  | 🌎 免费的AI知识星球