Linux 内核内存序操作完全指南:基于 LKMM ordering 文档的屏障、有序访问与无序访问分类解析
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
导读
本文围绕 Linux 内核内存模型(LKMM)的官方排序文档展开,系统梳理内核为并发编程提供的全部内存序(memory-ordering)操作,将其归纳为"屏障(Barriers)""有序内存访问(Ordered Memory Accesses)""无序访问(Unordered Accesses)"三大类,并逐一给出语义定义、适用场景与代码示例。读完本文,你将能够:准确区分smp_mb()、smp_wmb()、smp_rmb()、barrier()等屏障的强弱差异;正确选用smp_store_release()/smp_load_acquire()、rcu_assign_pointer()/rcu_dereference()等有序访问原语;并深刻理解控制依赖与未标记 C 语言访问的陷阱,从而写出可移植、可验证的无锁并发代码。
本文的骨架内容源自 Documentation/dev-tools/lkmm/docs/ordering.rst,该文档通过kernel-include指令以字面形式(literal include)引入 tools/memory-model/Documentation/ordering.txt;文中补充的源码证据来自 include/asm-generic/barrier.h、tools/memory-model/Documentation/cheatsheet.txt、tools/memory-model/Documentation/control-dependencies.txt 及 tools/memory-model/litmus-tests/ 中的可执行验证用例。
一、总览:LKMM 定义的内存序操作三大类别
按照约束强度从强到弱,ordering.txt 将内核的内存序操作划分为三个顶层类别:
- 屏障(Barriers,又称 fences):屏障将 CPU 的部分或全部"先前操作"与部分或全部"后续操作"排序。
- 有序内存访问(Ordered memory accesses):这类操作根据其子类别,将自身与 CPU 的部分或全部先前访问、或部分或全部后续访问排序。
- 无序访问(Unordered accesses):顾名思义,本身不具备排序属性,除非它们与上述两个类别的操作交互。不过"现实世界总是不完美的",部分所谓的"无序"操作在个别特殊场合仍提供有限排序。
三大类别内部还可进一步细分:
| 顶层类别 | 子类别 |
|---|---|
| 屏障 | 全内存屏障、RMW 排序增强屏障、写内存屏障、读内存屏障、编译器屏障 |
| 有序内存访问 | Release 操作、Acquire 操作、RCU 读侧排序、控制依赖 |
| 无序访问 | 无序标记操作、未标记的 C 语言访问 |
特别提醒(针对驱动开发者):许多排序原语在内核以CONFIG_SMP=n构建时完全不生成任何代码。因此,如果你在编写设备驱动程序,必须在CONFIG_SMP=n内核中也能正确排序对物理设备的访问,此时应使用为此目的提供的排序原语——例如用mb()代替smp_mb()(详见《Linux Device Drivers》一书及 LWN 相关文章)。这一要求在 include/asm-generic/barrier.h 中有直接体现:在!CONFIG_SMP分支中,smp_mb()、smp_rmb()、smp_wmb()一律被降级为barrier()。
二、屏障(Barriers)
2.1 全内存屏障(Full Memory Barriers)
提供全排序语义的内核原语包括三类:
smp_mb()全内存屏障;- 返回值型 RMW 原子操作,且名字不以
_acquire、_release或_relaxed结尾; - RCU 宽限期(grace-period)原语。
(1)smp_mb()的语义:它将该 CPU 的所有先前访问与所有后续访问排序,且这种排序对所有 CPU 可见。换言之,所有 CPU 都会认同该 CPU 的任一先前动作先于其任一后续动作发生。例如:
WRITE_ONCE(x, 1); smp_mb(); // Order store to x before load from y. r1 = READ_ONCE(y);所有 CPU 都会同意"对 x 的存储先于对 y 的读取"。原文特别强调:请为你的内存序原语添加注释——几个月后人们很难记起当初使用它的目的。
(2)提供全排序的 RMW 原子操作:指返回值非 void且名字不以_acquire/_release/_relaxed结尾的 RMW 原子操作,例如atomic_add_return()、atomic_dec_and_test()、cmpxchg()、xchg()。当这些操作提供全排序时,它们把 CPU 的访问划分为三组:
- RMW 原子操作之前执行的全部代码;
- RMW 原子操作本身;
- RMW 原子操作之后执行的全部代码。
所有 CPU 都会同意:任意较高编号分区的操作先于较低编号分区……(原文表述为:任意给定分区中的操作先于编号更高的分区中的操作)。注意,条件型 RMW 操作(如cmpxchg())仅在成功时保证排序。
反例:返回值类型为 void 的 RMW 操作(如atomic_inc()、atomic_dec())不保证任何排序;名字以_relaxed结尾的返回值型 RMW 操作(如atomic_cmpxchg_relaxed()、atomic_xchg_relaxed())同样不保证排序;非 RMW 的返回值型原子操作(如atomic_read())也不提供全排序,它们属于后文"无序访问"一节。名字以_acquire或_release结尾的返回值型 RMW 操作提供有限排序,将在后文详述。
(3)RCU 宽限期原语:包括synchronize_rcu()、synchronize_rcu_expedited()、synchronize_srcu()等。它们提供全排序,但开销比smp_mb()、atomic_xchg()高出若干个数量级,且只能在可睡眠(sleepable)上下文中调用。因此它们通常被用于为 RCU 读侧临界区提供排序(如各自注释头所描述);当然,如果确实需要synchronize_rcu()与读者交互,额外依赖其全内存屏障语义也不会增加成本——只需认真加注释,否则"未来的你会恨现在的你"。
2.2 RMW 排序增强屏障(RMW Ordering Augmentation Barriers)
如 2.1 所述,atomic_inc()、atomic_dec()这类非返回值 RMW 操作不保证任何排序。然而,包括 x86 在内的许多流行 CPU 家族本身就为这些原语提供了全排序。为了在所有架构上获得全排序,一个朴素的方案是在其后追加smp_mb():
WRITE_ONCE(x, 1); atomic_inc(&my_counter); smp_mb(); // Inefficient on x86!!! r1 = READ_ONCE(y);这可行,但在 x86 上徒增开销(x86 的atomic_inc()本身就提供全排序)。smp_mb__after_atomic()正是为此而生:
WRITE_ONCE(x, 1); atomic_inc(&my_counter); smp_mb__after_atomic(); // Order store to x before load from y. r1 = READ_ONCE(y);smp_mb__after_atomic()只在 atomic_inc() 实现不保证全排序的 CPU 上生成代码,因此在 x86 上零额外开销。smp_mb__*()家族还包括:
smp_mb__before_atomic():为无序 RMW 原子操作之前的访问提供全排序;smp_mb__after_atomic():如上所示,为无序 RMW 原子操作之后的访问提供全排序;smp_mb__after_spinlock():为成功获取自旋锁之后的访问提供全排序。注意spin_lock()总是成功,而spin_trylock()可能失败;smp_mb__after_srcu_read_unlock():为srcu_read_unlock()之后的访问提供全排序。
重要实践:不要在smp_mb__*()原语与它所增强排序的那个操作之间放置代码,否则这段中间代码在不同 CPU 架构上的排序行为会不一致。
从实现上看,include/asm-generic/barrier.h 给出了通用后备定义:__smp_mb__before_atomic()与__smp_mb__after_atomic()默认展开为__smp_mb(),架构(如 x86)可覆写为不生成指令的空实现;而在!CONFIG_SMP分支(barrier.h)中两者都降级为barrier()。此外,SMP 版本会包裹kcsan_mb()调用(barrier.h),以便 KCSAN 数据竞争检测工具感知屏障的存在。
2.3 写内存屏障(Write Memory Barrier)
内核的写内存屏障是smp_wmb()。若某 CPU 执行:
WRITE_ONCE(x, 1); smp_wmb(); WRITE_ONCE(y, 1);则任意给定 CPU 都会看到"对 x 的写先于对 y 的写"。不过,你通常更适合使用 release 存储(见下文"Release 操作"一节)。
一个必须警惕的编译器陷阱:smp_wmb()可能无法为未标记的 C 语言存储提供排序,因为基于 profile 的优化(PGO)可能判断被覆盖的值几乎总是等于新值,从而把x = 1与y = 1合理地变换为:
if (x != 1) x = 1; smp_wmb(); // BUG: does not order the reads!!! if (y != 1) y = 1;变换后屏障两侧变成了两个"读-判-写",smp_wmb()将不再对这些读操作排序。因此,若要用smp_wmb()配合未标记写,你必须确保构建内核所用的一切编译器(现在与未来)都不会执行这类变换——这正是官方文档反复强调"使用标记访问(WRITE_ONCE()等)"的根本原因。
2.4 读内存屏障(Read Memory Barrier)
内核的读内存屏障是smp_rmb()。若某 CPU 执行:
r0 = READ_ONCE(y); smp_rmb(); r1 = READ_ONCE(x);则任意给定 CPU 都会看到"对 y 的读先于对 x 的读"。同样,你通常更适合使用 acquire 加载(见下文"Acquire 操作"一节)。
2.5 编译器屏障(Compiler Barrier)
内核的编译器屏障是barrier()。它禁止编译器执行跨过该点移动内存引用的代码移动优化,但不约束硬件内存排序。典型用途是防止编译器把代码搬过一个无限循环:
WRITE_ONCE(x, 1); while (dontstop) barrier(); r1 = READ_ONCE(y);如果没有barrier(),编译器有权将WRITE_ONCE()移到循环之后;当中断处理器终止该循环时,这种代码移动可能造成问题。另一种做法是对dontstop的加载使用READ_ONCE()。
值得注意的是,前文讨论的所有屏障,其实现内部都使用了barrier()或其底层等价物——这一点在 include/asm-generic/barrier.h 中清晰可见:smp_mb()定义为do { kcsan_mb(); __smp_mb(); } while (0),而__smp_mb()的弱定义在非 SMP 下就是barrier(),即使在某些 SMP 架构上,__smp_mb()也常内嵌barrier()以避免编译器重排硬件屏障指令。
三、有序内存访问(Ordered Memory Accesses)
3.1 Release 操作
Release 操作包括smp_store_release()、atomic_set_release()、rcu_assign_pointer()以及名字以_release结尾的返回值型 RMW 操作。这些操作将自身的存储与该 CPU 的全部先前内存访问排序。相比显式屏障,Release 操作通常兼具更好的可读性与性能。
例如,用smp_store_release()替换 2.3 节的smp_wmb()示例可以省去一行:
WRITE_ONCE(x, 1); smp_store_release(&y, 1);更重要的是,smp_store_release()让并发算法的各片段更易"对上号":被 release 存储的变量(此处是y),通常会在算法的其他部分被某个 acquire 操作读取。
性能优势示例:假若上面的例子不是写x而是读x,则smp_wmb()无法保证排序,只能退而使用smp_mb():
r1 = READ_ONCE(x); smp_mb(); WRITE_ONCE(y, 1);但smp_mb()的开销往往远高于smp_store_release()(后者仍能提供所需的 x 对 y 的排序)。在 x86 上,smp_store_release()版本可能只编译为一条普通 load 加一条普通 store;而smp_mb()则编译为一条昂贵的排序指令。
Release 操作的具体成员:
- 存储类:除
smp_store_release()外,还有atomic_set_release()、atomic_long_set_release(); - RCU 的
rcu_assign_pointer():与smp_store_release()等价,但有三点区别:(1) 它接收"被赋值的指针"而非"指向该指针的指针";(2) 它设计上与rcu_dereference()等配合使用,而非smp_load_acquire();(3) 在 sparse 静态检查中会校验被赋值的确实是一个 RCU 保护的指针; - 名字以
_release结尾的返回值型 RMW 操作:如atomic_fetch_add_release()、cmpxchg_release()。注意:release 排序只针对 RMW 操作中的存储部分,不针对其加载部分;且cmpxchg_release()这类条件操作仅在成功时保证排序。
从实现看,include/asm-generic/barrier.h 的通用后备实现__smp_store_release(p, v)为:先执行__smp_mb(),再执行WRITE_ONCE(*p, v),并带有compiletime_assert_atomic_type(*p)编译期类型断言;x86 等强排序架构则会覆写为更轻量的实现。
3.2 Acquire 操作
Acquire 操作包括smp_load_acquire()、atomic_read_acquire()以及名字以_acquire结尾的返回值型 RMW 操作。这些操作将自身的加载与该 CPU 的全部后续内存访问排序,同样通常比显式屏障性能更好、可读性更高。用smp_load_acquire()替换 2.4 节smp_rmb()示例可省去一行:
r0 = smp_load_acquire(&y); r1 = READ_ONCE(x);与smp_store_release()类似,这也有助于读者通过查找"存储到 y 的smp_store_release()"来串联并发算法的不同片段。此外,smp_load_acquire()比smp_rmb()更进一步:它同时排序后续的存储与后续的加载。
Acquire 操作的具体成员:
- 加载类:除
smp_load_acquire()外,还有atomic_read_acquire()、atomic64_read_acquire(); - 名字以
_acquire结尾的返回值型 RMW 操作:如atomic_xchg_acquire()、atomic_cmpxchg_acquire()。注意:acquire 排序只针对 RMW 操作中的加载部分,不针对其存储部分;atomic_cmpxchg_acquire()这类条件操作仅在成功时保证排序。
include/asm-generic/barrier.h 中__smp_load_acquire(p)的通用后备实现为:先用READ_ONCE(*p)读取,再执行__smp_mb(),同样带类型断言。
Acquire/Release 配对示例:对称之美在于,acquire 操作常与 release 操作配对使用。考虑task0()与task1()并发执行的例子:
void task0(void) { WRITE_ONCE(x, 1); smp_store_release(&y, 1); } void task1(void) { r0 = smp_load_acquire(&y); r1 = READ_ONCE(x); }若x、y初始均为零,则要么r0的最终值为零,要么r1的最终值为一——这正是消息传递(message passing)模式所需的排序保证。
该模式在 LKMM 的 litmus 测试中有直接的可执行验证:tools/memory-model/litmus-tests/MP+pooncerelease+poacquireonce.litmus 声明"Result: Never",即exists (1:r0=1 /\ 1:r1=0)的坏结果永远不会出现,从而证明smp_store_release()与smp_load_acquire()对消息传递模式提供了充分排序。与之对照,tools/memory-model/litmus-tests/SB+fencembonceonces.litmus(store-buffering 模式)则演示了必须使用smp_mb()全屏障才能排除exists (0:r0=0 /\ 1:r0=0)的坏结果——"锁与 RCU 也可以,但其他手段大多不行"。
3.3 RCU 读侧排序(RCU Read-Side Ordering)
该类别包括rcu_read_lock()、rcu_read_unlock()等读侧标记,以及rcu_dereference()、srcu_dereference()等指针遍历原语。
与锁原语和 RMW 原子操作相比,RCU 读侧临界区的标记开销极低,因为它们只与对应的宽限期原语交互。例如rcu_read_lock()/rcu_read_unlock()与synchronize_rcu()、synchronize_rcu_expedited()、call_rcu()交互:若某次synchronize_rcu()无法证明自己先于某次rcu_read_lock()开始,那么它必须阻塞到与之匹配的rcu_read_unlock()为止。更多细节可参见synchronize_rcu()的 docbook 头注释及仓库内 Documentation/RCU 目录。
RCU 的指针遍历原语(rcu_dereference()、srcu_dereference())将其加载(必须是指针)与该 CPU 后续所有地址由该加载值计算得到的内存访问排序——这被称为从返回指针对后续访问存在地址依赖(address dependency)。
对某 RCU 保护指针的rcu_dereference()调用,通常与对同一指针的rcu_assign_pointer()配对,其方式与smp_load_acquire()/smp_store_release()的配对如出一辙。二者常常被封装进其他 API——例如 include/linux/rculist.h 中定义的 RCU 链表 API 成员。若指针值在rcu_dereference()返回后、再次rcu_dereference()之前被算术修改,请务必阅读 Documentation/RCU/rcu_dereference.rst。仓库中的 tools/memory-model/litmus-tests/MP+onceassign+derefonce.litmus 即为验证用例:它声明exists (1:r0=x /\ 1:r1=0)(读者看到新指针却读到初始化前的垃圾值)"Never" 出现,证明rcu_assign_pointer()与rcu_dereference()足以保证 RCU 读者在遍历含新插入元素的结构时不会看到未初始化的内容。
3.4 控制依赖(Control Dependencies)
控制依赖从一次标记加载(READ_ONCE()或更强)出发,经由一个if条件,延伸到仅在该if语句某一分支中执行的标记存储(WRITE_ONCE()或更强)。之所以叫"控制依赖",是因为它由比较、条件分支等控制流指令中介。简言之:当READ_ONCE()与WRITE_ONCE()之间存在if条件时,可以用控制依赖强制二者间的排序。经典示例:
q = READ_ONCE(a); if (q) WRITE_ONCE(b, 1);此时所有 CPU 都会看到"对 a 的读先于对 b 的写"。
然而,控制依赖极易被编译器优化摧毁,任何使用都必须通盘考虑构建内核所用的全部编译器。官方为此单列了一份深度文档:tools/memory-model/Documentation/control-dependencies.txt,其中列举了若干关键陷阱,择要如下:
- 只排序后来的存储:load-load 控制依赖不保证排序,必须显式加读屏障:
q = READ_ONCE(a); if (q) { smp_rmb(); p = READ_ONCE(b); }因为部分 CPU 允许猜测
b的加载结果;而存储不会被猜测,所以 load-store 控制依赖(通常)成立。 READ_ONCE()与WRITE_ONCE()缺一不可:没有READ_ONCE(),编译器可能把对a的加载与其他加载融合;没有WRITE_ONCE(),编译器可能把对b的存储与其他存储融合,甚至把存储变换为"加载+检查+存储",而编译器生成的这个加载不受控制依赖排序。- 编译器可能证明
a恒非零,从而合法地消去if:q = a; b = 1; /* BUG: Compiler and CPU can both reorder!!! */ - 两个分支存储相同值时,编译器会把存储提升到
if之外,从而消灭控制依赖;此时必须改用显式内存序(如smp_store_release())或smp_mb()。仅靠barrier()不够。 - 对局部变量
q的运算可能让编译器猜出值(如if (q % MAX)且MAX在编译期为 1),从而再次消去条件分支;必要时可用BUILD_BUG_ON(MAX <= 1)施加编译期约束。 - 布尔短路求值同样危险:
if (q || 1 > 0)中第二个条件恒真,编译器可将整个if消去。 - 控制依赖只作用于
if的分支内部,不排序if语句之后的代码;因为编译器可能把两分支的存储编译为条件移动指令(cmov),使依赖只延伸到 cmov 及其依赖的存储。
control-dependencies.txt的总结要点包括:控制依赖只能排序"先前加载 vs 后续存储",若需其他排序请用smp_load_acquire()、smp_store_release()或(先前存储 vs 后续加载时)smp_mb();两分支同值存储必须显式排序;至少需要一个涉及先前加载的运行时条件分支;控制依赖可与其它屏障正常配对;控制依赖不提供多拷贝原子性(multicopy atomicity),若需所有 CPU 就某存储与所有访问的排序达成一致,请用smp_mb();编译器不理解控制依赖,保护它们是你的责任。
四、无序访问(Unordered Accesses)
4.1 无序标记操作(Unordered Marked Operations)
对不同变量的无序操作就是无序的。但若一组 CPU 对单个变量施加这些操作,所有 CPU 会就操作顺序达成一致。当然,无序标记访问也可被本文前述机制约束。它们分三类:
- 标记写:如
WRITE_ONCE()、atomic_set()。这些原语要求编译器按预期执行顺序发出对应存储指令,从而抑制一系列破坏性优化;但不提供任何硬件排序保证——事实上,许多 CPU 会很乐意重排标记写(彼此之间,或与其它无序操作之间),除非这些操作针对同一变量。 - 标记读:如
READ_ONCE()、atomic_read()。语义与标记写对称:约束编译器,不约束硬件。 - 无序 RMW 原子操作:指非返回值型且名字不以
_acquire/_release结尾的 RMW 操作,以及名字以_relaxed结尾的返回值型 RMW 操作,例如atomic_add()、atomic_or()、atomic64_fetch_xor_relaxed()。它们仍保证 RMW 的原子性——五个并发的atomic_inc()施加于同一变量,可靠地使其值增加五;但许多 CPU 会重排它们与彼此、与其它无序操作之间的顺序。该类别可借助 2.2 节的smp_mb__before_atomic()/smp_mb__after_atomic()高效地获得排序。
简言之:除非全部作用于单一变量,或被本文前述操作约束,否则这些操作可以被自由重排。
4.2 未标记的 C 语言访问(Unmarked C-Language Accesses)
未标记的 C 语言访问,是指对普通变量的普通访问——即既非volatile、也非 C11 原子变量的变量。这类访问不提供任何排序保证,也不保证访问的"原子性"。例如编译器可能(也确实会)把一次普通存储拆分为多次更小的存储;当另一 CPU 在存储执行期间加载同一变量,可能看到新旧值混杂(mashup)的结果。
未标记访问天然无序,并受无数编译器优化影响,其中许多会破坏你的并发代码。为共享变量使用未标记访问虽有可能,但需要持续的高度谨慎。barrier()原语及本文讨论的各种排序原语可提供帮助;但必须再次强调:使用未标记访问不仅要盯着自己的代码,还要盯着可能用来构建它的所有编译器——它们可能把一系列加载合并为一次加载、把一系列存储合并为一次存储,甚至把一次存储拆成多次更小的存储。
不过,存在几种可以放心对共享变量使用未标记访问的方式:
- 用特定锁守护对该变量的所有访问,使该变量永不存在并发的冲突访问(当 (1) 对某变量的并发访问中至少有一个是未标记访问,且 (2) 其中至少有一个是写(无论是否标记)时,即构成"冲突访问");
- 与上相同,但改用读写锁、顺序锁(seqlock)等其它同步原语;
- 用锁等手段确保对某变量的所有并发访问都是读;
- 将该变量仅用于统计或启发式用途,容忍偶尔出现的错误值;
- 将访问的变量声明为 C11 原子类型;
- 将访问的变量声明为
volatile。
如果你需要"更危险地活着",请务必花时间理解编译器(LWN 上有两篇著名的相关文章:《Who's afraid of a big bad optimizing compiler?》与《Calibrating your fear of big bad optimizing compilers》)。用得妥当,未标记访问可以降低快速路径的开销;代价是持续的高度警觉——每当新版本编译器发布、新优化被启用,都要重新审视代码。
五、速查表与可执行验证
5.1 排序能力速查表
cheatsheet.txt 以表格形式总结了各类操作的排序能力。表中行代表操作类别,列代表"先前操作/后续操作"对,Y表示提供排序,a表示在存在中间 RMW 原子操作时提供排序,C表示排序是累积的(cumulative),P表示排序会传播(propagates):
| 操作 | 先前操作 | 后续操作 |
|---|---|---|
| Relaxed store | 对自身(Self) | 对同变量的后续访问(SV) |
| Relaxed load | 对自身 | 依赖读 DR、依赖写 DW、同变量 SV |
| Relaxed RMW 操作 | 对自身 | DR、DW、SV |
rcu_dereference() | 对自身 | DR、DW、SV |
成功的*_acquire() | 读 R | 后续读 R、依赖读 DR、依赖写 DW、RMW、SV |
成功的*_release() | 累积 C、自身、读 R、写 W、RMW | 写 W、SV |
smp_rmb() | 读 R | 读 R、DR |
smp_wmb() | 写 W | 写 W、DW |
smp_mb()与synchronize_rcu() | C、P、自身、R、W、RMW | 自身、R、W、DR、DW、RMW、SV |
| 成功的全排序非 void RMW | C、P、自身、R、W、RMW | 自身、R、W、DR、DW、RMW、SV |
smp_mb__before_atomic() | C、P、自身、R、W | 依赖中间 RMW 的 a、a、a、a、SV |
smp_mb__after_atomic() | C、P、依赖中间 RMW 的 a、a、W | 自身、R、W、DR、DW、RMW、SV |
其中 "Relaxed" 指READ_ONCE()、WRITE_ONCE()、*_relaxed()RMW、失败的 RMW、atomic_inc()这类非返回值 RMW,以及atomic*_read()/atomic*_set()家族。该表是排查并发 bug 时的高效索引。
5.2 用 LKMM 工具链验证排序
仓库 tools/memory-model/ 目录本身就是一套可运行的 LKMM 工具链,包含:
- linux-kernel.bell:定义事件的分类(读、写、RMW、屏障等);
- linux-kernel.cat:核心排序公理(acyclicity、sc 等)与约束;
- linux-kernel.def:各类原语到内存模型事件的映射;
- lock.cat:锁相关排序规则;
- scripts/:运行 litmus 测试的辅助脚本;
- litmus-tests/:大量可执行测试,覆盖屏障、锁、RCU、控制依赖、release/acquire 等模式。
例如MP+pooncerelease+poacquireonce(消息传递 + release/acquire)、SB+fencembonceonces(store-buffering + 全屏障)、MP+onceassign+derefonce(RCU 指针发布)等,每个文件头部都声明了期望结果(Result: Never / Sometimes),坏结果以exists (...)形式给出。你可以利用该工具链对自己编写的并发模式进行机械化验证,而不是仅凭直觉判断——这正是 LKMM 相对纯文档讲解的独特价值。
结语
内存序是 Linux 内核并发编程中最容易出错、也最需要精确理解的主题之一。ordering.txt 按"屏障 → 有序访问 → 无序访问"的强度层级,为开发者提供了一张清晰的选型地图:需要最强约束时用全屏障或全排序 RMW;需要对称的发布/消费语义时优先 release/acquire 配对;RCU 场景使用rcu_assign_pointer()/rcu_dereference();能不用屏障时则尽量依赖控制依赖——但务必先读透control-dependencies.txt的警示。无论选择哪条路径,记住三条铁律:优先使用标记访问(READ_ONCE()/WRITE_ONCE()及更强原语);始终考虑所有可能编译内核的编译器;为每个内存序原语写清注释。结合本仓库的速查表与 litmus 测试,你可以在动手写代码之前就完成排序语义的形式化验证。
【免费下载链接】linuxLinux kernel source tree项目地址: https://gitcode.com/GitHub_Trending/li/linux
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考