Add new zh translation zh_CN/memory-barrier.txt based on v6.5-rc5. Signed-off-by: Gang Li <gang.li@xxxxxxxxx> --- .../translations/zh_CN/memory-barriers.txt | 2458 +++++++++++++++++ 1 file changed, 2458 insertions(+) create mode 100644 Documentation/translations/zh_CN/memory-barriers.txt diff --git a/Documentation/translations/zh_CN/memory-barriers.txt b/Documentation/translations/zh_CN/memory-barriers.txt new file mode 100644 index 000000000000..caa2775cc9c6 --- /dev/null +++ b/Documentation/translations/zh_CN/memory-barriers.txt @@ -0,0 +1,2458 @@ +译注: +本文仅为方便汉语阅读,不保证与英文版本同步; +若有疑问,请阅读英文版本; +若有翻译问题,请通知译者; +若想修改文档,也请先修改英文版本。 + + ============================ + Linux 内核内存屏障 + ============================ + +作者:David Howells <dhowells@xxxxxxxxxx> + Paul E. McKenney <paulmck@xxxxxxxxxxxxx> + Will Deacon <will.deacon@xxxxxxx> + Peter Zijlstra <peterz@xxxxxxxxxxxxx> + +译者:李港 Gang Li <gang.li@xxxxxxxxx> + +========== +免责声明 +========== + +本文档不是一个规范;它由于有意 (为了简洁) 或无意 (由于人为因素) 的原因而不完整。本文档 +旨在指导如何使用 Linux 提供的各种内存屏障,如果有疑问 (而且有很多),请在邮件列表咨询。 +一些疑问可以通过参考正式的内存一致性模型和 tools/memory-model/ 下的相关文档来解决。 +这些内存模型是维护者的集体意见,而不是准确无误的实现规范。 + +再次重申,本文档不是 Linux 对硬件的规范或期望。 + +本文档的目的有两个: + + (1) 为内核屏障函数指定可以依赖的最小功能,以及 + + (2) 提供关于如何使用屏障的指南。 + +请注意,一个架构可以为任何屏障提供超出最低要求的功能,但是,如果架构提供的功能少于最低 +要求,则该架构是错误的。 + +还要注意的是,在某些架构中,部分屏障可能是空操作。因为该架构已经保证了该内存序,使得 +显式屏障是不必要的。 + +======== +目录 +======== + + (*) 抽象内存访问模型。 + + - 设备操作。 + - CPU 的保证。 + + (*) 什么是内存屏障? + + - 内存屏障的种类。 + - 关于内存屏障不可假设什么? + - 地址依赖屏障 (旧) 。 + - 控制依赖。 + - SMP 屏障配对。 + - 内存屏障序列示例。 + - 读屏障与预读。 + - 多拷贝原子性。 + + (*) 显式内核屏障函数。 + + - 编译器屏障。 + - CPU 内存屏障。 + + (*) 隐式内核屏障函数。 + + - 取锁函数。 + - 关中断函数。 + - 睡眠和唤醒函数。 + - 其他函数。 + + (*) Inter-CPU 获取屏障。 + + - 获取与内存访问。 + + (*) 哪里需要内存屏障? + + - 处理器间交互。 + - 原子操作。 + - 访问设备。 + - 中断。 + + (*) 内核 I/O 屏障。 + + (*) 假想的最小执行顺序模型。 + + (*) CPU 缓存的影响。 + + - 缓存一致性。 + - 缓存一致性与 DMA。 + - 缓存一致性与 MMIO。 + + (*) CPU 相关。 + + - 还有就是Alpha。 + - 虚拟机客户端。 + + (*) 使用示例。 + + - 环形缓冲区。 + + (*) 参考资料。 + +============================ +抽象内存访问模型 +============================ + +考虑以下抽象模型: + + : : + : : + : : + +-------+ : +--------+ : +-------+ + | | : | | : | | + | | : | | : | | + | CPU 1 |<----->| 内存 |<----->| CPU 2 | + | | : | | : | | + | | : | | : | | + +-------+ : +--------+ : +-------+ + ^ : ^ : ^ + | : | : | + | : | : | + | : v : | + | : +--------+ : | + | : | | : | + | : | | : | + +---------->| 设备 |<----------+ + : | | : + : | | : + : +--------+ : + : : + +每个 CPU 执行一个访存程序。在本文的抽象 CPU 中,访问指令的顺序非常松散,每个 CPU 可以 +按照任意顺序执行访存指令,每个 CPU 都保证在本核心看来,最终执行结果与不乱序的情况相同。 +同样,编译器也可以按照任意顺序排列指令,只要不影响程序的运行结果。 + +CPU可以通过一些本地缓存来提高指令运行效率,内存操作会被缓存在当前CPU上,每个 CPU 都能 +按顺序看到自己的内存操作。但每个缓存项写入到主存的顺序未知,在上图中,即每条指令的结果穿过 +虚线的顺序未知。 + + +例如,考虑以下内存操作序列: + + CPU 1 CPU 2 + =============== =============== + { A == 1; B == 2 } + A = 3; x = B; + B = 4; y = A; + +CPU 外部可能看到如下任意顺序: + + STORE A=3, STORE B=4, y=LOAD A->3, x=LOAD B->4 + STORE A=3, STORE B=4, x=LOAD B->4, y=LOAD A->3 + STORE A=3, y=LOAD A->3, STORE B=4, x=LOAD B->4 + STORE A=3, y=LOAD A->3, x=LOAD B->2, STORE B=4 + STORE A=3, x=LOAD B->2, STORE B=4, y=LOAD A->3 + STORE A=3, x=LOAD B->2, y=LOAD A->3, STORE B=4 + STORE B=4, STORE A=3, y=LOAD A->3, x=LOAD B->4 + STORE B=4, ... + ... + +可以得到四种不同的结果组合: + + x == 2, y == 1 + x == 2, y == 3 + x == 4, y == 1 + x == 4, y == 3 + + +此外,一个 CPU 核心向主存提交的写可能不会被另一个 CPU 核心按顺序读。 + + +再举一个例子,考虑这个序列: + + CPU 1 CPU 2 + =============== =============== + { A == 1, B == 2, C == 3, P == &A, Q == &C } + B = 4; Q = P; + P = &B; D = *Q; + +这里有一个明显的地址依赖,D 的值取决于 CPU 2 从 P 读到的地址。在序列结束时,可能出现以下任何结果: + + (Q == &A) 和 (D == 1) + (Q == &B) 和 (D == 2) + (Q == &B) 和 (D == 4) + +请注意,CPU 2 永远不会 将 C 的值写入 D,因为 CPU 会在读 *Q 前将 P 赋值给 Q。 + + +设备操作 +----------------- + +有些设备是通过将自己的寄存器映射到内存来控制的,访问控制寄存器的顺序非常重要。假设一个 +网卡有一组通过地址端口寄存器 (A) 和数据端口寄存器 (D) 访问的内部寄存器。 +要读取 5 号内部寄存器,可以使用以下代码: + + *A = 5; + x = *D; + +可能有两种执行顺序: + + STORE *A = 5, x = LOAD *D + x = LOAD *D, STORE *A = 5 + +其中第二个序列几乎肯定会导致故障,因为先读后写。 + + +CPU 的保证 +---------- + +如果我们使用了编译器屏障 READ_ONCE 和 WRITE_ONCE,那就可以避免编译器对代码进行优化, +此时生成的汇编指令跟代码是一样的,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() 还可以防止编译器优化。 + + (*) 对同一地址或重叠地址的读写操作应当是有序的: + + 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 + + (CPU 应当按照代码执行,不能自行优化) 。 + +如果不使用编译器屏障,编译器可能进行如下优化: + + (*) 没有 READ_ONCE() 和 WRITE_ONCE() 这两个编译器屏障,编译器可以在确保单线程安全 + 的情况下进行各种优化,这些优化在 COMPILER BARRIER 部分有介绍。 + + (*) 编译器会使读写乱序: + + X = *A; Y = *B; *D = Z; + + 我们可能会得到以下任意序列: + + X = LOAD *A, Y = LOAD *B, STORE *D = Z + X = LOAD *A, STORE *D = Z, Y = LOAD *B + Y = LOAD *B, X = LOAD *A, STORE *D = Z + Y = LOAD *B, STORE *D = Z, X = LOAD *A + STORE *D = Z, X = LOAD *A, Y = LOAD *B + STORE *D = Z, Y = LOAD *B, X = LOAD *A + + (*) 对相同地址的访问可能会合并或丢弃。这意味着对于: + + X = *A; Y = *(A + 4); + + 我们可能会得到以下任意序列: + + X = LOAD *A; Y = LOAD *(A + 4); + Y = LOAD *(A + 4); X = LOAD *A; + {X, Y} = LOAD {*A, *(A + 4) }; + + 对于: + + *A = X; *(A + 4) = Y; + + 我们可能会得到以下任意序列: + + STORE *A = X; STORE *(A + 4) = Y; + STORE *(A + 4) = Y; STORE *A = X; + STORE {*A, *(A + 4) } = {X, Y}; + +上述内容不适用于如下情况: + + (*) 不适用于位域,因为编译器通常会生成使用非原子性的 读-修改-写 序列修改这些位域的代码。 + 不要尝试使用位域来同步并行算法。 + + (*) 给定位域中的所有字段必须由一个锁保护。如果给定位域中的两个字段受不同锁保护,编译器的 + 非原子性读-修改-写序列可能会导致更新一个字段时破坏相邻字段的值。 + + (*) 这些保证仅适用于正确对齐且大小正确的标量变量。"正确大小" 目前意味着变量的大小与 + "char"、"short"、"int" 和 "long" 相同。"正确对齐" 指的是自然对齐,因此对于 + "char" 没有约束,"short" 需要两字节对齐,"int" 需要四字节对齐,对于 32 位和 64 + 位系统上的 "long" 分别需要四字节或八字节对齐。请注意,这些保证已引入 C11 标准, + 因此在使用较旧的编译器 (例如 gcc 4.6) 时要小心。包含此保证的标准部分 + 是第 3.14 节,它将 "memory location" 定义如下: + + 内存位置 + 是标量类型的对象,或者是所有宽度非零的相邻位域的最大序列 + + 注意1:两个执行线程可以更新和访问单独的内存位置,而不会相互干扰。 + + 注意2:位域和相邻的非位域成员位于单独的内存位置。对于两个相邻位域,如 + 果其中一个位域在嵌套结构中,而另一个没有,或者两个位域之间隔着一个零 + 长度的位域,或者他们被一个非位域成员分隔,那这两个位域也位于单独的内存 + 位置。如果在两个位域之间所有的成员也都是位域,那么无论两个位域间插入 + 多少位域,都认为是一个内存地址,同时更新这两个位域是不安全的。 + + +========================= +什么是内存屏障? +========================= + +如上所述,独立的内存操作在 CPU 外看起来是随机执行的,这对 CPU 之间的交互和 I/O 可能会 +造成问题。我们需要一种方法来限制编译器和 CPU 的乱序。 + +内存屏障就是这样的干预手段。它们使得屏障两侧的内存操作不能跨越屏障。 + +这种强制排序很重要,因为系统中的 CPU 和其他设备可以使用各种技巧来提高性能, +包括重排序、延迟执行、组合内存操作、预读、分支预测以及各种类型的缓存。内存屏 +障用于覆盖或抑制这些技巧,使代码能够合理地控制多个 CPU 和/或设备之间的交互。 + + +内存屏障的种类 +--------------------------- + +内存屏障主要有四种基本类型: + + (1) 写(store 或 write)屏障。 + + 写内存屏障保证,在系统的其他组件看来,所有屏障前的写操作都在该屏障后的写操作执行前完成。 + + 写屏障仅对写进行排序;不要求对读(LOAD)排序。 + + 写屏障前的写操作不会被乱序到写屏障之后,写屏障之后的写操作不会被乱序到写屏障前。 + + [!] 请注意,写屏障通常应与读屏障或地址依赖屏障配对;请参阅 "SMP 屏障配对" 子节。 + + + (2) 地址依赖屏障 (旧)。 + + 地址依赖屏障是一种较弱的读屏障。在执行两个读操作的情况下,第一个读为第二个读提供地址。 + 需要地址依赖屏障来确保第二个操作读到的是第一个操作读到的地址处的最新值。 + + 地址依赖屏障仅对相互依赖的读进行排序;不对写、单个读或重叠读排序。 + + 如果第一次读的地址与其他 CPU 的写地址重叠,那么其他 CPU 上写地址之前的写操作,在 + 地址依赖屏障之后都对当前 CPU 可见。当然前提是其他 CPU 上都是用了写屏障。 + + 请参阅 "内存屏障序列示例" 一节以查看示意图。 + + [!] 请注意,第一个读操作确实需要有一个 _地址_ 依赖,而不是控制依赖。如果第二个 + 读操作的地址依赖于第一个读操作,但依赖性是通过条件而不是实际读地址本身,那么 + 它是一个 _控制_ 依赖,需要完整的读屏障或更严格的屏障。有关更多信息,请参阅 + "控制依赖" 一节。 + + [!] 请注意,地址依赖屏障通常应与写屏障配对;请参阅 "SMP 屏障配对" 子节。 + + [!] 内核版本 v5.9 删除了显式地址依赖屏障的内核 API。 + 如今,READ_ONCE() 和 rcu_dereference() 已经包含了地址依赖屏障,无需显式调用。 + + (3) 读 (load 或 read) 屏障。 + + 在系统其他组件看来,所有在屏障之前的 LOAD 操作将在所有屏障之后的 LOAD 操作之前发生。 + + 读屏障仅对读进行部分排序;不要求对写产生任何影响。 + + 读屏障包含地址依赖屏障。 + + [!] 请注意,读屏障通常应与写屏障配对;请参阅 "SMP 屏障配对" 子节。 + + + (4) 通用内存屏障。 + + 在系统的其他组件看来,通用屏障前的所有内存操作都在屏障前发生,屏障后的所有内存操作都 + 在屏障后发生。 + + 通用内存屏障对读和写都进行排序。 + + 通用内存屏障包含了读和写内存屏障,因此可以替代它们。 + + +还有几种隐式屏障: + + (5) ACQUIRE 操作。 + + 这是单向屏障。它保证对系统的其他组件来说,在 ACQUIRE 操作之后的所有内存操作都在 + ACQUIRE 操作之后发生。ACQUIRE 操作包括 LOCK 操作以及 smp_load_acquire() + 和 smp_cond_load_acquire()。 + + 在 ACQUIRE 操作之前发生的内存操作可以被排到 ACQUIRE 后面执行。 + + ACQUIRE 操作几乎总是应该与 RELEASE 操作配对。 + + + (6) RELEASE 操作。 + + 这也是一个单向屏障。它保证了在 RELEASE 操作之前的所有内存操作相对 + 于系统的其他组件来说,看起来是在 RELEASE 操作之前发生。RELEASE 操作包括 + UNLOCK 操作和 smp_store_release() 操作。 + + 在 RELEASE 操作之后发生的内存操作可能看起来是在 RELEASE 之前发生的。 + + 使用 ACQUIRE 和 RELEASE 操作通常排除了对其他类型内存屏障的需求。此外, + RELEASE+ACQUIRE -不- 保证充当完整的内存屏障。然而,在对给定变量执行 + ACQUIRE 操作之后,在该变量上先前的任何 RELEASE 操作之前的所有内存访问都保 + 证是可见的。换句话说,在给定变量的临界区内,对该变量的所有先前临界区的所有访问 + 都保证已经完成。 + + 这意味着 ACQUIRE 充当最小的 "acquire" 操作,而 RELEASE 充当最小的 "release" 操作。 + +在 atomic_t.txt 中描述的原子操作具有 ACQUIRE 和 RELEASE 变体,此外还有完全有序 +和松散(无屏障)定义。对于执行读和写的复合原子操作,ACQUIRE 语义仅适用于读, +RELEASE 语义仅适用于写。 + +只有在 CPU 核心之间或 CPU 和设备之间可能存在交互的情况下,才需要内存屏障。如果代码没有 +多核访问,那么就不需要内存屏障。 + + +请注意,这些是 _最低_ 保证。不同的架构可能会提供更严格的保证,但在特定于架构的代码 +之外,它们可能 _不_ 被依赖。 + + +内存屏障做不到什么? +---------------------------------------------- + +Linux 内核内存屏障不能保证以下几点: + + (*) 读写内存屏障不能控制所有访存指令,它们只能控制特定类型的指令。比如读屏障只能控制读指令, + 写屏障只能控制写指令。 + + (*) 一个 CPU 上发出内存屏障不会对系统中的另一个 CPU 或任何其他硬件产生直接影响。 + 内存屏障只能间接地影响其他 CPU 看到该 CPU 的访存顺序,但: + + (*) 即使其他 CPU 正确使用了内存屏障,也不能保证一个 CPU 会正确地看到其他 CPU 的访存顺序, + 除非第一个CPU_也_使用了匹配的内存屏障 (请参阅"SMP屏障配对"的子节) 。 + + (*) CPU 外的硬件[*]也可能乱序。CPU 缓存一致性机制应在 CPU 之间传播内存屏障的效果, + 但可能不是按顺序进行的。 + + [*] 关于总线主控 DMA 和一致性的信息,请阅读: + + Documentation/driver-api/pci/pci.rst + Documentation/core-api/dma-api-howto.rst + Documentation/core-api/dma-api.rst + + +地址依赖屏障 (旧) +---------------------------------------- + +从Linux内核 v4.15 开始,DEC Alpha 的 READ_ONCE() 包含了地址依赖屏障,这意味着只有 +那些在 DEC Alpha 架构和在 READ_ONCE() 本身上工作的人需要关注这部分。 +对于需要它以及对历史感兴趣的人,下面是关于地址依赖屏障的故事。 + +[!] 虽然地址依赖在 读-读 和 读-写 关系中都能观察到,但在 读-写 情况下不需要地址依赖屏障。 + +地址依赖屏障的使用有点微妙,有时不容易看出哪里需要它。为了说明这一点,请看下列事件: + + CPU 1 CPU 2 + =============== =============== + { A == 1, B == 2, C == 3, P == &A, Q == &C } + B = 4; + <写屏障> + WRITE_ONCE(P, &B); + Q = READ_ONCE_OLD(P); + D = *Q; + +[!] READ_ONCE_OLD() 对应于 pre-4.15 内核的 READ_ONCE(),它并未包含地址依赖屏障。 + +这里明显存在地址依赖,看起来在序列结束时,Q 必须是 &A 或 &B,以及: + + (Q == &A) 相当于 (D == 1) + (Q == &B) 相当于 (D == 4) + +但是! CPU 2 可能在感知 B _之前_ 感知到 P,从而导致以下情况: + + (Q == &B) 且 (D == 2) ???? + +虽然这看起来像是一种一致性或因果关系维护的失败,但实际上并不是,而且在某些真实的 +CPU(如 DEC Alpha)上可以观察到这种行为。 + +为了处理这种情况,自内核版本 v4.15 以来,READ_ONCE() 提供了一个隐式的地址依赖屏障: + + CPU 1 CPU 2 + =============== =============== + { A == 1, B == 2, C == 3, P == &A, Q == &C } + B = 4; + <写屏障> + WRITE_ONCE(P, &B); + Q = READ_ONCE(P); + <隐式地址依赖屏障> + D = *Q; + +这样可以强制读地址前刷新缓存,避免读到旧值。 + + +[!] 请注意,这种极具违反直觉的情况最容易出现在具有分割缓存的机器上,例如, +一个缓存处理偶数缓存行,另一个处理奇数缓存行。指针 P 可能写在奇数缓存行中, +变量 B 可能写在偶数缓存行中。然后,如果偶数行非常繁忙,而奇数行处于空闲状态, +人们可以看到指针 P 的新值 (&B),但变量 B 的旧值 (2) 还没来得及更新。(译者注:详见: +https://www.cs.umd.edu/~pugh/java/memoryModel/AlphaReordering.html); + + +地址依赖屏障不需要对 读-写 进行排序,因为 Linux 内核支持的 CPU 应当符合以下规范: +不会进行写入,直到 (1) 写入实际会发生, (2) 写入的位置, (3) 要写入的值。 +但请仔细阅读 "CONTROL DEPENDENCIES" 部分和 +Documentation/RCU/rcu_dereference.rst 文件:编译器可以并且确实以许多极具创意的 +方式破坏依赖关系。 + + CPU 1 CPU 2 + =============== =============== + { A == 1, B == 2, C = 3, P == &A, Q == &C } + B = 4; + <写屏障> + WRITE_ONCE(P, &B); + Q = READ_ONCE_OLD(P); + WRITE_ONCE(*Q, 5); + +因此,对 Q 的读和写无需地址依赖屏障。换句话说,即使没有 READ_ONCE() 的隐式地址 +依赖屏障,这个结果也是被禁止的: + + (Q == &B) && (B == 4) + +请注意,这种模式应该是罕见的。毕竟,依赖性排序的全部意义就在于防止对数据结构的写操作, +以及与这些写操作相关的昂贵缓存未命中。这种模式可以用来记录罕见的错误条件等,CPU 的自 +然排序可以防止这些记录丢失。 + + +请注意,地址依赖提供的排序仅局限于包含它的 CPU。有关更多信息,请参见 "多拷贝原子性" 部分。 + + +地址依赖屏障对于 RCU 非常重要,例如,请参见 include/linux/rcupdate.h +中的 rcu_assign_pointer() 和 rcu_dereference()。这避免其他 CPU 读到新指针指向 +的地址的旧值。 + +有关更详尽的示例,请参见"缓存一致性"小节。 + + +控制依赖 +-------------------- + +控制依赖可能有点棘手,因为当前的编译器并不能理解它们。本节的目的是帮您防止 +编译器破坏您的代码。 + +读-读 控制依赖需要一个完整的读内存屏障,而不仅仅是一个隐式的地址依赖屏障来使其正常工作。 +考虑以下代码: + + q = READ_ONCE(a); + <隐式地址依赖屏障> + if (q) { + /* BUG: 没有地址依赖!!! */ + p = READ_ONCE(b); + } + +这将无法产生预期的效果,因为没有实际的地址依赖,而是一个控制依赖,CPU 可能通过预读拿到 +了变量的旧值。应该这么做: + + q = READ_ONCE(a); + if (q) { + <读屏障> + p = READ_ONCE(b); + } + +然而,写操作不会被预先执行。这意味着 读-写 控制依赖是有效的,如下例所示: + + q = READ_ONCE(a); + if (q) { + WRITE_ONCE(b, 1); + } + +控制依赖可以与其他类型的屏障正常配对。 +尽管如此,请注意 READ_ONCE() 和 WRITE_ONCE() 都是必须的!在没有 READ_ONCE() 的情况下, +编译器可能将 'a' 的读与其他 'a' 的读合并。没有 WRITE_ONCE(),编译器可能将'b'的写 +与其他'b'的写合并。任何一种情况都可能对排序产生影响。 + +更糟糕的是,如果编译器能够证明 (比如) 变量'a'的值始终不为零,那么它完全有权利通过 +消除"if"语句来优化原始示例,如下所示: + + q = a; + b = 1; /* BUG: 编译器和 CPU 都可以重排序!!! */ + +所以不要省略 READ_ONCE()。 + +人们可能会尝试在 "if" 语句的两个分支上执行相同的写,如下所示: + + q = READ_ONCE(a); + if (q) { + barrier(); + WRITE_ONCE(b, 1); + do_something(); + } else { + barrier(); + WRITE_ONCE(b, 1); + do_something_else(); + } + +不幸的是,当前的编译器会在高优化级别下将其转换为以下形式: +编译器优化掉了 CPU 所需的条件依赖! + + q = READ_ONCE(a); + barrier(); + WRITE_ONCE(b, 1); /* BUG: 与读 a 乱序!!! */ + if (q) { + /* WRITE_ONCE(b, 1); -- 向上移动,BUG!!! */ + do_something(); + } else { + /* WRITE_ONCE(b, 1); -- 向上移动,BUG!!! */ + do_something_else(); + } + +现在,从'a'读和写到'b'之间没有条件,这意味着CPU完全有权利对它们进行重新排序:条件绝 +对是必需的,即使在应用了所有编译器优化之后,在汇编代码中也必须存在。因此,如果您需要在此 +示例中进行排序,您需要显式编译器屏障,例如 smp_store_release(): + + q = READ_ONCE(a); + if (q) { + smp_store_release(&b, 1); + do_something(); + } else { + smp_store_release(&b, 1); + do_something_else(); + } + +在没有显式编译器屏障的情况下,两个分支只写不同的值才不会被提取公共子表达式,例如: + + 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 不需要尊重从变量 'a' 读到变量 'b' 的写之间的顺序。人们可能会想到 +添加一个 barrier(),但这并没有帮助。条件已经消失,barrier 也无法恢复它。因此,如果你 +依赖于此排序,你应该确保 MAX 大于 1,例如: + + q = READ_ONCE(a); + BUILD_BUG_ON(MAX <= 1); /* 将 a 的读顺序与 b 的写顺序关联。*/ + if (q % MAX) { + WRITE_ONCE(b, 1); + do_something(); + } else { + WRITE_ONCE(b, 2); + do_something_else(); + } + +请再次注意,两个分支的写 'b' 应当是不同的。如果它们是相同的,如前面所述,编译器可能会 +将此公共表达式移出 'if' 语句。 + +你还必须小心不要过分依赖布尔短路。考虑以下示例: + + q = READ_ONCE(a); + if (q || 1 > 0) + WRITE_ONCE(b, 1); + +因为第二个条件总是为真,所以编译器可以将此示例转换为以下内容,从而丢弃控制依赖: + + q = READ_ONCE(a); + WRITE_ONCE(b, 1); + +这个例子强调了确保编译器无法猜测你的代码的重要性。更一般地说,尽管 READ_ONCE() 确实强制 +编译器一定要生成读指令,但它并不强制编译器使用结果。 + +此外,控制依赖仅适用于 if 语句的 then 子句和 else 子句。不适用于 +if 语句之后的代码: + + q = READ_ONCE(a); + if (q) { + WRITE_ONCE(b, 1); + } else { + WRITE_ONCE(b, 2); + } + WRITE_ONCE(c, 1); /* BUG: 不受控制依赖的约束。*/ + +人们可能会认为,因为编译器无法重新排序 volatile 访问,并且还无法将 'b' 的写入与条件 +重新排序,所以实际上确实存在顺序。不幸的是,编译器可能将两个写入 'b' 编 +译为条件移动指令,如汇编语言: + + ld r1,a + cmp r1,$0 + cmov,ne r4,$1 + cmov,eq r4,$2 + st r4,b + st $1,c + +读 'a' 读和写 'c' 间没有任何依赖关系,可能会被 CPU 乱序。控制依赖仅扩展到一对 cmov 指令和写 'b'。 +简而言之,控制依赖仅适用于有关 if 语句的 then 子句和 else 子句中的写 +(包括由这两个子句调用的函数) ,而不适用于该 if 语句之后的代码。 + + +请注意,控制依赖提供的排序仅限于包含它的 CPU。有关更多信息,请参阅 "多拷贝原子性" 一节。 + + +总结: + + (*) 控制依赖可以对 读-写 进行排序。然而,它们*不*保证 读-读 和 写-* 排序。如果你需要这 + 些其他形式的排序,使用 smp_rmb(),smp_wmb() 或 smp_mb()。 + + (*) 如果 "if" 语句的两个分支都以相同变量的相同写开始,请使用 smp_mb() 或 + smp_store_release()。请注意,在 "if" 语句的每个分支开始使用 barrier() + 是*不*充分的,因为如上面的例子所示,编译器可以在遵守 barrier() 规则的前提下 + 破坏控制依赖。 + + (*) 控制依赖需要在读和写之间至少有一个条件,而且这个条件必须涉及到先前的读。 + 如果编译器能够优化掉这个条件,那么它也会优化掉排序。仔细使用 READ_ONCE() + 和 WRITE_ONCE() 可以帮助保留所需的条件。 + + (*) 控制依赖要求编译器避免将依赖关系重排序为不存在。仔细使用 READ_ONCE() 或 + atomic{,64}_read() 可以帮助保留你的控制依赖。有关更多信息,请参阅 + COMPILER BARRIER (编译器屏障) 部分。 + + (*) 控制依赖仅适用于包含控制依赖的 if 语句的 then 子句和 else 子句, + 包括这两个子句调用的任何函数。控制依赖*不*适用于包含控制依赖的 if 语句之后的代码。 + + (*) 控制依赖与其他类型的屏障正常配对。 + + (*) 控制依赖*不*提供多拷贝原子性。如果你需要所有 CPU 同时看到给定的写,请使用 smp_mb()。 + + (*) 编译器不理解控制依赖。因此,你的任务是确保它们不破坏你的代码。 + + +SMP 内存屏障配对 +------------------- + +处理 CPU 核间同步时,某些内存屏障必须配对使用。不配对肯定会出错。 + +通用屏障与彼此成对,它们也与大多数其他类型的屏障配对,它们没有多拷贝原子性。 +acquire 屏障与 release 屏障配对,但两者也可以与其他屏障配对,当然也包括通用屏障。 +写屏障与地址依赖屏障、控制依赖屏障、acquire 屏障、release 屏障、读屏障或通用屏障配对。 +类似地,读屏障、控制依赖或地址依赖屏障与写屏障、获取屏障、释放屏障或通用屏障配对: + + CPU 1 CPU 2 + =============== =============== + WRITE_ONCE(a, 1); + <写屏障> + WRITE_ONCE(b, 2); x = READ_ONCE(b); + <读屏障> + y = READ_ONCE(a); + +或: + + CPU 1 CPU 2 + =============== =============================== + a = 1; + <写屏障> + WRITE_ONCE(b, &a); x = READ_ONCE(b); + <隐式地址依赖屏障> + y = *x; + +甚至是: + + CPU 1 CPU 2 + =============== =============================== + r1 = READ_ONCE(y); + <通用屏障> + WRITE_ONCE(x, 1); if (r2 = READ_ONCE(x)) { + <隐式控制依赖> + WRITE_ONCE(y, 1); + } + + assert(r1 == 0 || r2 == 0); + +基本上,读屏障必须始终存在,即使它可能是"较弱"类型。 + +[!] 请注意,写屏障之前的写操作通常应与读屏障或地址依赖屏障之后的操作匹配,反之亦然: + + CPU 1 CPU 2 + =================== =================== + WRITE_ONCE(a, 1); }---- --->{ v = READ_ONCE(c); + WRITE_ONCE(b, 2); } \ / { w = READ_ONCE(d); + <写屏障> \ <读屏障> + WRITE_ONCE(c, 3); } / \ { x = READ_ONCE(a); + WRITE_ONCE(d, 4); }---- --->{ y = READ_ONCE(b); + + +内存屏障序列示例 +------------------------------------ + +首先,写屏障对写操作起到排序作用。 +考虑以下事件序列: + + 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 } <--- 写屏障要求在屏障之前的所有写操作 + | | +------+ } 对系统其他部分可见后, + | | : | E=5 | } 再进行进一步的写操作 + | | : +------+ } + | |------>| D=4 | } + | | +------+ + +-------+ : : + | + | CPU 1 向内存提交写的顺序 + | + V + + +其次,地址依赖屏障对地址依赖读起到排序作用。考虑以下事件序列: + + 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 | V + | | : +------+ \ +-------+ + | CPU 1 | : | A=1 | \ --->| C->&Y | + | | +------+ | +-------+ + | | wwwwwwwwwwwwwwww | : : + | | +------+ | : : + | | : | C=&B |--- | : : +-------+ + | | : +------+ \ | +-------+ | | + | |------>| D=4 | ----------->| C->&B |------>| | + | | +------+ | +-------+ | | + +-------+ : : | : : | | + | : : | | + | : : | CPU 2 | + | +-------+ | | + 此时没有屏障 ---> | | B->7 |------>| | + 可能会预读错误的 B (!) | +-------+ | | + | : : | | + | +-------+ | | + 读 X 后才更新 B ---> \ | X->9 |------>| | + \ +-------+ | | + ----->| B->2 | +-------+ + +-------+ + : : + + +在上面的例子中,尽管 *C 的读 (即 B) 在 C 的读之后,CPU 2 仍然认为 B 是 7。 + +然而,如果在 CPU 2 上的 C 读和 *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 |------>| | + | +-------+ | | + 确保1写C前的所有操作对后续 ---> \ aaaaaaaaaaaaaaaaa | | + 2的读都是可见的 \ +-------+ | | + ----->| B->2 |------>| | + +-------+ | | + : : +-------+ + + +第三,读屏障可以约束读的顺序。考虑以下事件序列: + + 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上的B读和A读之间放置一个读屏障: + + CPU 1 CPU 2 + ======================= ======================= + { A = 0, B = 9 } + STORE A=1 + <写屏障> + STORE B=2 + LOAD B + <读屏障> + LOAD A + +那么,由CPU 1施加的部分排序将被CPU2正确感知: + + + +-------+ : : : : + | | +------+ +-------+ + | |------>| A=1 |------ --->| A->0 | + | | +------+ \ +-------+ + | CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 | + | | +------+ | +-------+ + | |------>| B=2 |--- | : : + | | +------+ \ | : : +-------+ + +-------+ : : \ | +-------+ | | + ---------->| B->2 |------>| | + | +-------+ | CPU 2 | + | : : | | + | : : | | + 读屏障后的所有读操作都能 ----> \ rrrrrrrrrrrrrrrrr | | + 感知到写屏障前的写操作 \ +-------+ | | + ---->| A->1 |------>| | + +-------+ | | + : : +-------+ + + +为了更完整地说明这个问题,考虑一下如果代码在读屏障的两侧都包含了读 A,可能会发生什么: + + CPU 1 CPU 2 + ======================= ======================= + { A = 0, B = 9 } + STORE A=1 + <写屏障> + STORE B=2 + LOAD B + LOAD A [第一次加载 A] + <读屏障> + LOAD A [第二次加载 A] + +尽管两次加载 A 都在加载 B 之后进行,但它们可能会得到不同的值: + + +-------+ : : : : + | | +------+ +-------+ + | |------>| A=1 |------ --->| A->0 | + | | +------+ \ +-------+ + | CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 | + | | +------+ | +-------+ + | |------>| B=2 |--- | : : + | | +------+ \ | : : +-------+ + +-------+ : : \ | +-------+ | | + ---------->| B->2 |------>| | + | +-------+ | CPU 2 | + | : : | | + | : : | | + | +-------+ | | + | | A->0 |------>| 第一次 | + | +-------+ | | + 此时,读写屏障配对会使 ----> \ rrrrrrrrrrrrrrrrr | | + 屏障之前的所有写 \ +-------+ | | + 对 CPU 2 可见 ---->| A->1 |------>| 第二次 | + +-------+ | | + : : +-------+ + + +但在读屏障完成之前,CPU 1 对 A 的更新可能已经对 CPU 2 变得可察觉了: + + +-------+ : : : : + | | +------+ +-------+ + | |------>| A=1 |------ --->| A->0 | + | | +------+ \ +-------+ + | CPU 1 | wwwwwwwwwwwwwwww \ --->| B->9 | + | | +------+ | +-------+ + | |------>| B=2 |--- | : : + | | +------+ \ | : : +-------+ + +-------+ : : \ | +-------+ | | + ---------->| B->2 |------>| | + | +-------+ | CPU 2 | + | : : | | + \ : : | | + \ +-------+ | | + ---->| A->1 |------>| 1st | + +-------+ | | + rrrrrrrrrrrrrrrrr | | + +-------+ | | + | A->1 |------>| 2nd | + +-------+ | | + : : +-------+ + + +可以确定的是如果读B的值为2 (B == 2) ,那么第二次读A的值一定为1 (A == 1) 。对于第一次 +读A的值没有这样的保证;它可能是 A == 0 或者 A == 1。 + + +读屏障与预读 +-------------------- + +许多 CPU 会对读进行预测:当它们检测到它们需要从内存中读某变量,同时没有其他读操作在使用 +总线时,即使指令流还没有执行到那个读,CPU 也会提前做读操作。这样当真正执行到读操作 +时,指令可以立即完成而无需等待内存相应。 + +如果预测失败,这条指令实际没有执行,那 CPU 就会丢弃或将预读到的数据缓存备用。 + +考虑以下情况: + + CPU 1 CPU 2 + ======================= ======================= + LOAD B + DIVIDE } 除法指令通常 + DIVIDE } 需要很长时间才能执行完 + LOAD A + +如下所示: + + : : +-------+ + +-------+ | | + --->| B->2 |------>| | + +-------+ | CPU 2 | + : :DIVIDE | | + +-------+ | | + 在执行除法操作时,CPU忙碌 ---> --->| A->0 |~~~~ | | + 于此,对A的读进行推测 +-------+ ~ | | + : : ~ | | + : :DIVIDE | | + : : ~ | | + 一旦除法完成,CPU就可以立即执行 --> : : ~-->| | + 后续的读操作而无需读取内存 : : | | + : : +-------+ + + +在第二次读之前放置一个读屏障或地址依赖屏障: + + CPU 1 CPU 2 + ======================= ======================= + LOAD B + DIVIDE + DIVIDE + <读屏障> + LOAD A + +这将强制刷新读缓存,也就是取消所有预测,取消程度取决于所使用的屏障类型。 +如果没有对推测的内存位置进行更改,那么将直接使用推测出的值: + + : : +-------+ + +-------+ | | + --->| B->2 |------>| | + +-------+ | CPU 2 | + : :DIVIDE | | + +-------+ | | + CPU 忙于除法,预读 A ---> --->| A->0 |~~~~ | | + +-------+ ~ | | + : : ~ | | + : :DIVIDE | | + : : ~ | | + : : ~ | | + rrrrrrrrrrrrrrrr~ | | + : : ~ | | + : : ~-->| | + : : | | + : : +-------+ + + +但如果有来自其他 CPU 的更新或使无效操作,那么预测就会取消并重新读取。 + + : : +-------+ + +-------+ | | + --->| B->2 |------>| | + +-------+ | CPU 2 | + : :DIVIDE | | + +-------+ | | + 忙于除法的 CPU 预读 A ---> --->| A->0 |~~~~ | | + +-------+ ~ | | + : : ~ | | + : :DIVIDE | | + : : ~ | | + : : ~ | | + rrrrrrrrrrrrrrrrr | | + +-------+ | | + 丢弃预测并读取新值 ---> --->| A->1 |------>| | + +-------+ | | + : : +-------+ + + +多拷贝原子性 +---------------- + +多拷贝原子性是关于排序的一种深刻直观概念,但现实计算机系统并不总是提供这种概念, +即一个给定的写操作对所有CPU同时可见,或者说,所有CPU都同意所有写操作变得可 +见的顺序。然而,支持完全的多拷贝原子性将忽略一些有价值的硬件优化,因此,一种较弱的 +形式"其他多拷贝原子性"仅保证一个给定的写操作对所有 -其他- CPU同时可见。 +本文档剩余部分讨论这种较弱的形式,但为简洁起见,仍然简称为"多拷贝原子性"。 + +以下示例演示了多拷贝原子性: + + CPU 1 CPU 2 CPU 3 + ======================= ======================= ======================= + { X = 0, Y = 0 } + STORE X=1 r1=LOAD X (读取 1) LOAD Y (读取 1) + <通用屏障> <读屏障> + STORE Y=r1 LOAD X + +假设CPU 2 读 X 得 1,然后将其写到 Y,CPU 3 读 Y 得 1。 +这表明CPU 1写X、CPU2读X写Y、CPU3读Y是依次发生的。 +那么问题是,CPU 3 读X能否得到0? + +因为 CPU 3 读X在CPU 2读X之后,自然期望CPU 3能读到1。这 +种期望源于多拷贝原子性:一个CPU对内存的改动,要么同时对其他组件可见,要么同时对其他组件 +不可见,不能对某些组件可见对某些组件又不可见。然而,Linux内核并不要求系统具有多拷贝原子性。 + +在上面的例子中使用通用内存屏障可以弥补多拷贝原子性的缺失。在这个例子中, +如果CPU 2读X得1,CPU 3读Y得1,那么CPU 3读X也必须返回1。 + +然而,依赖关系、读屏障和写屏障并不总是能够弥补非多拷贝原子性。例如,假设从上面的例子中 +移除CPU 2的通用屏障,只留下以下数据依赖关系: + + CPU 1 CPU 2 CPU 3 + ======================= ======================= ======================= + { X = 0, Y = 0 } + STORE X=1 r1=LOAD X (读取 1) LOAD Y (读取 1) + <数据依赖> <读屏障> + STORE Y=r1 LOAD X (读取 0) + +不用显式屏障就不保证多拷贝原子性:在这个例子中,CPU 2从X读返回1,CPU 3从Y读返回1, +以及从X读返回0都是完全合法的。 + +关键在于,尽管CPU 2的数据依赖关系对其读和写操作进行了排序,但它并不能确保对CPU 1的 +写操作进行排序。因此,如果这个程序运行在一个非多拷贝原子系统上,其中CPU 1和2共享一个 +写缓冲区或一个缓存级别,CPU 2可能会提前访问CPU 1的写操作。因此,需要通用屏障来确保 +所有CPU在多次访问的组合顺序上达成一致。 + +通用屏障不仅可以实现多拷贝原子性,还可以生成额外的排序,以确保 -所有- CPU都能以相同顺序 +感知到所有操作。相比之下,一系列 发布-获取 不能提供这种额外的排序,只有链条上的 +那些CPU保证在访问的组合顺序上达成一致。例如: + + int u, v, x, y, z; + + void cpu0(void) + { + r0 = smp_load_acquire(&x); + WRITE_ONCE(u, 1); + smp_store_release(&y, 1); + } + + void cpu1(void) + { + r1 = smp_load_acquire(&y); + r4 = READ_ONCE(v); + r5 = READ_ONCE(u); + smp_store_release(&z, 1); + } + + void cpu2(void) + { + r2 = smp_load_acquire(&z); + smp_store_release(&x, 1); + } + + void cpu3(void) + { + WRITE_ONCE(v, 1); + smp_mb(); + r3 = READ_ONCE(u); + } + +因为 cpu0()、cpu1() 和 cpu2() 构成了 smp_store_release()/smp_load_acquire() +的链条,以下结果不应出现: + + r0 == 1 && r1 == 1 && r2 == 1 + +此外,由于 cpu0() 和 cpu1() 之间的 发布-获取 关系,cpu1() 必须看到 cpu0() 的写操作, +因此以下结果不应出现: + + r1 == 1 && r5 == 0 + +然而,发布-获取链提供的排序仅局限于参与该链的 CPU,并且不适用于 cpu3(), +至少在写方面是如此。因此,以下结果可能出现: + + r0 == 0 && r1 == 1 && r2 == 1 && r3 == 0 && r4 == 0 + +以下结果也可能出现: + + r0 == 0 && r1 == 1 && r2 == 1 && r3 == 0 && r4 == 0 && r5 == 1 + +尽管 cpu0()、cpu1() 和 cpu2() 会按顺序看到各自的读取和写入操作,但是未参与发布-获取链 +的 CPU 并不保证能看到这个顺序。这种分歧源于实现 smp_load_acquire() 和 +smp_store_release() 的弱内存屏障指令,并不要求在所有情况下对先前的写和后续的读进 +行排序。这意味着 cpu3() 可以将 cpu0() 对 u 的写视为发生在 cpu1() 读 v -之后-, +尽管 cpu0() 和 cpu1() 都认为这两个操作以预期的顺序发生。 + +然而,请记住 smp_load_acquire() 并非魔法。它只是有序地读内存。 +它并 -不- 确保会读取任何特定的值。因此,以下结果是可能的: + + r0 == 0 && r1 == 0 && r2 == 0 && r5 == 0 + +请注意,即使在永远不会发生重排序的强一致性系统中也可能发生。 + +重申一下,如果您的代码需要对所有操作进行完全排序,请始终使用通用屏障。 + + +======================== +显式内核屏障 +======================== + +Linux 内核具有多种不同层次的屏障: + + (*) 编译器屏障。 + + (*) CPU 内存屏障。 + + +编译器屏障 +---------------- + +Linux 内核具有显式的编译器屏障函数,阻止编译器将其两侧的内存访问移动到另一侧: + + barrier(); + +这是一个通用屏障 —— barrier() 没有专门的 读-读 或 写-写 变体。然而,READ_ONCE() +和 WRITE_ONCE() 可以被认为是弱形式的 barrier()。 + +barrier() 函数具有以下效果: + + (*) 阻止编译器将 barrier() 前后的指令排序到另一侧。 + 这个属性的一个示例用途是简化中断处理器代码与被中断代码之间的通信。 + + (*) 在循环内,强制编译器在每次通过该循环时读该循环条件中使用的变量。 + +READ_ONCE() 和 WRITE_ONCE() 函数可以防止许多优化,这些优化尽管在单线程代码中完全安全, +但在并发代码中可能是致命的。以下是这些类型优化的一些例子: + + (*) 编译器有权重新排序对同一变量的读和写,在某些情况下,CPU 也有权重新排序对同一变量 + 的读。这意味着以下代码: + + a[0] = x; + a[1] = x; + + 可能导致 a[1] 中的 x 值比 a[0] 更旧。阻止编译器和 CPU 这样做: + (译注:这怎么防止编译器乱序?是所有volatile变量相互保序吗) + + a[0] = READ_ONCE(x); + a[1] = READ_ONCE(x); + + 简而言之,READ_ONCE() 和 WRITE_ONCE() 为多个 CPU 访问单个变量提供了缓存一致性。 + + (*) 编译器可能合并来自同一变量的连续读。这样的合并可能导致编译器将以下代码"优化": + + while (tmp = a) + do_something_with(tmp); + + 成为以下代码,尽管在某种意义上对于单线程代码是合理的,但几乎肯定不是开发者所期望的: + + if (tmp = a) + for (;;) + do_something_with(tmp); + + 使用 READ_ONCE() 阻止编译器这样做: + + while (tmp = READ_ONCE(a)) + do_something_with(tmp); + + (*) 编译器有权重新读变量,例如,编译器可能会优化掉我们之前示例中的变量 'tmp': + + while (tmp = a) + do_something_with(tmp); + + 这可能导致以下代码,在单线程代码中完全安全,但在并发代码中可能是致命的: + + while (a) + do_something_with(a); + + 例如,这种优化版本的代码可能导致在变量 a 在 "while" 语句和调用 + do_something_with() 之间被其他 CPU 修改的情况下,将零传递给 + do_something_with()。 + + 同样,使用 READ_ONCE() 阻止编译器这样做: + + while (tmp = READ_ONCE(a)) + do_something_with(tmp); + + 请注意,如果编译器寄存器不足,它可能会将 tmp 保存到栈上。这种保存和后续恢复的 + 开销就是编译器重新读变量的原因。对于单线程代码来说,这样做是完全安全的,所以 + 您需要告诉编译器在哪些情况下这是不安全的。 + + (*) 如果编译器知道值将是什么,它可以完全省略读。例如,如果编译器可以证明变量 + 'a' 的值总是为零,它可以将此代码优化为: + + while (tmp = a) + do_something_with(tmp); + + 变为: + + do { } while (0); + + 这种转换对于单线程代码来说是有益的,因为它减少了读和分支。问题是编译器在证明中假 + 设当前 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'。) + + (*) 同样,如果编译器知道变量已经具有正在写的值,它可以省略写操作。 + 同样,编译器假设当前 CPU 是唯一一个 + 写到变量的 CPU,这可能导致编译器在共享变量上出错。例如,假设你有 + 以下内容: + + a = 0; + ... 不 写a 的代码 ... + a = 0; + + 编译器看到变量 'a' 的值已经是零,所以它可能会省略第二次写。 + 如果其他 CPU 在此期间已经写 'a',那不执行第二条写就是错误的。 + + 使用 WRITE_ONCE() 防止编译器做这种错误的猜测: + + WRITE_ONCE(a, 0); + ... 不 写a 的代码 ... + WRITE_ONCE(a, 0); + + (*) 编译器可以重新排序内存访问指令。例如,考虑以下进程级代码和中断处理函数之间的交互: + + void process_level(void) + { + msg = get_message(); + flag = true; + } + + void interrupt_handler(void) + { + if (flag) + process_message(msg); + } + + 没有什么能阻止编译器将 process_level() 转换为以下内容,实际上,这对单线程代码可能是有益的: + + void process_level(void) + { + flag = true; + msg = get_message(); + } + + 如果中断发生在这两个语句之间,那么 + interrupt_handler() 可能会收到一个混乱的 msg。使用 WRITE_ONCE() 防止这种情况: + + void process_level(void) + { + WRITE_ONCE(msg, get_message()); + WRITE_ONCE(flag, true); + } + + void interrupt_handler(void) + { + if (READ_ONCE(flag)) + process_message(READ_ONCE(msg)); + } + + 请注意,如果此中断处理程序本身可以被中断,并且被其他也访问 'flag' 和 'msg' 的 + 事物所中断,例如嵌套中断或 NMI,则需要中断处理程序中也使用 READ_ONCE() 和 + WRITE_ONCE()。(请注意,现代 Linux 内核通常不会发生嵌套中断,事实上,如果中 + 断处理程序返回时启用了中断,您将收到 WARN_ONCE()。) + + 编译器可以将 READ_ONCE() 和 WRITE_ONCE() 与 不包含屏障的代码进行乱序。 + + 这种效果也可以使用 barrier() 实现,但 READ_ONCE() 和 WRITE_ONCE() 更具选择性: + 对于 READ_ONCE() 和 WRITE_ONCE(),只控制它自己,而对于 barrier(), + 编译器必须丢弃所有当前缓存在任何寄存器中的值。当然,编译器还必须尊重 READ_ONCE() + 和 WRITE_ONCE() 发生的顺序。 + + (*) 编译器可以生成写操作, + 如下示例: + + if (a) + b = a; + else + b = 42; + + 编译器可以通过如下优化来节省一个分支: + + b = 42; + if (a) + b = a; + + 在单线程代码中,这不仅安全,而且还节省了一个分支。不幸的是,在并发代码中, + 这种优化可能导致其他 CPU 从 'b' 中读到错误的值 42 + 使用 WRITE_ONCE() 防止这种情况: + + if (a) + WRITE_ONCE(b, a); + else + WRITE_ONCE(b, 42); + + 编译器还可以生成读操作。这些通常影响较小,但它们可能导致缓存行弹跳, + 从而降低性能和扩展性。使用 READ_ONCE() 来防止编译器生成这样的读操作。 + + + (*) 防止 "读撕裂" 和 "写撕裂"。单个大型访问被替换为多个较小的访问。例如,假设一架构有 16 位写指令 + 和 7 位立即数的指令,编译器可能会尝试使用两个 16 位写立即指令来实现以下 32 位写: + + p = 0x00010002; + + 请注意,GCC 确实使用这种优化,这并不奇怪,因为构建常量然后写它可能需要超过 + 两个指令。因此,这种优化在单线程代码中可能是有益的。最近的一个错误 (已修复) + 导致 GCC 在 volatile 中错误地使用这种优化。在没有这种错误的 + 情况下,使用 WRITE_ONCE() 可以防止写撕裂: + + WRITE_ONCE(p, 0x00010002); + + 使用 __packed__ 结构体也可能导致撕裂,如下例所示: + + struct __attribute__((__packed__)) foo { + short a; + int b; + short c; + }; + struct foo foo1, foo2; + ... + + foo2.a = foo1.a; + foo2.b = foo1.b; + foo2.c = foo1.c; + + 因为没有 READ_ONCE() 或 WRITE_ONCE() 以及没有 volatile 标志,编译器完全有 + 权将这三个赋值语句实现为一对 32 位读,然后是一对 32 位写。这将导致 + 'foo1.b' 上的读撕裂和 'foo2.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 可以根据需要对其进行重新排序。如果要对 CPU +施加屏障,还需要为代码添加 CPU 屏障。 + + +CPU 内存屏障 +------------------- + +Linux 内核有七个基本的 CPU 内存屏障: + + 类型 强制屏障 SMP屏障 + ======================= =============== =============== + 通用 mb() smp_mb() + 写入 wmb() smp_wmb() + 读取 rmb() smp_rmb() + 地址依赖 READ_ONCE() + + +除地址依赖屏障外,所有内存屏障都隐含编译器屏障。地址依赖不会对编译器排序施加任何额外的限制。 + +另外:在地址依赖的情况下,编译器会预期以正确的顺序发出读指令 (例如,`a[b]`需要在读 +a[b] 之前读 b 的值) ,然而 C 语言规范并没有保证编译器不会推测 b 的值 (例如,等于 1) +并在 b 之前读 a[b] (例如,tmp = a[1]; if (b != 1) tmp = a[b]; ) 。 +此外,编译器在读 a[b] 之后重新读 b,这样 b 比 a[b] 更新,也是有问题的。 +这些问题尚未达成共识,但 READ_ONCE() 宏是一个很好的起点。 + +在单处理器的系统上,SMP 内存屏障会降级为编译器屏障,因为假设 CPU 对自身看起来是自洽的, +并且会正确排序自身的重叠访问。然而,请参阅下面关于"虚拟机客户机"的小节。 + +[!] 请注意,SMP屏障仅用于控制对 SMP 系统上共享内存的访存顺序,尽管使用锁定也是足够的。 + +强制屏障可以控制IO设备视角下的整个CPU的内存顺序。而SMP屏障仅控制CPU内多个核心之间的内存顺序。 +不应该用强制屏障控制仅在 CPU 范围内所需的顺序,这会带来不必要的开销。 + + +还有一些更高级的屏障函数: + + (*) smp_store_mb(var, value) + + 这将为变量赋值,然后在其后插入一个完整的内存屏障。在 UP 编译中,不保证插入比编译器 + 更严格的屏障。 + + (*) smp_mb__before_atomic(); + (*) smp_mb__after_atomic(); + + 这些是用于原子 RMW 函数的,这些函数不隐含内存屏障,但代码需要内存屏障。 + 例如,不隐含内存屏障的原子 RMW 函数包括加法、减法、 (失败的) 条件操作、_relaxed + 函数,但不包括 atomic_read 或 atomic_set。 + 当原子操作用于引用计数时,可能需要内存屏障。 + + 这些还用于不隐含内存屏障的原子 RMW 位操作函数 (如 set_bit 和 clear_bit) 。 + + 例如,考虑一段代码,它将一个对象标记为死亡,然后减少该对象的引用计数: + + obj->dead = 1; + smp_mb__before_atomic(); + atomic_dec(&obj->ref_count); + + 这确保了在引用计数器递减之前,对象上的死亡标记一定被设置。 + + 有关更多信息,请参阅 Documentation/atomic_{t,bitops}.txt。 + + + (*) dma_wmb(); + (*) dma_rmb(); + (*) dma_mb(); + + 确保 CPU 和 DMA 设备间的共享内存的读写顺序。 + 更多信息请参阅 Documentation/core-api/dma-api.rst 文件。 + + 例如,一个设备驱动程序与设备共享内存,并使用描述符状态值来指示描述符属于 + 设备还是 CPU,以及一个门铃来通知它何时有新的描述符可用: + + if (desc->status != DEVICE_OWN) { + /* 在拥有描述符之前不读取数据 */ + dma_rmb(); + + /* 读取/修改数据 */ + read_data = desc->data; + desc->data = write_data; + + /* 在状态更新之前刷新修改 */ + dma_wmb(); + + /* 分配所有权 */ + desc->status = DEVICE_OWN; + + /* 使描述符状态对设备可见,然后 + * 通知设备有新的描述符 + */ + writel(DESC_NOTIFY, doorbell); + } + + dma_rmb() 确保在从描述符中读取数据之前,设备已经释放了所有权,而 + dma_wmb() 确保在设备看到自己被赋予所有权之前,CPU已经将数据写入描述符。 + dma_mb() 隐含了 dma_rmb() 和 dma_wmb()。 + + 请注意,dma_*() 屏障不为 MMIO 区域的访问提供任何排序保证。有关 I/O 访问器和 + MMIO 排序的更多信息,请参阅后面的"内核 I/O 屏障效果"小节。 + +(*) pmem_wmb(); + + 该函数用于持久性内存,以确保已将修改写入持久性写的写操作已到达平台耐久性领域。 + + 例如,在对 pmem 区域进行非临时性写操作后,我们使用 pmem_wmb() 确保写操作完成。 + 这可以让持久存储在后续读执之前就更新。这是在 wmb() 不保证的。 + + 对于从持久性内存读,现有的读内存屏障足以确保读取顺序。 + + (*) io_stop_wc(); + + 对于具有写入组合属性的内存访问(例如,由 ioremap_wc() 返回的那些),CPU 可能 + 会等待先前的访问与后续的访问合并。当此类等待对性能有影响时,可以使用 io_stop_wc() + 防止在此宏之前的写入组合内存访问与之后的访问合并。 + +=============================== +隐式内核内存屏障 +=============================== + +Linux 内核中的其他一些函数隐含了内存屏障,其中包括锁和调度函数。 + +此规范是一个 _最低_ 保证;任何特定架构可能提供更实质性的保证,但在通用代码中应当按照最 +坏情况来考虑。 + + +取锁函数 +-------------------------- + +Linux 内核有多种锁: + + (*) 自旋锁 + (*) 读/写自旋锁 + (*) 互斥锁 + (*) 信号量 + (*) 读/写信号量 + +在所有情况下,都有针对每种操作的 "ACQUIRE" (获取) 操作和 "RELEASE" (释放) 操作 +的变体。这些操作都暗示着特定屏障: + + (1) ACQUIRE 操作的含义: + + 在 ACQUIRE 之后的指令都会排在 ACQUIRE 之后。 + + 在 ACQUIRE 之前的指令不受限制。 + + (2) RELEASE 操作的含义: + + 在 RELEASE 之前的操作都排在 RELEASE 之前。 + + 在 RELEASE 之后的指令不受限制。 + + (3) ACQUIRE 与 ACQUIRE: + + 在另一个 ACQUIRE 操作之前发出的所有 ACQUIRE 操作将在该 ACQUIRE 操作之前完成。 + + (4) ACQUIRE 与 RELEASE: + + 在 RELEASE 操作之前发出的所有 ACQUIRE 操作将在 RELEASE 操作之前完成。 + + (5) 可失败 ACQUIRE: + + ACQUIRE 操作的某些锁定变体可能会失败,原因可能是无法立即获取锁,或者在等待 + 锁可用时收到唤醒信号。失败的锁不包含任何类型的屏障。 + +[!] 注意:锁的 ACQUIRE 和 RELEASE 只是单向屏障的一个后果是,临界区外的指令可能会渗入临界区内部。 + +不能假定 ACQUIRE 后跟 RELEASE 是完全内存屏障,因为在 ACQUIRE 之前发生的访问可能在 +ACQUIRE 之后发生,而在 RELEASE 之后发生的访问可能在 RELEASE 之前发生, +这两者之间可以相互交叉: + + *A = a; + ACQUIRE M + RELEASE M + *B = b; + +可能会发生以下情况: + + ACQUIRE M, STORE *B, STORE *A, RELEASE M + +当 ACQUIRE 和 RELEASE 分别是锁获取和释放时,如果锁的 ACQUIRE 和 RELEASE 都针对同 +一个锁变量,则只能从未持有该锁的另一个 CPU 的角度来看,可以发生这种重新排序。 +简而言之,ACQUIRE 后跟一个 RELEASE 不等于通用屏障。 + +同样,RELEASE 后跟 ACQUIRE 也不是通用内存屏障。RELEASE 和 ACQUIRE 可以交叉: + + *A = a; + RELEASE M + ACQUIRE N + *B = b; + +可能会发生以下情况: + + ACQUIRE N, STORE *B, STORE *A, RELEASE M + +这种重新排序可能导致死锁。然而,这是不可能发生的,因为如果存在这种死锁威胁,RELEASE +将简单地完成,从而避免死锁。 + + 为什么会这样? + + 我们只讨论 CPU 进行重新排序,而不是编译器。如果编译器 (或者开发者) + 交换了操作,死锁确实可能发生。 + + 假设 CPU 对操作进行了重新排序。在这种情况下,在汇编代码中解锁操作在锁定 + 操作之前。CPU 只是选择先尝试执行后面的锁操作。如果存在死锁,这个锁操作将 + 简单地旋转 (或尝试睡眠,但稍后会有更多解释) 。最终,CPU 将执行解锁操作 + (在汇编代码中位于锁操作之前) ,从而解开潜在的死锁,使锁操作成功。 + + 但如果锁是一个 sleeplock 怎么办?在这种情况下,代码将尝试进入调度程序, + 最终遇到内存屏障,强制早期的解锁操作完成,再次解开死锁。可能存在睡眠-解锁竞争, + 但无论如何,锁定原语都需要正确解决这种竞争。 + +在 UP 编译的系统上,锁和信号量可能无法提供任何排序保证,因此在这种情况下不能依赖 +它们来实际实现任何事情 - 特别是关于 I/O 访问 - 除非与中断禁用操作结合使用。 + +参见 "跨 CPU ACQUIRE 屏障" 一节。 + + +以以下例子为例: + + *A = a; + *B = b; + ACQUIRE + *C = c; + *D = d; + RELEASE + *E = e; + *F = f; + +以下事件序列是可以接受的: + + ACQUIRE, {*F,*A}, *E, {*C,*D}, *B, RELEASE + + [+] 注意 {*F,*A} 表示一个组合访问。 + +但以下都不是: + + {*F,*A}, *B, ACQUIRE, *C, *D, RELEASE, *E + *A, *B, *C, ACQUIRE, *D, RELEASE, *E, *F + *A, *B, ACQUIRE, *C, RELEASE, *D, *E, *F + *B, ACQUIRE, *C, *D, RELEASE, {*F,*A}, *E + + + +关中断函数 +------------------------- + +禁用中断 (相当于 ACQUIRE) 和启用中断 (相当于 RELEASE) 的函数将仅充当编译器屏障。 +因此,如果在这种情况下需要内存或 I/O 屏障,需要额外添加。 + + +睡眠和唤醒函数 +------------------------- + +等待事件完成的睡眠,可以看作是进程状态和事件状态间的交互。为了他们按正确的 +顺序更改,睡眠和唤醒函数都包含了某些屏障。 + +首先,睡眠者通常遵循类似于以下的事件序列: + + for (;;) { + set_current_state(TASK_UNINTERRUPTIBLE); + if (event_indicated) + break; + schedule(); + } + +通用内存屏障会自动在 set_current_state() 修改任务状态后插入: + + CPU 1 + =============================== + set_current_state(); + smp_store_mb(); + STORE current->state + <通用屏障> + LOAD event_indicated + +set_current_state() 可能被以下函数调用: + + prepare_to_wait(); + prepare_to_wait_exclusive(); + +因此,设置状态也包含了通用内存屏障。上面的整个序列以各种形式提供,所有这些形式都在 +正确的位置插入内存屏障: + + wait_event(); + wait_event_interruptible(); + wait_event_interruptible_exclusive(); + wait_event_interruptible_timeout(); + wait_event_killable(); + wait_event_timeout(); + wait_on_bit(); + wait_on_bit_lock(); + + +其次,执行唤醒操作的代码通常遵循类似于以下的内容: + + event_indicated = 1; + wake_up(&event_wait_queue); + +或: + + 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 ... + <通用屏障> <通用屏障> + LOAD event_indicated if ((LOAD task->state) & TASK_NORMAL) + STORE task->state + +其中 "task" 是被唤醒的线程,它等于 CPU 1 的 "current"。 + +重复一遍,如果实际唤醒了目标任务,wake_up() 保证执行通用内存屏障,如果不需要唤醒目标任 +务则没有屏障。请考虑以下事件序列,其中 X 和 Y 最初都是零: + + CPU 1 CPU 2 + =============================== =============================== + X = 1; Y = 1; + smp_mb(); wake_up(); + LOAD Y LOAD X + +如果唤醒确实发生,两个读中的至少一个必须看到 1。另一方面,如果唤醒没有发生,两个加 +载都可能看到 0。 + +wake_up_process() 总是执行通用内存屏障。屏障发生在访问任务状态之前。也就是说,如 +果在前面的片段中将 wake_up() 替换为对 wake_up_process() 的调用,那么两个读中的一个将保证看到 1。 + +可用的唤醒函数包括: + + complete(); + wake_up(); + wake_up_all(); + wake_up_bit(); + wake_up_interruptible(); + wake_up_interruptible_all(); + wake_up_interruptible_nr(); + wake_up_interruptible_poll(); + wake_up_interruptible_sync(); + wake_up_interruptible_sync_poll(); + wake_up_locked(); + wake_up_locked_poll(); + wake_up_nr(); + wake_up_poll(); + wake_up_process(); + +在内存排序方面,这些函数都提供了 wake_up() (或更强) 的相同保证。 + +[!] 请注意,睡眠和唤醒函数的内存屏障 _不会_ 在唤醒之前按顺序对多个写进行排序, +也不会在睡眠者调用 set_current_state() 之后对读进行排序。例如,如果睡眠者执行以下操作: + + set_current_state(TASK_INTERRUPTIBLE); + if (event_indicated) + break; + __set_current_state(TASK_RUNNING); + do_something(my_data); + +如果唤醒者执行: + + my_data = value; + event_indicated = 1; + wake_up(&event_wait_queue); + +不能保证睡眠者会将 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); + + +杂项函数 +----------------------- + +其他具有障碍意义的函数: + + (*) schedule() 和类似的函数包含通用内存屏障。 + + +=================================== +跨CPU ACQUIRING 屏障的影响 +=================================== + +在SMP系统中,lock功能可以通过争抢锁来切实影响其他CPU上的内存访问顺序。 + + +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上单独的锁对CPU 3施加的约束之外,没有保证CPU 3将看到*A至*H的访问顺序。例如,它可能看到: + + *E, ACQUIRE M, ACQUIRE Q, *G, *C, *F, *A, *B, RELEASE Q, *D, *H, RELEASE M + +但它不会看到: + + *B, *C 或 *D 在 ACQUIRE M 之前 + *A, *B 或 *C 在 RELEASE M 之后 + *F, *G 或 *H 在 ACQUIRE Q 之前 + *E, *F 或 *G 在 RELEASE Q 之后 + + +================================= +哪里需要内存屏障? +================================= + +在正常操作下,内存操作重排序通常不会成为问题,因为即使是在SMP内核中,单线程线性代码仍 +然会正常工作。在以下四种情况下,需要考虑使用内存屏障: + + (*) 处理器间交互。 + + (*) 原子操作。 + + (*) 访问设备。 + + (*) 中断。 + + +处理器间交互 +-------------------------- + +当一个系统有多个处理器时,系统中的多个CPU可能会同时处理同一组数据。这可能导致同步问题, +通常处理这些问题的方法是使用锁。然而,锁代价相当高昂,因此如果可能的话,最好在不使用 +锁的情况下进行操作。这就需要用屏障对两个CPU上的代码进行排序。 + +以 R/W 信号量的慢速路径为例。在这里,一个等待的进程被排队在信号量上,它的堆栈上有 +一部分与信号量的等待进程列表相连接: + + struct rw_semaphore { + ... + spinlock_t lock; + struct list_head waiters; + }; + + struct rwsem_waiter { + struct list_head list; + struct task_struct *task; + }; + +要唤醒特定的等待者,up_read() 或 up_write() 函数必须: + + (1) 从该等待者的记录中读取下一个指针,以了解下一个等待者记录在哪里; + + (2) 读取指向等待者任务结构的指针; + + (3) 清除任务指针,告诉等待者它已经获取了信号量; + + (4) 在任务上调用 wake_up_process();并且 + + (5) 释放对等待者任务结构的引用。 + +换句话说,它必须执行以下事件序列: + + LOAD waiter->list.next; + LOAD waiter->task; + STORE waiter->task; + CALL wakeup + RELEASE task + +如果这些步骤按照错误的顺序发生,那么整个事情可能会出错。 + +一旦将自己排队并释放信号量,等待者不再获取锁;相反,它只是等待任务指针被清除后再继续。 +由于记录在等待者的堆栈上,这意味着如果在读取列表中的下一个指针之前清除了任务指针,另 +一个CPU可能会开始处理等待者,可能会在up*()函数有机会读取下一个指针之前损坏等待者的堆栈。 + +那么,考虑一下上述事件序列可能发生的情况: + + CPU 1 CPU 2 + =============================== =============================== + down_xxx() + 排队 + 睡眠 + up_yyy() + LOAD waiter->task; + STORE waiter->task; + 由其他事件唤醒 + <preempt> + 恢复处理 + down_xxx() 返回 + 调用 foo() + foo() 破坏了 *waiter + </preempt> + LOAD waiter->list.next; + --- 错误 --- + +可以使用信号量锁来处理这个问题,但是这样的话,在被唤醒后 down_xxx() 函数就不必再次获得自旋锁了。 + +处理这个问题的方法是插入一个通用的SMP内存屏障: + + LOAD waiter->list.next; + LOAD waiter->task; + smp_mb(); + STORE waiter->task; + CALL wakeup + RELEASE task + +在这种情况下,屏障保证了在屏障之前的所有内存访问相对于系统上的其他CPU看起来都是在屏障之 +后的所有内存访问之前发生的。它并 _不_ 保证在屏障指令本身完成时,屏障之前的所有内存访问 +都将完成。 + +在一个UP系统中 - 在这里这不会成为问题 - smp_mb() 只是一个编译器屏障,因此确保编译器 +以正确的顺序发出指令,而无需实际干预CPU。由于只有一个CPU,那个CPU的依赖排序逻辑将处理 +其他所有内容。 + +原子操作 +----------------- + +它们中有的包含通用内存屏障,有的没有,内核大量依赖原子操作。 + +有关更多信息,请参阅 Documentation/atomic_t.txt。 + + +访问设备 +----------------- + +许多设备可以进行内存映射,因此对 CPU 来说,访问它们就像访问内存一样。要控制这样的设备, +驱动程序通常必须以完全正确的顺序进行内存访问。 + +然而,拥有聪明的 CPU 或聪明的编译器会产生一个潜在的问题,即驱动代码中仔细安排的访问 +顺序如果没有按照所需的顺序到达设备,CPU 或编译器认为重新排序、组合或合并访问更有效的 +话,就会导致设备故障。 + +在 Linux 内核中,I/O 应该通过适当的访问例程 (如 inb() 或 writel()) 完成,这些例 +程知道如何使此类访问适当地顺序进行。在大多数情况下,这使得显式使用内存屏障变得不必要, +但是,如果访问函数用于引用具有宽松内存访问属性的 I/O 内存窗口,则需要 _强制性_ 内存 +屏障来强制排序。 + +有关更多信息,请参阅 Documentation/driver-api/device-io.rst。 + + +中断 +---------- + +驱动程序可能会被其自身的中断服务例程中断,因此驱动程序的两个部分可能会相互干扰对设 +备的控制或访问。 + +至少在某种程度上,可以通过禁用本地中断 (一种锁定形式) 来缓解这种情况,这样关键操作 +都包含在驱动程序中的禁用中断部分内。在驱动程序的中断例程执行期间,驱动程序的核心可 +能无法在同一 CPU 上运行,而且在处理当前中断之前,不允许再次发生中断,因此中断处理程 +序不需要对其进行锁定。 + +然而,考虑一个与以太网卡通信的驱动程序,该以太网卡具有地址寄存器和数据寄存器。如果 +驱动程序的核心在禁用中断的情况下与卡进行通信,然后调用驱动程序的中断处理程序: + + LOCAL IRQ DISABLE + writew(ADDR, 3); + writew(DATA, y); + LOCAL IRQ ENABLE + <interrupt> + writew(ADDR, 4); + q = readw(DATA); + </interrupt> + +如果排序规则足够宽松,那么在第二次写入地址寄存器之后,可能会发生对数据寄存器的写: + + STORE *ADDR = 3, STORE *ADDR = 4, STORE *DATA = y, q = LOAD *DATA + +如果排序规则宽松,必须假定在禁用中断的部分内完成的访问可能会泄漏到其外部,并且可能与在 +中断中执行的访问交错,反之亦然,除非使用隐式或显式屏障。 + +通常这不会是问题,因为在这些部分内完成的 I/O 访问将包括对严格排序的 I/O 寄存器的同步 +读操作,这些操作形成了隐式的 I/O 屏障。 + +类似的情况可能发生在中断例程和在单独的 CPU 上运行的两个相互通信的例程之间。如果可能出 +现这种情况,那么应使用禁用中断的锁来保证排序。 + + +========================== +内核 I/O 屏障 +========================== + +通过 I/O 与外围设备进行交互,在不同架构和设备间有很大的差异。因此,不打算移植的驱动程序 +可能依赖于其目标系统的特定行为,以尽可能轻量地实现同步。对于打算在多个架构和总线实现之间 +移植的驱动程序,内核提供了一系列函数,提供不同程度的排序保证: + + (*) readX(), writeX(): + + readX() 和 writeX() MMIO 访问器接受一个指向被访问外设的 __iomem * 参数。 + 对于具有默认 I/O 属性的映射指针 (例如,由 ioremap() 返回的指针) ,排序保证如下: + + 1. 所有针对同一外设的 readX() 和 writeX() 访问彼此有序。这确保了同一 CPU 线 + 程对特定设备的 MMIO 寄存器访问将按程序顺序到达。 + + 2. 一个持有自旋锁的 CPU 线程发出的 writeX() 将在另一个 CPU 线程在后续获取相 + 同自旋锁后发出的对同一外设的 writeX() 之前排序。这确保了在持有自旋锁时发出 + 的对特定设备的 MMIO 寄存器写入将按照获取锁的顺序到达。 + + 3. 一个 CPU 线程对外设的 writeX() 将首先等待由同一线程发出或传播给同一线程的 + 所有先前对内存的写入完成。这确保了 CPU 对由 dma_alloc_coherent() 分配的 + 出站 DMA 缓冲区的写入在 CPU 写入其 MMIO 控制寄存器以触发传输时对 DMA 引擎 + 可见。 + + 4. 一个 CPU 线程从外设读取的 readX() 将在相同线程的任何后续内存读取开始之前完 + 成。这确保了 CPU 从 DMA 引擎的 MMIO 状态寄存器读取以确定 DMA 传输已完成后 + 从由 dma_alloc_coherent() 分配的传入 DMA 缓冲区不会看到过时的数据。 + + 5. 一个 CPU 线程从外设读取的 readX() 将在相同线程上的任何后续 delay() 循环 + 开始执行之前完成。这确保了 CPU 对外设的两个 MMIO 寄存器写入至少相隔 1us, + 如果第一个写入立即使用 readX() 读回并在第二个 writeX() 之前调用 udelay(1): + + writel(42, DEVICE_REGISTER_0); // 到达设备 ... + readl(DEVICE_REGISTER_0); + udelay(1); + writel(42, DEVICE_REGISTER_1); // ...至少等待 1us。 + + 具有非默认属性的 __iomem 指针的排序属性 (例如,由 ioremap_wc() 返回的指针) + 特定于底层架构,因此上述保证通常不能依赖于访问这些类型的映射。 + + (*) readX_relaxed(), writeX_relaxed(): + + 这些类似于 readX() 和 writeX(),但提供较弱的内存排序保证。具体来说,它们不 + 保证与锁定、正常内存访问或 delay() 循环 (即上述第 2-5 点) 有序,但仍保证在 + 使用具有默认 I/O 属性的映射的 __iomem 指针时,与来自相同 CPU 线程的其他访 + 问有序。 + + (*) readsX(), writesX(): + + readsX() 和 writesX() MMIO 访问器设计用于访问位于不支持 DMA 的外设上的 + 基于寄存器的内存映射 FIFO。因此,它们只提供 readX_relaxed() 和 + writeX_relaxed() 的排序保证,如上文所述。 + + (*) inX(), outX(): + + inX() 和 outX() 访问器旨在访问传统的端口映射 I/O 外设,在某些架构上可能 + 需要特殊指令 (尤其是 x86) 。所访问外设的端口号作为参数传递。 + + 由于许多 CPU 架构最终通过内部虚拟内存映射访问这些外设,因此 inX() 和 outX() + 提供的可移植排序保证与分别访问具有默认 I/O 属性的映射时 readX() 和 writeX() + 提供的保证相同。 + + 设备驱动程序可能期望 outX() 发出一个非传递写事务,在返回之前等待来自 I/O 外 + 设的完成响应。这并不是所有架构都能保证的,因此不属于可移植排序语义的一部分。 + + (*) insX(), outsX(): + + 如上所述,分别访问具有默认 I/O 属性的映射时,insX() 和 outsX() 访问器提供与 + readsX() 和 writesX() 相同的排序保证。 + + (*) ioreadX(), iowriteX(): + + 这些将根据它们实际执行的访问类型执行适当的操作,无论是 inX()/outX() 还是 + readX()/writeX()。 + +除了字符串访问器 (insX()、outsX()、readsX() 和 writesX()) 之外,上述所有内容都 +假定外设为小端模式,并因此在大端架构上执行字节交换操作。 + + +======================================== +假定的最小执行顺序模型 +======================================== + +CPU必须确保本核视角下本核的指令是按顺序执行的,本核的乱序是一个黑箱。 +不同架构的内存一致性模型不同,因此我们要选择一致性最差的CPU作为模型,即 DEC Alpha + +这意味着必须假设 CPU 将以任何顺序执行指令,甚至可能是并行执行。如果指令流中的一个指令 +依赖于较早的指令,则被依赖的较早指令满足该指令的条件[*]后才能执行这条指令。 +换句话说:要保持指令间的因果关系。 + + [*] 一些指令具有多种效果 - 例如改变条件代码、改变寄存器或改变内存 - 不同的指令可能依 + 赖于不同的效果。 + +CPU 可能丢弃没有效果的指令。例如,如果两个相邻的指令都将立即数读到同一个寄存器中, +那第一个指令可能会被丢弃。 + + +同样,编译器也会在保持因果关系的情况下以任何方式重新排序指令。 + + +============================ +CPU 缓存的影响 +============================ + +被缓存的内存的操作在系统中的感知方式在一定程度上受到位于 CPU 和内存之间的缓存以及维护系统状 +态一致性的内存一致性系统的影响。 + +就 CPU 通过缓存与系统其他部分交互的方式而言,内存系统必须包括 CPU 的缓存,而内存屏 +障在很大程度上作用在 CPU 和其缓存之间的接口上 (在下图中,内存屏障在虚线上起作用) : + + <--- CPU ---> : <----------- Memory -----------> + : + +--------+ +--------+ : +--------+ +-----------+ + | | | | : | | | | +--------+ + | CPU | | Memory | : | CPU | | | | | + | Core |--->| Access |----->| Cache |<-->| | | | + | | | Queue | : | | | |--->| Memory | + | | | | : | | | | | | + +--------+ +--------+ : +--------+ | | | | + : | Cache | +--------+ + : | Coherency | + : | Mechanism | +--------+ + +--------+ +--------+ : +--------+ | | | | + | | | | : | | | | | | + | CPU | | Memory | : | CPU | | |--->| Device | + | Core |--->| Access |----->| Cache |<-->| | | | + | | | Queue | : | | | | | | + | | | | : | | | | +--------+ + +--------+ +--------+ : +--------+ +-----------+ + : + : + +有的读或写操作可能实际上没有在发出它的 CPU 外部出现,因为它可能已经在 CPU 自己 +的缓存中得到满足,但就其他 CPU 而言,它仍然会表现得好像已经发生了完整的内存访问,因为缓存 +一致性机制将在缓存间传播内存操作。 + +CPU 核心可以以它认为合适的任何顺序执行指令,前提是保持预期的程序因果关系。一些指令生成读 +和写操作,然后进入要执行的内存访问队列。核心可以按照它希望的任何顺序将这些放入队列中,并继 +续执行,直到它被迫等待指令完成。 + +内存屏障关心的是控制访问从 CPU 端到内存端的顺序,以及系统中其他观察者感知到的效果发生的顺序。 + +[!] 在给定的 CPU 内部,不需要内存屏障,因为 CPU 总是会看到它们自己的读和写操作,就好像 +它们是按照程序顺序发生的一样。 + +[!] MMIO 或其他设备访问可能会绕过缓存系统。这取决于通过哪个内存窗口访问设备的属性和/或 +CPU 可能具有的任何特殊设备通信指令的使用。 + + +CACHE 一致性 VS DMA +---------------------- + +并不是所有架构都维护DMA的cache一致性。如果脏cache还没有刷到内存中,那DMA设备访问 +内存时可能得到旧数据。为了解决这个问题,内核必须在向设备传递数据前将相应cacheline刷到内 +存中。 + +另外, 设备通过DMA写到内存,内存中的新数据可能会被写回的脏cache覆盖。也可能会因为cache没 +有更新而被CPU忽略,直到cache被丢弃并重新读。为了解决这个问题,内核必须在设备向内存写完 +数据之后将对应的cacheline无效化以重新从内存中读新数据。 + +Documentation/core-api/cachetlb.rst 中有更多关于cache管理的信息。 + + +CACHE 一致性 VS MMIO +----------------------- + +内存映射 I/O 是通过内存地址发起的 IO,这些内存地址是 CPU 内存空间的一部分, +其属性与通常的 RAM 不同。 + +在这些属性之中,尤其要注意访问会完全绕过缓存,直接与设备总线通信。这意味着,MMIO访问可能 +会超越之前发出的已经存在于缓存中的内存访问。在这种情况下,仅使用内存屏障是不够的, +如果两个操作有依赖关系,则必须在写缓存和MMIO访问之间刷新缓存。 + + +========================= +CPU 做的事情 +========================= + +程序员通常确信CPU一定会按照自己编写的顺序来执行内存操作。 +看下面的代码: + + a = READ_ONCE(*A); + WRITE_ONCE(*B, b); + c = READ_ONCE(*C); + d = READ_ONCE(*D); + WRITE_ONCE(*E, e); + +他们会希望CPU按照指令顺序来执行内存操作,让系统外的观察者能看到明确的操作顺序。 + + LOAD *A, STORE *B, LOAD *C, LOAD *D, STORE *E. + + +然而,现实是非常混乱的,编译器通常不会按照代码顺序生成汇编,CPU也不按汇编语句顺序执行 + + (*) 读需要立即完成以使指令继续执行,而写操作通常可以推迟; + + (*) 读可以预测,如果证明是不必要的,结果会被丢弃; + + (*) 读可能是投机性的,导致结果在预期事件序列中的错误时间被取走。 + + (*) 内存访问是乱序的,以便更好地利用CPU总线和缓存。 + + (*) 当与内存或I/O硬件交互时,如果硬件可以对相邻位置进行批量访问, + 那多个读或多个写可以合并起来执行以提高性能, + +(*) CPU 的数据缓存可能会影响顺序,虽然缓存一致性机制可能会缓解这个问题。 + 但不能保证一致性机制一定会按顺序传播内存操作。 + +因此,另一个CPU可能观察到的是: + + LOAD *A, ..., LOAD {*C,*D}, STORE *E, STORE *B + + ("LOAD {*C,*D}" 是一个合并的操作) + + +但是,CPU 看到的自己访问是正确排序的,不需要内存屏障。 例如使用以下代码: + + U = READ_ONCE(*A); + WRITE_ONCE(*A, V); + WRITE_ONCE(*A, W); + X = READ_ONCE(*A); + WRITE_ONCE(*A, Y); + Z = READ_ONCE(*A); + +假设没有外部干预,我们可以假设最终结果将呈现为: + + U == *A 的原始值 + X == W + Z == Y + *A == Y + +上述代码可能会导致 CPU 生成完整的内存访问序列: + + U=LOAD *A, STORE *A=V, STORE *A=W, X=LOAD *A, STORE *A=Y, Z=LOAD *A + +该序列可能会被任意乱序或组合。请注意,在上面的示例中,READ_ONCE() 和 +WRITE_ONCE() 是 _必须_ 的,因为有些架构可能会对相同位置的 +连续读进行重排序。在这样的架构上,READ_ONCE() 和 WRITE_ONCE() 做任何必要 +的操作以防止这种情况,例如,在 Itanium 上,READ_ONCE() 和 WRITE_ONCE() 使 +用的 volatile 类型转换导致 GCC 发出特殊的 ld.acq 和 st.rel 指令,以 +防止这种重排序。 + +编译器也可以合并,丢弃或延迟一些指令: + +比如: + + *A = V; + *A = W; + +或许会被简化为: + + *A = W; + +如果没有写屏障或 WRITE_ONCE(),会仅保留最后一次写。类似的: + + *A = Y; + Z = *A; + +如果不使用内存屏障或READ_ONCE() 和 WRITE_ONCE(),代码会被简化为: + + *A = Y; + Z = Y; + +那么CPU将不会向外发出读 *A 的指令。 + + +接下来是 Alpha +-------------------------- + +DEC Alpha CPU 是内存一致性最差的 CPU 之一。不仅如此,某些版本的 Alpha CPU 具有分割 +的数据cache,这两个 cache 间没有地址依赖机制,是唯一需要使用地址依赖屏障的地方。 + +Linux 将 Alpha 作为内存模型,尽管从 v4.15 开始,Linux 内核在 Alpha 架构上将 +smp_mb() 添加到 READ_ONCE(),大大减少了其对内存模型的影响。 + + +虚拟机客户机 +---------------------- + +即使客户机本身编译为不支持 SMP,也可能会受到宿主机 SMP 效应的影响,从而在与宿主机交互 +时发生错误。在UP客户机与宿主机交互时,可以用强制屏障,但并不是最优解。 + +内核提供了低级别的 virt_mb() 等宏。即使客户机编译为 UP,它们也会生成与编译为 SMP 时的 +smp_mb() 等效的代码。客户机应在与 (可能是 SMP 的) 主机同步时使用 virt_mb() 而不是 +smp_mb()。 + +在所有其他方面,它们等同于 smp_mb() 等,特别是,它们不控制 MMIO 效果:要控制 MMIO 效果, +请使用强制屏障。 + + +============ +使用案例 +============ + +环形缓冲区 +---------------- + +内存屏障可用于实现环形缓冲,而无需锁来将生产者与使用者序列化。具体见: + + Documentation/core-api/circular-buffers.rst + + +========== +引用 +========== + +Alpha AXP 架构参考手册,第二版 (Sites & Witek,Digital Press) + 第 5.2 章:物理地址空间特性 + 第 5.4 章: cache 和写缓冲区 + 第 5.5 章:数据共享 + 第 5.6 章:读/写排序 + +AMD64 架构程序员手册第 2 卷:系统编程 + 第 7.1 章:内存访问排序 + 第 7.4 章:buffer 和组合内存写入 + +ARM 架构参考手册 (ARMv8,用于 ARMv8-A 架构) + 第 B2 章:AArch64 应用级内存模型 + +IA-32 英特尔架构软件开发者手册,第 3 卷:系统编程指南 + 第 7.1 章:锁原子操作 + 第 7.2 章:内存排序 + 第 7.4 章:序列化指令 + +SPARC 架构手册,第 9 版 + 第 8 章:内存模型 + 附录 D:内存模型的正式规范 + 附录 J:使用内存模型进行编程 + +PowerPC 中的存储 (Stone 和 Fitzgerald) + +UltraSPARC 程序员参考手册 + 第 5 章:内存访问和可缓存性 + 第 15 章:Sparc-V9 内存模型 + +UltraSPARC III Cu 用户手册 + 第 9 章:内存模型 + +UltraSPARC IIIi 处理器用户手册 + 第 8 章:内存模型 + +UltraSPARC 架构 2005 + 第 9 章:内存 + 附录 D:内存模型的正式规范 + +UltraSPARC T1 补充 UltraSPARC 架构 2005 + 第 8 章:内存模型 + 附录 F:cache 和 cache 一致性 + +深入 Solaris,核心内核架构,第 63-68 页: + 第 3.3 章:锁和同步的硬件考虑因素 + +给内核程序员写的 Unix 系统现代架构,对称多处理和缓存: + 第 13 章:其他内存模型 + +英特尔 Itanium 架构软件开发者手册:第 1 卷: + 第 2.6 节:推测 + 第 4.4 节:内存访问 -- 2.20.1