Linux 6.6内核并发与同步(一):原子操作与内存屏障 — 内核同步的硬件底座

引言:从"一行 i++"说起

i++ 在 C 里是一行,但编译成汇编是"读 + 加 1 + 写回"三步。两个 CPU 同时 i++,各读一次、各写一次,最终 i 可能只加了 1------这就是数据竞争(data race)。

内核里到处都是这种东西:自旋锁的加锁、引用计数的增减、RCU 的读写、percpu-rwsem 的计数。它们凭什么在几十个 CPU 并发下不出错?靠两层硬件底座:

  1. 原子操作(atomic operation) :把"读-改-写"变成 CPU 眼里不可分割的一条,其他 CPU 要么看到改之前、要么看到改之后,绝看不到中间态。
  2. 内存屏障(memory barrier) :CPU 和编译器会重排访存指令(为了性能),屏障强制某段代码的访存顺序,保证"先写 A 再写 B"这个约定在别的 CPU 眼里依然成立。

这篇讲这两层的硬件机制 + 内核封装 ------为什么需要屏障、LOCK 前缀怎么保证原子性、x86 的 TSO 模型允许什么重排、内核的 barrier()/smp_mb()/READ_ONCE 分别管什么。


全景图

#mermaid-svg-KDHgTQr72cohSN5y{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-KDHgTQr72cohSN5y .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-KDHgTQr72cohSN5y .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-KDHgTQr72cohSN5y .error-icon{fill:#552222;}#mermaid-svg-KDHgTQr72cohSN5y .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-KDHgTQr72cohSN5y .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-KDHgTQr72cohSN5y .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-KDHgTQr72cohSN5y .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-KDHgTQr72cohSN5y .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-KDHgTQr72cohSN5y .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-KDHgTQr72cohSN5y .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-KDHgTQr72cohSN5y .marker{fill:#333333;stroke:#333333;}#mermaid-svg-KDHgTQr72cohSN5y .marker.cross{stroke:#333333;}#mermaid-svg-KDHgTQr72cohSN5y svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-KDHgTQr72cohSN5y p{margin:0;}#mermaid-svg-KDHgTQr72cohSN5y .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-KDHgTQr72cohSN5y .cluster-label text{fill:#333;}#mermaid-svg-KDHgTQr72cohSN5y .cluster-label span{color:#333;}#mermaid-svg-KDHgTQr72cohSN5y .cluster-label span p{background-color:transparent;}#mermaid-svg-KDHgTQr72cohSN5y .label text,#mermaid-svg-KDHgTQr72cohSN5y span{fill:#333;color:#333;}#mermaid-svg-KDHgTQr72cohSN5y .node rect,#mermaid-svg-KDHgTQr72cohSN5y .node circle,#mermaid-svg-KDHgTQr72cohSN5y .node ellipse,#mermaid-svg-KDHgTQr72cohSN5y .node polygon,#mermaid-svg-KDHgTQr72cohSN5y .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-KDHgTQr72cohSN5y .rough-node .label text,#mermaid-svg-KDHgTQr72cohSN5y .node .label text,#mermaid-svg-KDHgTQr72cohSN5y .image-shape .label,#mermaid-svg-KDHgTQr72cohSN5y .icon-shape .label{text-anchor:middle;}#mermaid-svg-KDHgTQr72cohSN5y .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-KDHgTQr72cohSN5y .rough-node .label,#mermaid-svg-KDHgTQr72cohSN5y .node .label,#mermaid-svg-KDHgTQr72cohSN5y .image-shape .label,#mermaid-svg-KDHgTQr72cohSN5y .icon-shape .label{text-align:center;}#mermaid-svg-KDHgTQr72cohSN5y .node.clickable{cursor:pointer;}#mermaid-svg-KDHgTQr72cohSN5y .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-KDHgTQr72cohSN5y .arrowheadPath{fill:#333333;}#mermaid-svg-KDHgTQr72cohSN5y .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-KDHgTQr72cohSN5y .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-KDHgTQr72cohSN5y .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KDHgTQr72cohSN5y .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-KDHgTQr72cohSN5y .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KDHgTQr72cohSN5y .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-KDHgTQr72cohSN5y .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-KDHgTQr72cohSN5y .cluster text{fill:#333;}#mermaid-svg-KDHgTQr72cohSN5y .cluster span{color:#333;}#mermaid-svg-KDHgTQr72cohSN5y div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-KDHgTQr72cohSN5y .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-KDHgTQr72cohSN5y rect.text{fill:none;stroke-width:0;}#mermaid-svg-KDHgTQr72cohSN5y .icon-shape,#mermaid-svg-KDHgTQr72cohSN5y .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-KDHgTQr72cohSN5y .icon-shape p,#mermaid-svg-KDHgTQr72cohSN5y .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-KDHgTQr72cohSN5y .icon-shape .label rect,#mermaid-svg-KDHgTQr72cohSN5y .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-KDHgTQr72cohSN5y .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-KDHgTQr72cohSN5y .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-KDHgTQr72cohSN5y :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 原子 RMW 本身是全屏障
LOCK 前缀 = 最便宜的屏障
并发正确性两件套
原子操作 atomic_t

解决: 中间态
内存屏障 barrier

解决: 乱序
LOCK 前缀

lock; addl/inc/dec
专用指令

xadd / cmpxchg / xchg
128 位

cmpxchg16b (CX16)
锁 cache line

(RFO 独占, 接 CPU 篇 #14 MESI)
xchg 隐式原子

cmpxchg/xadd 要 LOCK
编译器屏障 barrier()

memory clobber
CPU 屏障

mfence/lfence/sfence
smp_ 屏障

smp_mb/smp_wmb/smp_rmb
READ/WRITE_ONCE

volatile 强转
x86 TSO 下:

smp_wmb/rmb = barrier()

smp_mb = lock; addl $0

这张图的核心信息只有一句:原子操作和内存屏障是"原子性"和"顺序"两本账,但在 x86 上它们共享同一个硬件机制------LOCK 前缀。


一、概念合集(零基,术语首现即定义)

写这一篇必须先把几个词钉死,否则后面全是浆糊。

术语 一句话定义 反例/后果
原子性(atomicity) 一次操作对别的观察者"要么全做、要么没做",看不到中间态 两个 CPU 同时 i++,丢一次加
可见性(visibility) 一个 CPU 的写,何时能被别的 CPU 看到 写进了 store buffer 但还没刷进 cache,别的核读旧值
重排(reordering) 实际执行顺序 ≠ 程序书写顺序(编译器或 CPU 干的) a=1; b=2 实际先写 b
顺序一致性 SC 最强的模型:所有 CPU 的所有操作,按某个全局顺序串行执行,且每 CPU 内部保序 代价极高,真实 CPU 都不做
TSO(Total Store Order) x86 的内存模型:只允许"store 之后跟 load"这一种重排 Dekker 例子 (r1,r2)=(0,0)
acquire(获取语义) 读操作之后的所有访存不许重排到这个读之前 拿到锁后,临界区读的数据一定是最新的
release(释放语义) 写操作之前的所有访存不许重排到这个写之后 释放锁前,临界区改的数据一定已全局可见
数据竞争(data race) 多个 CPU 访问同一内存、至少一个是写、且没有同步 未定义行为,一切皆可能

关键区分,务必记住:

  • 原子性 ≠ 可见性 。atomic_set() 是原子的(一次写不撕裂),但它不提供任何排序 ,别的 CPU 不保证马上看到。看源码一目了然:atomic_read()/atomic_set() 的实现就是 __READ_ONCE()/__WRITE_ONCE()(atomic.h:17/23,见 4.5),只是"不被撕裂的单次访问"------排序归排序、原子归原子。
  • 原子性 ≠ 顺序 。LOCK addl 既保证原子性、又顺带提供了全屏障(下文第四节),但这是 x86 的实现巧合 ,不是"原子操作"的定义要求。C11 里 atomic_fetch_add(..., memory_order_relaxed) 就是"只要原子性、不要顺序"。

二、乱序的三个来源

内存屏障要防的是"重排"。重排有三个独立的来源,一层比一层难抓:

2.1 编译器重排(最上层,可静态抓)

编译器优化时会做:把不相关的访存放在寄存器里不写回、把两个无依赖的访存交换顺序、把循环里的读提到循环外(loop-invariant code motion)。只要编译器认为"单线程语义不变",它就敢重排。

内核的防线 :barrier()(include/linux/compiler.h:85)------

c 复制代码
# define barrier() __asm__ __volatile__("": : :"memory")

这是一个空的汇编语句,但 "memory" clobber 告诉编译器:假设所有内存都可能被这个语句改了 。于是编译器不敢把任何内存访问跨过它移动。它管的是编译器,对 CPU 硬件乱序毫无作用。

2.2 CPU 乱序执行(中间层)

现代 CPU 是乱序执行(out-of-order execution)的:指令按序进流水线,但真正执行、提交(commit,把结果写回寄存器)可以不按序。只要不影响单核自己的程序语义,CPU 就敢让后面的指令先跑完。

访存之间的四种相邻组合,CPU 可能重排哪几种,取决于内存模型(第三节)。

2.3 store buffer(最底层,StoreLoad 重排的根源)

这是最反直觉的一个,也是 x86 上唯一需要真正硬件屏障的地方。

CPU 执行 store X=1 时,为了不阻塞(写要等 cache line 独占,慢),它把写操作先丢进一个每核私有的 store buffer(写缓冲区) ,立刻返回去执行后面的指令。当后面的 load Y 来了,如果 CPU 直接读内存/cache,就可能读到旧值 ------因为自己的 store X 还没真正刷出去。

于是从别的 CPU 视角看:CPU A 先写了 X,后读了 Y,但它的 X 写还没被别人看到 。这就表现为 store 之后的 load 提前完成 ,即 StoreLoad 重排。
#mermaid-svg-eZP2vRdL1enmMp2Y{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-eZP2vRdL1enmMp2Y .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-eZP2vRdL1enmMp2Y .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-eZP2vRdL1enmMp2Y .error-icon{fill:#552222;}#mermaid-svg-eZP2vRdL1enmMp2Y .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-eZP2vRdL1enmMp2Y .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-eZP2vRdL1enmMp2Y .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-eZP2vRdL1enmMp2Y .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-eZP2vRdL1enmMp2Y .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-eZP2vRdL1enmMp2Y .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-eZP2vRdL1enmMp2Y .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-eZP2vRdL1enmMp2Y .marker{fill:#333333;stroke:#333333;}#mermaid-svg-eZP2vRdL1enmMp2Y .marker.cross{stroke:#333333;}#mermaid-svg-eZP2vRdL1enmMp2Y svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-eZP2vRdL1enmMp2Y p{margin:0;}#mermaid-svg-eZP2vRdL1enmMp2Y .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-eZP2vRdL1enmMp2Y .cluster-label text{fill:#333;}#mermaid-svg-eZP2vRdL1enmMp2Y .cluster-label span{color:#333;}#mermaid-svg-eZP2vRdL1enmMp2Y .cluster-label span p{background-color:transparent;}#mermaid-svg-eZP2vRdL1enmMp2Y .label text,#mermaid-svg-eZP2vRdL1enmMp2Y span{fill:#333;color:#333;}#mermaid-svg-eZP2vRdL1enmMp2Y .node rect,#mermaid-svg-eZP2vRdL1enmMp2Y .node circle,#mermaid-svg-eZP2vRdL1enmMp2Y .node ellipse,#mermaid-svg-eZP2vRdL1enmMp2Y .node polygon,#mermaid-svg-eZP2vRdL1enmMp2Y .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-eZP2vRdL1enmMp2Y .rough-node .label text,#mermaid-svg-eZP2vRdL1enmMp2Y .node .label text,#mermaid-svg-eZP2vRdL1enmMp2Y .image-shape .label,#mermaid-svg-eZP2vRdL1enmMp2Y .icon-shape .label{text-anchor:middle;}#mermaid-svg-eZP2vRdL1enmMp2Y .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-eZP2vRdL1enmMp2Y .rough-node .label,#mermaid-svg-eZP2vRdL1enmMp2Y .node .label,#mermaid-svg-eZP2vRdL1enmMp2Y .image-shape .label,#mermaid-svg-eZP2vRdL1enmMp2Y .icon-shape .label{text-align:center;}#mermaid-svg-eZP2vRdL1enmMp2Y .node.clickable{cursor:pointer;}#mermaid-svg-eZP2vRdL1enmMp2Y .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-eZP2vRdL1enmMp2Y .arrowheadPath{fill:#333333;}#mermaid-svg-eZP2vRdL1enmMp2Y .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-eZP2vRdL1enmMp2Y .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-eZP2vRdL1enmMp2Y .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eZP2vRdL1enmMp2Y .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-eZP2vRdL1enmMp2Y .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eZP2vRdL1enmMp2Y .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-eZP2vRdL1enmMp2Y .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-eZP2vRdL1enmMp2Y .cluster text{fill:#333;}#mermaid-svg-eZP2vRdL1enmMp2Y .cluster span{color:#333;}#mermaid-svg-eZP2vRdL1enmMp2Y div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-eZP2vRdL1enmMp2Y .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-eZP2vRdL1enmMp2Y rect.text{fill:none;stroke-width:0;}#mermaid-svg-eZP2vRdL1enmMp2Y .icon-shape,#mermaid-svg-eZP2vRdL1enmMp2Y .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-eZP2vRdL1enmMp2Y .icon-shape p,#mermaid-svg-eZP2vRdL1enmMp2Y .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-eZP2vRdL1enmMp2Y .icon-shape .label rect,#mermaid-svg-eZP2vRdL1enmMp2Y .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-eZP2vRdL1enmMp2Y .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-eZP2vRdL1enmMp2Y .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-eZP2vRdL1enmMp2Y :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} CPU1
CPU0
还没进 cache
还没进 cache
store X=1
store buffer

(X=1 暂存, 未刷出)
load Y (读旧值)
store Y=1
store buffer
load X (读旧值)
内存/cache

为什么 store buffer 必须 FIFO 且不能合并 :x86 要求 StoreStore 不重排,如果两个 store 进 buffer 后被合并(coalescing)或乱序刷出,第二个 store 可能先被看到,就破坏了 StoreStore 顺序。所以 x86 的 store buffer 是先进先出的。

一句话总结这一节 :编译器重排靠 barrier()(memory clobber);CPU 乱序靠内存模型约束;store buffer 是 StoreLoad 重排的硬件根源,只能靠真·屏障指令(mfence 或 lock 指令)压制。


三、x86 的内存模型:TSO

3.1 TSO 允许什么、禁止什么

x86 采用 TSO(Total Store Order,全存储序) ,是一种强内存模型(相对 ARM 的弱模型而言)。四种相邻访存组合里:

重排类型 含义 x86 (TSO) 是否允许 需要什么屏障
LoadLoad 两个读换序 ❌ 禁止 smp_rmb()(实为编译器屏障)
LoadStore 读之后跟写换序 ❌ 禁止 无需
StoreStore 两个写换序 ❌ 禁止 smp_wmb()(实为编译器屏障)
StoreLoad 写之后跟读换序 ✅ 允许 smp_mb()(真指令)

这正是 arch/x86/include/asm/barrier.h 的代码所表达的(详细见第五节):

c 复制代码
#define __smp_wmb()	barrier()      // barrier.h:60 ------ 写屏障只是编译器屏障
#define __smp_rmb()	dma_rmb()      // barrier.h:59 ------ 读屏障最终也是编译器屏障
#define __smp_mb()	asm volatile("lock; addl $0,-4(%%" _ASM_SP ")" ...)  // :57 ------ 全屏障才是真指令

读屏障和写屏障为什么在 x86 上只是 barrier() :因为 TSO 已经保证 LoadLoad/LoadStore/StoreStore 三种顺序,CPU 硬件不会重排它们,唯一要防的是编译器 重排,所以一个 memory clobber 就够了。只有 StoreLoad 会乱,所以 smp_mb() 才需要真正发指令。

3.2 Dekker 例子:为什么 StoreLoad 重排违反直觉

两个 CPU 各自"先写后读"一个对方的变量:

复制代码
初始: X = 0, Y = 0

CPU 0               CPU 1
STORE X = 1         STORE Y = 1
r0 = LOAD Y         r1 = LOAD X

如果没有任何同步,可能得到 r0 = 0, r1 = 0------两个 CPU 都读到了对方的旧值。

  • 按顺序一致性(SC) :所有操作得排成一个全局串行顺序,两个写必有一个先发生,后发生的那个 CPU 的读一定能看到先写的值,(0,0) 不可能。
  • 按 TSO :每个 CPU 的 STORE 先钻进自己的 store buffer,然后 LOAD 直接读 cache 里的旧值,于是两个 CPU 都看到 0。(0,0) 完全合法。

这就是 TSO 与 SC 的唯一差别:TSO 允许 store 被"延迟"到后续 load 之后。

3.3 传递性与多拷贝原子性

Documentation/memory-barriers.txt:1429 讲了两个 smp_mb() 独有的能力,是 acquire/release 链给不了的:

  • 多拷贝原子性(multicopy atomicity) :x86 是"other-multicopy atomic"------除了执行 store 的 CPU 自己(它可以从 store buffer 提前读到自己的写),其他所有 CPU 对这个 store 被看到的顺序是一致的。
  • 传递性(transitivity) :要保证"CPU 1 看到 X=1 且 CPU 2 看到 Y=1 时,某个第三者的写也一定被一致地看到",靠的是通用屏障 smp_mb() 。读屏障、数据依赖、写屏障都给不了 传递性------这是 smp_mb() 贵、但不可替代的原因。
c 复制代码
// memory-barriers.txt:1437 的 C 代码骨架
cpu0: r0 = smp_load_acquire(&x); WRITE_ONCE(u,1); smp_store_release(&y,1);
cpu1: r1 = smp_load_acquire(&y); r4 = READ_ONCE(v);   smp_store_release(&z,1);
cpu2: r2 = smp_load_acquire(&z);                       smp_store_release(&x,1);
// 结论: 一条 release-acquire 链只保证链上 CPU 一致,
//       要"所有 CPU 一致"必须上 smp_mb() (general barrier)

Why(这一章的意义) :TSO 是 x86 的硬件契约。内核写的屏障,本质是"在 TSO 允许的那唯一一个漏洞(StoreLoad)上补一张网"。理解了 TSO,就理解了为什么 x86 上 wmb() 便宜(几乎免费)、mb() 贵(要发指令),也就理解了为什么 ARM 移植代码时屏障最容易出 bug------ARM 是弱模型,wmb() 也必须是真指令。


四、原子操作与 LOCK 前缀

4.1 LOCK 前缀:锁的是 cache line,不是总线

arch/x86/include/asm/atomic.h:31 的 arch_atomic_add:

c 复制代码
static __always_inline void arch_atomic_add(int i, atomic_t *v)
{
	asm volatile(LOCK_PREFIX "addl %1,%0"
		     : "+m" (v->counter)
		     : "ir" (i) : "memory");
}

翻译成人话:LOCK addl = "原子地把 i 加到 v->counter 上"。

老 CPU(Pentium 之前) :LOCK 前缀直接锁总线------整条总线被独占,其他 CPU 全都不能访存,直到操作完成。粗暴但正确。

现代 CPU(P6 起) :LOCK 前缀锁 cache line。机制是 MESI(接 CPU 篇 #14):

  1. CPU 要把这条 cache line 读进自己 L1 并置为 M(Modified,独占+脏) 状态------即先做一次 RFO(Read For Ownership,读所有权)。
  2. RFO 会广播 Invalidate,让其他 CPU 上这条 line 的副本全部失效(置 I)。
  3. 独占之后,这个 CPU 在这条 line 上做"读-改-写",期间别的 CPU 拿不到这条 line,自然看不到中间态。
  4. 做完后 line 仍是 M,等将来被赶出去时写回。

所以 LOCK addl 的"原子性",本质是 MESI 协议保证同一时刻只有本 CPU 独占这条 line 。这就是为什么 CPU 篇 #14 讲 MESI 是这里的地基。

顺带一句 :正因为 LOCK 操作要独占 cache line,所以两个 CPU 对同一行 上的不同变量做原子操作会互相打架(false sharing,缓存乒乓),这也呼应了 CPU 篇 #8 percpu 把热点数据每核一份的动机。

4.2 x86 原子操作的三类实现

atomic.h 里几十个函数,按底层指令分成三族:

家族 底层指令 LOCK 前缀? atomic.h 例子
算术/逻辑 addl/sub/incl/decl/andl/orl/xorl 要 LOCK_PREFIX arch_atomic_add :31、arch_atomic_inc :51
读改写返回旧值 xadd / cmpxchg 要 LOCK_PREFIX arch_atomic_add_return :83、arch_atomic_cmpxchg :107
交换 xchg 不要(隐式原子) arch_atomic_xchg :119

关键差异在 xchg:arch/x86/include/asm/cmpxchg.h:73 的注释写得很清楚------

c 复制代码
/*
 * Note: no "lock" prefix even on SMP: xchg always implies lock anyway.
 */
#define arch_xchg(ptr, v)	__xchg_op((ptr), (v), xchg, "")

XCHG 指令天生带锁语义 ,不管加不加 LOCK 前缀都是原子的。这是 x86 的历史设计(XCHG 常用于信号量),所以内核的 arch_xchg 传空字符串当 lock 参数(cmpxchg.h:78)。

而 cmpxchg/xadd 就不行,必须显式 LOCK_PREFIX(cmpxchg.h:133-134、:239-240)。

fetch_and/or/xor 的怪相 (atomic.h:133):这三个没有对应的"LOCK 且返回旧值"的指令,内核用 CAS 循环模拟:

c 复制代码
static __always_inline int arch_atomic_fetch_and(int i, atomic_t *v)
{
	int val = arch_atomic_read(v);
	do { } while (!arch_atomic_try_cmpxchg(v, &val, val & i));  // :137
	return val;
}

读当前值 → 算新值 → CAS 试写,失败(被抢)就重读重试。这是无锁编程最基础的 CAS 循环模板。

4.3 LOCK_PREFIX 的 alternatives 魔法:SMP 内核跑在 UP 上

LOCK_PREFIX 不是一个简单的 "lock; " 字符串,arch/x86/include/asm/alternative.h:39 定义了它的双重身份:

c 复制代码
#ifdef CONFIG_SMP
#define LOCK_PREFIX_HERE \
		".pushsection .smp_locks,\"a\"\n"	\
		".balign 4\n"				\
		".long 671f - .\n" /* offset */		\
		".popsection\n"				\
		"671:"

#define LOCK_PREFIX LOCK_PREFIX_HERE "\n\tlock; "
#else
#define LOCK_PREFIX_HERE ""
#define LOCK_PREFIX ""
#endif

它展开成两部分:

  1. .smp_locks 段 :把这条 lock 前缀指令的地址偏移 记进一个纯地址表(:34-36 注释:不含替换指针和 size,表尽量小)。
  2. lock; :实际指令前缀。

用途(注释 :26-32):同一个 SMP 内核,跑在单核 UP 机器上时 ,apply_alternatives() 用 X86_FEATURE_UP 标志,把这堆 lock 前缀全 NOP 掉 ------UP 上没有并发,lock 纯属浪费。而且表可以保留在内存里,hotplug 热插一个新 CPU 进来时再切回 SMP 版本(这在虚拟化环境特别有用)。

Why :这是"一次编译、两套行为"的 alternatives 机制在 lock 前缀上的特化------用最小的表(只存地址、不存替换内容)换 UP/SMP 两个模式的零成本切换。CPU 篇 #13 CPU 特性位篇讲过 static_cpu_has 的 alternatives patching,这里是同一套机制作用在指令前缀上。

4.4 128 位原子:cmpxchg16b

atomic_t 是 32 位、atomic64_t 是 64 位。要原子地改 128 位(比如一个 64 位指针 + 64 位序列号的无锁栈),x86 靠 cmpxchg16b(arch/x86/include/asm/cmpxchg_64.h:35):

c 复制代码
asm volatile(_lock "cmpxchg16b %[ptr]"
	     : [ptr] "+m" (*(_ptr)), "+a" (o.low), "+d" (o.high)
	     : "b" (n.low), "c" (n.high) : "memory");

这条指令用 RDX:RAX(旧值)+ RCX:RBX(新值)四寄存器拼 128 位,与内存里的 128 位比较交换。前提是 CPU 支持 CX16 特性 (system_has_cmpxchg128() = boot_cpu_has(X86_FEATURE_CX16),cmpxchg_64.h:88)。

4.5 陷阱:atomic_read/atomic_set 不是"带序的原子"

这是最容易踩的坑。atomic.h:17/26:

c 复制代码
static __always_inline int arch_atomic_read(const atomic_t *v)
{
	return __READ_ONCE((v)->counter);   // :23
}
static __always_inline void arch_atomic_set(atomic_t *v, int i)
{
	__WRITE_ONCE(v->counter, i);        // :28
}

atomic_read/atomic_set 就是 READ_ONCE/WRITE_ONCE(见 5.3)------保证一次访问不被撕裂,但完全不提供排序 。多核下 atomic_set(v, 1) 之后,别的 CPU 不保证马上看到 1,也不保证看到 1 之前已看到你更早写的其它变量。

真正带全屏障的是 RMW 操作 (add/inc/cmpxchg 等),因为 x86 的 LOCK 前缀指令本身就是序列化点。这也正是 barrier.h:78-80 写空实现的原因:

c 复制代码
/* Atomic operations are already serializing on x86 */
#define __smp_mb__before_atomic()	do { } while (0)
#define __smp_mb__after_atomic()	do { } while (0)

结论 :atomic_inc 前不用再加 smp_mb()(LOCK 指令自带全屏障);但 atomic_read 前后的顺序必须自己用屏障管。


五、内存屏障分类与 x86 实现

5.1 两层屏障:编译器 vs CPU

内存屏障必须先分两层,混淆这两层是新手最常犯的错:

层 防谁 x86 实现 代价
编译器屏障 barrier() 编译器重排 "memory" clobber(compiler.h:85) 几乎零
CPU 屏障 mb()/smp_mb() CPU 硬件重排 mfence / lock; addl 有(几十~上百 cycle)

所有 CPU 屏障都隐含 编译器屏障(因为汇编带 "memory" clobber),但反过来 barrier() 对 CPU 零作用。

5.2 完整家族对照表

arch/x86/include/asm/barrier.h 定义了 x86 的 __ 底层版本,include/asm-generic/barrier.h 再包一层 kcsan 检测。x86 下的最终效果:

宏 x86 展开 管什么 备注
barrier() "" : "memory" 编译器 compiler.h:85
mb()/rmb()/wmb() mfence/lfence/sfence 全部访存/读/写 公开宏 asm-generic:30/34/38(kcsan + 底层 __mb()),底层 __mb()=mfence arch barrier.h:22
dma_rmb()/dma_wmb() barrier() 设备 DMA 访存顺序 barrier.h:54-55,cache coherent 下只需编译器屏障
smp_mb() lock; addl $0,-4(%rsp) SMP 全屏障 barrier.h:57
smp_rmb() barrier() SMP 读屏障 barrier.h:59(经 dma_rmb)
smp_wmb() barrier() SMP 写屏障 barrier.h:60
smp_store_mb() xchg 存 + 全屏障 barrier.h:61
smp_load_acquire() READ_ONCE + barrier() acquire 读 barrier.h:70
smp_store_release() barrier() + WRITE_ONCE release 写 barrier.h:63
smp_mb__before/after_atomic() 空 原子操作前后 barrier.h:79-80

三条关键规律:

  1. 非 SMP 版(mb/rmb/wmb)用真 fence,SMP 版(smp_*)反而更省 。mb() 用 mfence 是因为它还要对付设备 (UP 单核上跟设备 DMA 的可见性也要保证,见 barrier.h:8-11 注释);而 smp_wmb/rmb 只在多核间排序,TSO 下只需编译器屏障。
  2. smp_mb() 是唯一需要真指令的 SMP 屏障,因为 TSO 只留了 StoreLoad 这个漏洞。
  3. UP 系统上所有 smp_* 屏障退化成 barrier() (asm-generic/barrier.h:110-124)------单核自洽,多核排序无意义。

5.3 READ_ONCE / WRITE_ONCE:不是屏障,是"访问纪律"

include/asm-generic/rwonce.h:44/53 的实现只有一行强转:

c 复制代码
#define __READ_ONCE(x)	(*(const volatile __unqual_scalar_typeof(x) *)&(x))   // :44
#define __WRITE_ONCE(x, val)	*(volatile typeof(x) *)&(x) = (val);          // :53

本质是把一次访问强转成 volatile。它干三件事(rwonce.h 文件头注释 :3-18):

  1. 禁止编译器合并/重取 :不许把两次 READ_ONCE(x) 优化成读一次,也不许把 WRITE_ONCE 的多次写合并。
  2. 禁止跨它重排(仅当编译器"意识到顺序"时,即两个访问在不同 C 语句)。
  3. 保证不被撕裂 (配合 compiletime_assert_rwonce_type,:35)。

它管的是编译器,不是 CPU 。READ_ONCE 不产生任何 CPU 指令,CPU 该乱序还乱序。但它是最基础的"读一次、别缓存"纪律,是 RCU、无锁结构里每一个共享变量访问的前置要求。

5.4 acquire / release:x86 上"免费的午餐"

C11 内存模型和内核的 smp_load_acquire/smp_store_release 表达的是单向屏障:

  • smp_load_acquire(p):读 p 之后的所有访存不许跑到这个读之前。
  • smp_store_release(p, v):写 p 之前的所有访存不许跑到这个写之后。

通用实现(asm-generic/barrier.h:138/147)要在前后各插一个 smp_mb()。但 x86 override 掉了,只留 barrier()(barrier.h:63-76):

c 复制代码
#define __smp_store_release(p, v)	do { compiletime_assert_atomic_type(*p); \
					barrier(); WRITE_ONCE(*p, v); } while (0)   // :63
#define __smp_load_acquire(p)	({ typeof(*p) ___p1 = READ_ONCE(*p);	\
				   compiletime_assert_atomic_type(*p); barrier(); ___p1; })  // :70

Why 免费 :TSO 已经保证"读之后的访问不提前""写之前的访问不推后",acquire/release 要防的那两种重排(LoadLoad/LoadStore、StoreStore/LoadStore 的跨边界移动)在 x86 硬件上本来就不发生,所以只需挡住编译器即可。这是强内存模型(x86 TSO)相比弱模型(ARM)的最大红利 ------ARM 上 smp_store_release 必须是一条真正的 stlr 指令。

5.5 为什么 smp_mb 用 lock; addl $0,-4(%rsp) 而不是 mfence

barrier.h:57:

c 复制代码
#define __smp_mb()	asm volatile("lock; addl $0,-4(%%" _ASM_SP ")" ::: "memory", "cc")

这条指令:lock 前缀 + 把 0 加到栈顶下方 4 字节 (-4(%rsp)),结果不变、纯属占位。为什么选它当全屏障?

  • lock 前缀指令天然是全屏障(序列化点),能堵住 StoreLoad 重排------这是我们要的唯一效果。
  • 操作数落在栈上(每 CPU 私有的内存位置) :不会和其他 CPU 竞争 cache line,代价最低。如果 lock; addl 操作一个共享变量,反而会引发 cache line 抢占。
  • mfence 也能当全屏障,但通常更慢 :mfence 要等待所有之前的 store 刷出,lock 空操作在某些微架构上更便宜。所以内核选 lock; addl $0 这个"最便宜的序列化点"当 smp_mb()。

顺带:32 位代码里 mb()/rmb()/wmb() 用 ALTERNATIVE("lock; addl $0,-4(%esp)", "mfence", X86_FEATURE_XMM2)(barrier.h:15-20)------有 XMM2(SSE2)就用 mfence,老 CPU 退回 lock 空操作。64 位直接固定用 fence(barrier.h:22-24)。


六、应用实例:屏障怎么拼成同步原语

前面几章讲的是"零件"(第四、五章的原子操作 + 内存屏障),这一章看内核怎么把它们拼成三个真实同步原语,正好呼应 #3(死锁)里的三个主角。

6.1 自旋锁 qspinlock:acquire 抢锁 + release 解锁

x86 的自旋锁是 queued spinlock(MCS 队列锁) ,不是老式 ticket lock(spinlock.h:19 的注释已经过时,实际 include 的是 qspinlock.h:27)。include/asm-generic/qspinlock.h:107 的快速路径:

c 复制代码
static __always_inline void queued_spin_lock(struct qspinlock *lock)
{
	int val = 0;
	if (likely(atomic_try_cmpxchg_acquire(&lock->val, &val, _Q_LOCKED_VAL)))
		return;                          // 抢到锁,直接进临界区
	queued_spin_lock_slowpath(lock, val);  // 被占了,进 MCS 队列排队
}

解锁(qspinlock.h:123):

c 复制代码
static __always_inline void queued_spin_unlock(struct qspinlock *lock)
{
	smp_store_release(&lock->locked, 0);  // :128 ------ release 语义解锁
}

两个屏障在这里各司其职:

  • atomic_try_cmpxchg_acquire :cmpxchg 的 LOCK 前缀保证"抢锁"这个比较交换是原子的(只有一个 CPU 能成功),acquire 语义保证抢到锁之后,临界区里读到的共享数据一定是最新的(不会被提前到抢锁之前读)。
  • smp_store_release :release 语义保证解锁之前,临界区里改的所有数据都已经全局可见(不会被推迟到解锁之后写)。

一进一出,正好用 acquire/release 把"临界区内的访存"夹在中间,这就是"锁 = 临界区的内存屏障"的精确含义。

6.2 RCU:release 发布 + READ_ONCE 读取

RCU 的"读-拷贝-更新"全靠内存屏障保证读者要么看到旧指针、要么看到新指针,绝看不到半成品。include/linux/rcupdate.h:

c 复制代码
#define rcu_assign_pointer(p, v)	\
do {					\
	...				\
	smp_store_release(&p, RCU_INITIALIZER((typeof(p))_r_a_p__v));  // :502
} while (0)

#define rcu_dereference(p) rcu_dereference_check(p, 0)   // :658
  • 发布端 rcu_assign_pointer = smp_store_release (rcupdate.h:502):release 语义保证"初始化新对象"的写,一定在"发布新指针"这个写之前全局可见。读者一旦读到新指针,就能保证新对象已初始化完。
  • 读取端 rcu_dereference :底层是 READ_ONCE(读一次、不缓存),配合 acquire/数据依赖,保证读到指针之后,对指针指向对象的访问不会乱序。

这正是 #3 里 cpu_hotplug_lock(percpu-rwsem)的底层:它的读者快速路径判据 rcu_sync_is_idle,就是靠 RCU 的这套 release/acquire 机制,让读者几乎无锁地判断"现在有没有写者在等"。

6.3 percpu-rwsem:读多写少,用 percpu 计数器 + rcu_sync

#3 详细讲过 cpu_hotplug_lock 是 percpu-rwsem。这里只从"原子 + 屏障"的角度补一句它的组合方式:

  • 读侧 :每个读者 this_cpu_inc 一个 percpu 计数器(CPU 篇 #8 percpu 的 %gs 段寻址,一条指令、无锁),写者要看"有没有读者"时把这些 percpu 计数求和。读路径零屏障、零竞争。
  • 写侧 :写者要用 rcu_sync(本质是 RCU 宽限期)等所有在读的读者退出 ,再独占。这就是 smp_mb() 全屏障 + RCU 宽限期联手保证的。

三种原语的选型逻辑已经浮现:抢锁看竞争激烈度(qspinlock 用 CAS + 队列)、读多写少用 percpu(无锁读)、指针发布用 RCU(release/acquire + 宽限期)。这正是下一章的选型表。


七、对比与选型

7.1 x86(TSO,强模型) vs ARM(弱模型):屏障的代价差

这是面试最爱问的"内存模型"题。核心差异:

维度 x86 (TSO) ARM (弱模型)
允许的重排 只 StoreLoad 一种 四种都可能
smp_wmb()/smp_rmb() barrier()(几乎免费) 真 fence 指令(dmb)
smp_load_acquire/store_release READ_ONCE+barrier() 专用 ldar/stlr 指令
smp_mb() lock; addl / mfence dmb ish(全屏障,重)
移植风险 代码在 x86 上"碰巧对",换 ARM 就炸 屏障少写了在弱模型上必然出 bug

关键结论 :在 x86 上靠"忘记加屏障"侥幸跑对的代码,移植到 ARM 会静默出错 ------因为 ARM 需要屏障的地方比 x86 多得多。这也是为什么内核用 smp_* 屏障(而非裸 barrier())表达意图:同一份代码,在强/弱模型上各展开成正确的实现。写内核同步代码时,语义(acquire/release/full)比具体指令重要。

7.2 什么时候用原子操作 vs 锁 vs RCU

场景 用什么 原因
单个计数器增减 atomic_inc/dec 一条 LOCK 指令,最轻
短临界区、竞争中等 自旋锁 qspinlock 简单、公平(MCS 队列)
读极多、写极少 percpu 变量 / percpu-rwsem / RCU 读路径零锁零竞争
指针发布、链表遍历 RCU 读者永不阻塞,写者延迟回收
读改写一个值、要旧值 atomic_cmpxchg/atomic_xchg 一条指令完成,免锁
无锁栈/队列(含 128 位) cmpxchg16b 的 CAS 循环 需要指针+序号整体原子

判断口诀 :先看访问模式------纯计数用 atomic;短临界区用锁;读多写少用 RCU/percpu;需要"整体快照"用 CAS。

7.3 一张表收尾:重排 → 屏障 → x86 代价

要防的重排 内核宏 x86 指令 代价
编译器任意重排 barrier() "" : "memory" 零
LoadLoad / LoadStore smp_rmb() / 数据依赖 barrier() 零(TSO 已保证)
StoreStore smp_wmb() barrier() 零(TSO 已保证)
StoreLoad smp_mb() lock; addl $0 / mfence 有(唯一真花钱的)
acquire 读 smp_load_acquire READ_ONCE+barrier() 零
release 写 smp_store_release barrier()+WRITE_ONCE 零

八、记住三点

  1. 两本账分开算 :原子性(LOCK 前缀锁 cache line,接 MESI)解决"中间态",内存屏障(防编译器/CPU/store buffer 三层乱序)解决"顺序"。atomic_read/atomic_set 只有原子性、没有顺序。
  2. x86 是 TSO 强模型,只有 StoreLoad 会乱 ,根因是每核的 store buffer。所以 smp_wmb()/smp_rmb()/acquire/release 在 x86 上都是免费的 barrier(),只有 smp_mb() 要真指令 (lock; addl $0)。
  3. 同步原语 = 原子操作 + 屏障的固定组合 :qspinlock 用 cmpxchg_acquire 抢锁 + store_release 解锁;RCU 用 store_release 发布 + READ_ONCE 读取;读懂这三层,#3 死锁里的锁序、CPU 篇 #8 percpu 的无锁读、CPU 篇 #14 MESI 的缓存乒乓就全部打通了。
相关推荐
小冯不疯1 小时前
直播切片带货分账:3 类分成模型 × 4 道结算节点 × 2 类税务坑
1024程序员节
我是大猴子1 小时前
py how claude work项目分析
运维·服务器·windows·1024程序员节
Bug收容所1 小时前
Vue+JS Day3
前端·javascript·vue.js·前端框架·1024程序员节
码完就睡1 小时前
Linux——IP协议与路由选择
linux·网络·tcp/ip·1024程序员节
H.莓飛1 小时前
【数据结构】排序_OJ题(二):划分与选择
linux·服务器·c语言·开发语言·数据结构·visual studio
李纲明1 小时前
WordPress 挂上 Cloudflare 后询盘和后台异常:缓存规则、SSL 模式和真实 IP 怎么核
运维·缓存·https·cdn·wordpress·cloudflare
远航计算机1 小时前
收录从分钟级变 3 到 7 天,抢首发为什么失效了
大数据·人工智能·爬虫·chatgpt·aigc·1024程序员节
iDao技术魔方1 小时前
Bun v1.4.4 实战:用 bun fmt 替掉 prettier 的 5 个步骤
typescript·1024程序员节
箓维1 小时前
UDP Socket的实现
网络·1024程序员节