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)中内存序的应用:

DPDK rte_ring 内存序模型 生产者核心 写数据 + RELEASE 消费者核心 读数据 + ACQUIRE 环形缓冲区 (Ring Buffer) 槽0 槽1 槽2 槽3 prod_head prod_tail cons_head cons_tail enqueue dequeue 内存序策略: 生产者写槽数据使用RELEASE,写prod_tail使用RELEASE;消费者读槽数据使用ACQUIRE,读prod_tail使用ACQUIRE。

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的“幸运”行为。