2. 原子操作与内存屏障:GCC __sync与__atomic内置函数、CAS操作详解、内存序模型
在DPDK无锁数据结构中,原子操作与内存屏障是构建正确性的基石。本章深入剖析GCC内置原子函数、CAS(Compare-And-Swap)操作以及内存序模型,帮助你在工程选型中做出精准决策。
核心要点: 原子操作保证操作的不可分割性,内存屏障控制多核间的可见性与顺序性。理解这两者,是编写正确无锁代码的前提。
2.1 GCC __sync与__atomic内置函数
GCC提供了两代原子内置函数:__sync(旧版)和 __atomic(C++11标准对齐版)。DPDK早期代码大量使用 __sync,新代码逐步迁移至 __atomic。
2.1.1 __sync 系列(旧版)
定义在 <bits/atomicity.h> 或通过 __sync 前缀直接使用。常见操作:
__sync_fetch_and_add(ptr, value):原子加,返回旧值。__sync_fetch_and_sub(ptr, value):原子减,返回旧值。__sync_bool_compare_and_swap(ptr, old, new):CAS,成功返回true。__sync_val_compare_and_swap(ptr, old, new):CAS,返回旧值。__sync_synchronize():全屏障(相当于__atomic_thread_fence(__ATOMIC_SEQ_CST))。
注意:
__sync 系列默认隐含全屏障(Sequential Consistency),性能开销较大。在DPDK高频路径中,过度使用会导致性能下降。
2.1.2 __atomic 系列(新版)
从GCC 4.7开始支持,提供更细粒度的内存序控制。核心函数:
__atomic_load_n(ptr, memorder)__atomic_store_n(ptr, val, memorder)__atomic_fetch_add(ptr, val, memorder)__atomic_compare_exchange_n(ptr, expected, desired, weak, success_memorder, failure_memorder)__atomic_thread_fence(memorder)
| 特性 | __sync 旧版 | __atomic 新版 |
|---|---|---|
| 内存序控制 | 固定全屏障(SEQ_CST) | 支持6种内存序 |
| CAS接口 | bool/val 两种 | 支持weak/strong、分离成功/失败内存序 |
| 性能优化空间 | 低(强制屏障) | 高(可降级为relaxed) |
| DPDK使用现状 | 遗留代码 | 新代码推荐 |
// __sync 示例:原子递增
static inline uint32_t
sync_atomic_inc(uint32_t *ptr) {
return __sync_fetch_and_add(ptr, 1);
}
// __atomic 示例:带内存序的原子递增
static inline uint32_t
atomic_inc_relaxed(uint32_t *ptr) {
return __atomic_fetch_add(ptr, 1, __ATOMIC_RELAXED);
}
// __atomic 示例:带内存序的CAS
static inline int
atomic_cas_strong(uint64_t *ptr, uint64_t *expected, uint64_t desired) {
return __atomic_compare_exchange_n(ptr, expected, desired,
0, // weak=0 表示strong版本
__ATOMIC_ACQUIRE,
__ATOMIC_RELAXED);
}
2.2 CAS操作详解
CAS(Compare-And-Swap)是无锁数据结构的核心原语。它原子地比较内存位置的值与期望值,若相等则更新为新值。
2.2.1 CAS的ABA问题
经典陷阱:线程1读取值A,被线程2改为B又改回A,线程1的CAS成功但实际数据已变化。DPDK中常用两种方案解决:
- 双宽CAS(DCAS):使用128位CAS,高64位存版本号。
- Hazard Pointer(危险指针):延迟回收,避免ABA。
DPDK实践: DPDK的ring库使用32位无符号整数作为生产/消费指针,通过2的幂次大小和模运算天然避免ABA(指针回绕时版本号隐含在高低位中)。
2.2.2 CAS的weak与strong版本
__atomic_compare_exchange_n 提供weak和strong两种版本:
- weak版本:可能虚假失败(spurious fail),但性能更高,适合循环CAS(LL/SC风格)。
- strong版本:保证成功时一定更新,失败时一定不更新,适合条件分支。
// weak版本:循环直到成功
uint64_t expected = *ptr;
uint64_t desired = expected + 1;
while (!__atomic_compare_exchange_n(ptr, &expected, desired,
1, // weak
__ATOMIC_ACQUIRE,
__ATOMIC_RELAXED)) {
desired = expected + 1; // 重新计算
}
// strong版本:单次尝试
if (__atomic_compare_exchange_n(ptr, &expected, desired,
0, // strong
__ATOMIC_ACQUIRE,
__ATOMIC_RELAXED)) {
// 成功处理
} else {
// 失败处理
}
2.3 内存序模型
内存序(Memory Order)定义了原子操作周围的内存访问如何被排序和可见。C++11/GCC定义了6种内存序,从弱到强:
| 内存序 | 含义 | 性能开销 | DPDK典型用途 |
|---|---|---|---|
__ATOMIC_RELAXED |
无顺序约束,仅保证原子性 | 最低 | 统计计数器、单生产者场景 |
__ATOMIC_CONSUME |
数据依赖顺序(当前GCC实现较弱) | 低 | 很少使用,推荐用acquire替代 |
__ATOMIC_ACQUIRE |
后续读写不能重排到该操作之前 | 中 | 锁获取、读取指针 |
__ATOMIC_RELEASE |
之前读写不能重排到该操作之后 | 中 | 锁释放、写入指针 |
__ATOMIC_ACQ_REL |
acquire + release 组合 | 较高 | CAS操作(读-改-写) |
__ATOMIC_SEQ_CST |
全局顺序一致(最强) | 最高 | 默认值,但DPDK中应避免滥用 |
DPDK选型建议: 在无锁队列中,生产者写入数据使用
__ATOMIC_RELEASE,消费者读取数据使用 __ATOMIC_ACQUIRE,即可保证正确的happens-before关系,无需使用全屏障。
2.3.1 内存屏障指令
除了原子操作附带的内存序,GCC还提供显式屏障:
__atomic_thread_fence(memorder):线程间屏障。__atomic_signal_fence(memorder):信号处理器屏障(DPDK中极少用)。
// 使用显式屏障实现生产者-消费者同步
// 生产者
data[0] = 42;
data[1] = 100;
__atomic_thread_fence(__ATOMIC_RELEASE);
__atomic_store_n(&flag, 1, __ATOMIC_RELAXED);
// 消费者
while (__atomic_load_n(&flag, __ATOMIC_RELAXED) == 0);
__atomic_thread_fence(__ATOMIC_ACQUIRE);
int val0 = data[0]; // 保证看到42
int val1 = data[1]; // 保证看到100
2.4 内存序模型与DPDK无锁结构
下图展示了DPDK无锁队列(rte_ring)中内存序的应用:
2.5 工程选型对比
| 场景 | 推荐原子函数 | 推荐内存序 | 理由 |
|---|---|---|---|
| 统计计数器(多核累加) | __atomic_fetch_add |
__ATOMIC_RELAXED |
仅需原子性,无需顺序约束 |
| 无锁栈/队列(单生产者) | __atomic_store_n / __atomic_load_n |
RELEASE / ACQUIRE | 保证数据可见性 |
| 无锁栈/队列(多生产者) | __atomic_compare_exchange_n |
ACQ_REL | CAS需要读-改-写原子性 |
| 引用计数 | __atomic_fetch_add / __atomic_fetch_sub |
__ATOMIC_RELAXED + 释放时ACQ_REL |
计数本身relaxed,最后释放需同步 |
| DPDK rte_ring | __atomic_load_n / __atomic_store_n / CAS |
混合(见上图) | 经过DPDK团队极致优化 |
性能调优提示: 在DPDK数据面,优先使用
__ATOMIC_RELAXED 或 __ATOMIC_ACQUIRE/__ATOMIC_RELEASE,避免 __ATOMIC_SEQ_CST。使用perf工具观察 sync_synchronize 开销,若占比超过5%,考虑降级内存序。
2.6 常见陷阱与避坑指南
- 陷阱1:混合使用__sync和__atomic —— 同一变量不要混用两套API,可能导致未定义行为。
- 陷阱2:过度使用全屏障 ——
__sync_synchronize或__ATOMIC_SEQ_CST会触发内存屏障指令(如mfence),在x86上开销约20-40 cycles,在ARM上更高。 - 陷阱3:忽略编译器重排 —— 即使使用
__ATOMIC_RELAXED,编译器也可能重排普通内存访问,必要时使用volatile或asm volatile("" ::: "memory")。 - 陷阱4:CAS循环中的ABA —— 务必使用版本号或DCAS,DPDK的rte_ring通过指针回绕设计规避了此问题。
// 错误示例:混合使用两套API
uint32_t val = __sync_fetch_and_add(&counter, 1); // 旧版
__atomic_store_n(&counter, 0, __ATOMIC_RELAXED); // 新版 — 未定义行为!
// 正确示例:统一使用__atomic
uint32_t val = __atomic_fetch_add(&counter, 1, __ATOMIC_RELAXED);
__atomic_store_n(&counter, 0, __ATOMIC_RELAXED);
平台差异: x86架构由于强内存模型(TSO),
__ATOMIC_ACQUIRE/__ATOMIC_RELEASE 在x86上通常不生成额外屏障指令(仅阻止编译器重排)。ARM/PowerPC等弱内存模型架构会生成dmb/isync等指令,性能差异显著。DPDK跨平台代码必须显式指定内存序,不能依赖x86的“幸运”行为。