atomic_inc(&counter) 背后,不同 CPU
架构的实现天差地别。理解硬件原子原语,才能写出高性能并发代码。
一、先看图
flowchart LR
subgraph x86
LOCK[LOCK prefix<br/>锁总线/缓存行] --> MESI[MESI 协议<br/>保证一致性]
end
subgraph ARMv8_early["ARM LL/SC"]
LDXR[LDXR 加载独占] --> STXR[STXR 存储独占<br/>可能失败 → 重试]
end
subgraph ARMv8_1["ARM LSE"]
LSE_ADD[LDADD<br/>硬件原子加]
end
subgraph RISCV["RISC-V A"]
AMO[AMOADD<br/>原子加]
LR_SC[LR/SC<br/>类似 LL/SC]
end
classDef x86c fill:#388bfd22,stroke:#388bfd,color:#adbac7;
classDef armc fill:#3fb95022,stroke:#3fb950,color:#adbac7;
classDef rvc fill:#a371f722,stroke:#a371f7,color:#adbac7;
class LOCK,MESI x86c
class LDXR,STXR,LSE_ADD armc
class AMO,LR_SC rvc
二、x86:LOCK 前缀
lock add [counter], 1 ; 原子加
lock cmpxchg [ptr], rax ; CAS
lock xadd [counter], rax ; fetch_addLOCK 前缀让 CPU 独占缓存行 → MESI E/M 状态 → 其他核的读必须等。
成本:
- 同一缓存行无竞争 → ~10-20 cycles
- 竞争激烈 → 100+ cycles(cache line bouncing)
三、ARM LL/SC
retry:
ldxr x0, [x1] // Load-Exclusive
add x0, x0, #1
stxr w2, x0, [x1] // Store-Exclusive
cbnz w2, retry // 失败则重试独占监视器(exclusive monitor)跟踪 cacheline。其他核写同一行 → stxr 失败 → 重试。
问题:高竞争 → 无限重试(livelock 风险)。
四、ARMv8.1 LSE
大型原子扩展(Large System Extensions):
ldadd x0, x1, [x2] // 硬件原子 fetch_add
cas x0, x1, [x2] // 硬件 CAS
swp x0, x1, [x2] // 硬件 swap硬件保证原子性,无重试循环。
性能:64 核以上 → LSE 比 LL/SC 快 10 倍以上。
内核检测 LSE → 运行时选择 LL/SC 或 LSE
路径(ALTERNATIVE 宏)。
五、RISC-V A 扩展
两种方式并存:
amoadd.w a0, a1, (a2) // 原子加
lr.w a0, (a1) // Load-Reserved
sc.w a2, a0, (a1) // Store-Conditional类似 ARM 的两代方案。
六、内核 atomic API
atomic_t counter = ATOMIC_INIT(0);
atomic_inc(&counter); // ++
atomic_dec_and_test(&counter); // --,返回是否为 0
int old = atomic_fetch_add(5, &counter); // fetch_add
bool ok = atomic_try_cmpxchg(&counter, &old, new); // CAS底层展开为架构对应的原子指令。
七、Cache Line Bouncing
sequenceDiagram
participant C0 as Core 0
participant C1 as Core 1
participant CL as Cache Line
C0->>CL: atomic_inc → 获取 E 状态
C1->>CL: atomic_inc → 发送 Invalidate
C0->>CL: 降级到 I 状态
C1->>CL: 获取 E 状态 → 执行
C0->>CL: 再次 atomic_inc → 又要抢
高频原子操作在同一 cacheline → 核间不断争抢 → 性能崩溃。
解决:per-CPU 计数 → 只在需要精确值时汇总。
八、Tearing
非原子宽度访问可能被拆成多次总线事务:
long x; // 64 位
// 32 位 CPU 上,读/写可能拆成两个 32 位操作 → tearing内核保证:atomic_long_t、READ_ONCE/WRITE_ONCE
在对齐地址上原子。
九、CAS vs fetch_add
| 操作 | 失败重试 | 竞争时性能 | 用途 |
|---|---|---|---|
| CAS | 需要(compare-and-swap) | 高竞争退化 | 通用 |
| fetch_add | 无需重试 | 稳定 | 计数器、序列号 |
尽可能用 fetch_add 替代 CAS loop。
十、小结
- x86 LOCK 前缀简单高效但竞争时代价高
- ARM LL/SC 灵活但高竞争可能 livelock
- ARM LSE / RISC-V AMO 提供硬件原子操作
- cache line bouncing 是并发性能的头号杀手
- 内核 atomic API 封装了架构差异
参考文献
arch/x86/include/asm/atomic.harch/arm64/include/asm/atomic_lse.hDocumentation/atomic_t.txt- linux/cas-vs-helping(旧文延伸阅读)
- Will Deacon, “ARM64 atomics and LSE.” 2018
工具
perf stat -e cache-missesperf c2c(cache line false sharing 检测)objdump -d(查看原子指令生成)
延伸阅读
上一篇:Linux 内核内存模型 下一篇:spinlock 家族
同主题继续阅读
把当前热点继续串成多页阅读,而不是停在单篇消费。
【操作系统百科】内存回收
Linux 内存回收是 VM 最复杂的子系统之一。本文讲 active/inactive LRU、kswapd 与 direct reclaim、watermark 三线、swappiness 的真实含义、MGLRU 改造、memcg 回收与 PSI。
【操作系统百科】交换
swap 还值得开吗?本文讲 swap area 基础、swap cache、zram 压缩内存、zswap 前端压缩池、swappiness 的真实含义、容器里的 swap 策略,以及为什么现代 Android 全靠 zram 不靠磁盘。
【操作系统百科】Slab/SLUB 分配器
buddy 只管页粒度(4K+),内核大多数对象只有几十到几百字节。slab/SLUB 在 buddy 之上做对象级缓存。本文讲 slab 历史、SLUB 接手、SLOB 退场、kmem_cache、per-CPU cache、KASAN 集成。
【操作系统百科】用户态分配器
glibc malloc、tcmalloc、jemalloc、mimalloc 各有哲学。本文讲 arena、thread cache、size class、madvise 返还策略、碎片与 RSS 膨胀、如何根据负载选分配器。