Linux 内核内存屏障(中文译文)
============================
Linux 内核内存屏障(中文译文)
作者:David Howells dhowells@redhat.com
Paul E. McKenney paulmck@linux.vnet.ibm.com
说明:本译文意在忠实呈现 Linux 内核文档 memory-barriers.txt 的技术内容与结构。为便于阅读,保留了重要术语(如 ACQUIRE/RELEASE、mb/rmb/wmb、READ_ONCE/WRITE_ONCE、DMA、MMIO、RCU 等)的英文形式,并在需要时给出中文释义。示例中的伪代码、时序图与要点总结均以更适合中文读者的方式表述。
目录
- 抽象的内存访问模型
- 什么是内存屏障
- 内存屏障的种类
- 内存屏障不保证什么
- 数据依赖屏障
- 控制依赖
- SMP 屏障配对
- 屏障序列示例
- 读屏障与加载推测
- 传递性(累积性)
- 显式内核屏障
- 编译器屏障、READ_ONCE/WRITE_ONCE
- CPU 内存屏障(mb/rmb/wmb 等)
- 高级屏障(smp_store_mb、smp_mb__before/after_atomic 等)
- 一致性内存与 DMA 屏障(dma_rmb/dma_wmb)
- MMIO 写屏障(mmiowb)
- 隐式内核屏障
- 加锁原语(ACQUIRE/RELEASE)
- 禁用/启用中断
- 睡眠与唤醒(wait/wake)
- 其他杂项函数(如 schedule)
- CPU 间加锁的屏障效应
- 锁 vs 内存访问
- 锁 vs I/O 访问(需 mmiowb)
- 在哪里需要内存屏障
- 处理器间交互
- 原子操作
- 访问设备(MMIO/一致性内存)
- 中断
- 假定的最低执行顺序模型(简述)
- CPU 缓存的影响
- 缓存一致性
- 与 DMA/与 MMIO 的一致性问题
- CPU 行为与架构差异(含 Alpha)
- 示例(如环形缓冲区)
- 参考资料
抽象的内存访问模型
在多核系统中,每个 CPU 执行程序并发起内存访问;系统其他部分(内存、其他 CPU、设备)通过接口观察这些访问的“效果”。编译器与 CPU 可在不破坏单核语义的前提下重排指令与内存操作,因此不同 CPU 之间看到的顺序可能不同。
关键点:
- 独立的内存访问可能以几乎任意顺序被其他参与者观察到。
- 数据依赖可保证同一 CPU 内某些加载的顺序,但跨 CPU 时仍需屏障。
- 设备(通过 MMIO)通常要求严格的访问顺序;CPU/编译器的重排会导致设备操作失败。
什么是内存屏障
内存屏障(memory barrier)用于对屏障两侧的内存操作施加可被其他 CPU/设备观察到的“部分顺序”。它们抑制重排、推测与合并,使并发代码对交互的时序具备可控性。
内存屏障的种类
典型类型如下:
- 写屏障(Write barrier,wmb/smp_wmb):保证屏障之前的所有写(store)先于屏障之后的所有写被其他参与者观察。它只约束写,不约束读。
- 数据依赖屏障(read_barrier_depends/smp_read_barrier_depends):当第二次加载的地址或内容依赖第一次加载的结果时,保证被依赖的数据在后续加载前可见。它只约束存在“真实数据依赖”的加载,不影响写或独立加载。
- 读屏障(Read barrier,rmb/smp_rmb):保证屏障之前的所有读(load)先于屏障之后的所有读被观察。它隐含数据依赖屏障。
- 通用屏障(Full/General barrier,mb/smp_mb):同时约束读与写,即屏障前的所有读写先于屏障后的所有读写被观察。它隐含读与写屏障。
- ACQUIRE/RELEASE:单向屏障。ACQUIRE 保证其后的访问在它之后出现;RELEASE 保证其前的访问在它之前出现。二者通常配对使用(如锁的加/解)。ACQUIRE+RELEASE 并非完整屏障,但在临界区层面保证“之前的临界区完成后,后继临界区的可见性”。
内存屏障不保证什么
内核屏障不保证:
- 屏障完成即等同“屏障之前的访问已对所有参与者完成”。它只在本 CPU 的提交序列上画一条线,合格类型的访问不得跨线重排。
- 单侧屏障能让其他 CPU 自动“按正确顺序观察”你的访问;通常需要配对屏障(见下)。
- 中间硬件不重排;总线与缓存可能仍以自身协议传播与重排。
数据依赖屏障
当第二次加载依赖第一次加载的结果(如先读指针、再读该指针指向的数据)时,若不插入数据依赖屏障,另一 CPU 对数据的更新可能在你“看到指针更新”之后仍不可见,导致读到旧值。DEC Alpha 等架构上这种反直觉行为可出现。解决:在“读指针”与“读指针指向内容”之间放数据依赖屏障或更强屏障。
控制依赖
若第二次加载(或存储)仅通过分支条件依赖第一次加载(而不是地址本身依赖),属于控制依赖。加载-加载的控制依赖需要读屏障(而非数据依赖屏障)才能可靠排序。编译器可能消除条件或合并访问;必须使用 READ_ONCE/WRITE_ONCE 防优化,并在必要处使用显式屏障(如 smp_store_release)。控制依赖不具传递性。
SMP 屏障配对
当涉及 CPU-CPU 交互时需配对:
- 写屏障通常应与对端的读屏障或数据依赖屏障配对。
- 通用屏障可与多数屏障配对,但不具传递性。
- ACQUIRE 通常配 RELEASE。
配对的意图是确保一侧的“先后关系”能被另一侧以相应的顺序观察到。
屏障序列示例
示例展示:
- 写屏障把“屏障前的一组写”排在“屏障后的一组写”之前。
- 数据依赖屏障把“依赖加载”的可见性与顺序对齐到另一 CPU 的更新之后。
- 读屏障保证“先读 B 后读 A”时,若对端在写屏障后写了 B 与 A,则第二次读 A 将看到对应更新。
读屏障与加载推测
CPU 可能在分支或长指令(如除法)期间对某加载进行推测。读屏障或数据依赖屏障会在屏障处“重新审视”该推测值:若没有失效/更新,推测值可以继续使用;否则强制重新加载以获得最新值。
传递性(累积性)
通用屏障在内核中保证跨 CPU 的传递性:若 CPU2 在通用屏障配对下观察到 X 已更新、Y 未更新,则 CPU3 对 X 的读不应回退。仅读/写屏障不保证累积性;需要通用屏障确保“全系统顺序一致”。
显式内核屏障
编译器屏障与 READ_ONCE/WRITE_ONCE
- barrier():编译器屏障,阻止编译器跨越屏障重排内存访问(对 CPU 无直接约束)。
- READ_ONCE()/WRITE_ONCE():防止编译器合并、消除、撕裂或重排针对某一变量的访问;在并发语境下保持单变量访问的“原子性”与“可见性假设”。用于防止多种“看似合理但有害”的优化。
CPU 内存屏障(必选与 SMP 条件)
- 必选:mb()/wmb()/rmb()/read_barrier_depends()。对 UP 也有意义,尤其用于 I/O 顺序。
- SMP 条件:smp_mb()/smp_wmb()/smp_rmb()/smp_read_barrier_depends()。在 UP 上退化为编译器屏障;在 SMP 上提供跨 CPU 的顺序约束。
高级屏障与原子操作辅助
- smp_store_mb(var, value):存储后插入完整屏障。
- smp_mb__before_atomic()/smp_mb__after_atomic():与不返回值的原子操作(如原子加减、位操作)配合,保证相邻访问相对于其他 CPU 的顺序可感知,常用于引用计数与对象生命周期管理。
- lockless_dereference():围绕数据依赖屏障的无锁指针获取工具,适合非 RCU 管理生命周期的场景。
一致性内存与 DMA 屏障(共享内存)
- dma_rmb()/dma_wmb():约束 CPU 与具 DMA 能力设备共享内存的读写顺序。示例:只有在设备释放描述符所有权后再读数据(dma_rmb);在把数据写回描述符后再把所有权交给设备(dma_wmb)。
- 与 wmb() 配合:在向设备 MMIO 写“门铃”之前,确保共享内存写入完成。
MMIO 写屏障(设备寄存器)
- mmiowb():对弱序 I/O 区域的写进行部分有序化。在 NUMA 或桥设备场景下,多 CPU 释放同一锁前分别写 MMIO,若不使用 mmiowb(),可能在桥上交错导致设备看到错序。mmiowb() 保障“同一锁保护下的写”在释放前一致地提交。
隐式内核屏障
加锁原语(ACQUIRE/RELEASE)
- 自旋锁/读写自旋锁/互斥量/信号量/读写信号量都具 ACQUIRE/RELEASE 语义。
- ACQUIRE:其后的访问出现在 ACQUIRE 之后;之前的访问可能渗入 ACQUIRE 之后(单向)。
- RELEASE:其前的访问出现在 RELEASE 之前;之后的访问可能渗入 RELEASE 之前(单向)。
- ACQUIRE 与 RELEASE 并非完整屏障;不要将“加锁后立即解锁”视作 mb。
- 某些失败的获取(未能拿到锁)不隐含屏障。
禁用/启用中断
- 禁用中断与启用中断仅作为编译器屏障;若需要内存或 I/O 屏障,需显式加入。
睡眠与唤醒(wait/wake)
- set_current_state() 在设置任务状态后执行 smp_store_mb()(完整屏障),保证“先设置状态,再观察事件标志”的顺序。
- wake_up()/wake_up_process() 在实际唤醒时隐含写屏障,排序“事件指示写”与“设置 TASK_RUNNING”。
- 若睡眠方需要在看到事件后读取多个数据项的顺序(相对于唤醒方的写),双方需显式插入 smp_wmb/smp_rmb 以对齐可见性。
其他杂项函数
- schedule() 等调度相关函数隐含完整内存屏障。
CPU 间加锁的屏障效应
锁 vs 内存访问
- 在 SMP 上,不同 CPU 的临界区访问相互可见,但顺序受到各自 ACQUIRE/RELEASE 的约束。第三方 CPU 不会观察到“释放锁后的写”排在“获取锁前的写”之前(相对于该锁)。
锁 vs I/O 访问(需 mmiowb)
- NUMA/桥设备可能导致两个 CPU 在同一锁保护下的 MMIO 写在桥上交错。释放自旋锁前调用 mmiowb() 可强制同一锁内的 MMIO 写顺序一致。若同一设备在写后紧随读(readl),读可强制写完成,从而免 mmiowb。
在哪里需要内存屏障
处理器间交互
- 多 CPU 访问同一数据集时,若不使用锁,需要以屏障保证关键读写的先后与可见性。例如在信号量慢路径中,唤醒方必须以“读 next 指针、读 task 指针、mb、清 task、唤醒”的顺序进行,以防在清 task 后被唤醒方破坏栈导致读取 next 失败。
原子操作
- 修改内存并返回旧/新状态的原子操作(如 xchg、atomic_return、test_and_bit、cmpxchg 成功时等)隐含 SMP 条件的完整屏障(除特殊锁原语外),适合实现 ACQUIRE/RELEASE 或引用计数语义。
- 不隐含屏障的原子(atomic_set、set_bit/clear_bit/change_bit、atomic_add/sub/inc/dec 等)在实现 RELEASE 或构造锁场景可能需要显式 smp_mb__before_atomic 等。
访问设备(MMIO/一致性内存)
- 设备往往要求严格顺序;CPU 与编译器可能重排、合并或融合内存访问,导致设备看到错序。需结合 wmb/rmb/mb、dma_wmb/dma_rmb、mmiowb 与 READ_ONCE/WRITE_ONCE 保证序与可见性。
中断
- 进程级与中断处理在共享数据上的交互常需 READ_ONCE/WRITE_ONCE;在需要时以屏障确保“先写消息,再置标志;中断侧先读标志,再读消息”的顺序。
假定的最低执行顺序模型(简述)
内核文档在此部分描述了在缺乏更强架构保证时,内核可依赖的最低排序模型:独立读写可能重排;依赖访问在本 CPU 内保持顺序;跨 CPU 的顺序需通过显式屏障或锁来建立;通用屏障提供跨 CPU 的传递性。
CPU 缓存的影响与一致性
缓存一致性
- 大多数现代架构在 CPU 之间提供缓存一致性协议,使得对同一缓存行的更新可传播。但传播的时序与顺序可能与程序期望不一致,需屏障来约束可见性。
与 DMA 的一致性
- 设备通过 DMA 直接访问内存;CPU 的缓存可能需要刷写/失效以与设备视图一致。dma_rmb/dma_wmb 可在共享描述符/队列的所有权转移与数据读写之间建立正确的顺序。
与 MMIO 的一致性
- MMIO 通常不参与 CPU 缓存一致性;需 wmb/rmb/mb 与 mmiowb 来约束寄存器写的顺序与提交点,防止桥或设备端观察到交错或乱序。
架构差异与 CPU 行为
- 不同架构对推测、重排与缓存行为的下限保证不同。Alpha 等架构尤其宽松,数据依赖屏障在其上必需;而在多数其它架构上,数据依赖屏障可能为空操作。编写可移植并发代码时应以“最低保证”设计。
示例:环形缓冲区(简述)
- 生产者写入数据与更新写指针之间需 wmb/mb,消费者读指针与读数据之间需 rmb/mb(或 ACQUIRE/RELEASE 组合),以确保“先有数据,再更新指针;先观察指针,再读取数据”的顺序与可见性。若指针以指针-数据的形式组织,消费者端的数据依赖屏障也可能适用。
参考资料
- Documentation/atomic_ops.txt:原子操作的语义与隐含屏障。
- Documentation/DMA-API.txt 与 DMA-API-HOWTO.txt:DMA 与一致性内存的使用与屏障。
- Documentation/PCI/pci.txt:PCI 相关一致性与访问次序问题。
- Documentation/DocBook/deviceiobook.tmpl:设备 I/O 顺序与屏障讨论。
- Peter Sewell 等人的内存模型试验(如 LB/WWC):理解控制依赖与传递性限制。
术语对照(便于查阅)
- barrier:编译器屏障
- mb/rmb/wmb:必选 CPU 内存屏障(全/读/写)
- smp_mb/smp_rmb/smp_wmb:SMP 条件屏障(全/读/写)
- read_barrier_depends/smp_read_barrier_depends:数据依赖屏障(必选/SMP 条件)
- smp_store_release/smp_load_acquire:释放/获取操作(单向屏障)
- READ_ONCE/WRITE_ONCE:防编译器优化的单次访问原语
- dma_rmb/dma_wmb:一致性内存的读/写屏障(CPU 与设备共享内存)
- mmiowb:MMIO 写屏障(在释放锁前强制同锁内 MMIO 写顺序)
- ACQUIRE/RELEASE:获取/释放(锁语义)
- MMIO:内存映射 I/O
- RCU:Read-Copy Update(读-复制-更新),常与数据依赖屏障配合
结语
内存屏障是并发内核代码的基石之一。正确选择与配对屏障,结合 READ_ONCE/WRITE_ONCE、防止控制依赖被编译器优化掉、理解设备与缓存一致性,是保证跨 CPU 与与设备交互时序可见性与正确性的关键。编写并发代码时,务必以最低保证为基线,必要时使用通用屏障以获得传递性。
