Lazy loaded image
🥳嵌入式Linux开发
⚛️Linux 内核原子操作详解:CAS 与 Test-and-Set
Words 3495Read Time 9 min
2026-8-21
2026-9-3
原子操作(Atomic Operations)是 Linux 内核同步机制的基石。它们保证指令执行的不可分割性,即在执行过程中不会被中断或被其他处理器干扰。原子操作是构建自旋锁、互斥量等高级同步原语的基础,同时也是无锁编程(Lock-free programming)的核心。
本文详细解析 Linux 内核提供的三类主要原子操作:原子整数、原子位操作(Test-and-Set)以及通用的 CAS(Compare and Swap)。

1. 原子整数操作 (Atomic Integer Operations)

内核定义了 atomic_t(32位)和 atomic64_t(64位)类型,专门用于整数的原子更新。这些操作常用于实现计数器(如引用计数)。
核心数据结构
使用结构体而非直接使用 int,是为了确保开发者只能通过专用 API 操作它,防止被普通算术指令错误修改。
常用 API
操作
函数接口
描述
:---
:---
:---
读/写
atomic_read(v) atomic_set(v, i)
原子读取/设置值
算术
atomic_add(i, v) atomic_sub(i, v) atomic_inc(v)
原子加/减/自增
测试
atomic_dec_and_test(v)
自减,若结果为 0 返回 true(常用于对象释放)
返回
atomic_inc_return(v)
自增并返回新值
最佳实践注:对于引用计数,现代内核推荐使用 refcount_t 系列接口(如 refcount_set, refcount_inc),它们能检测并防止溢出攻击(Use-after-free)。

2. 原子位操作与 Test-and-Set

Linux 内核提供了一组直接操作内存地址中特定位的函数,定义在 <asm/bitops.h>。这类操作不需要特殊的 atomic_t 类型,直接作用于 void * 指针和位偏移。
Test-and-Set 机制
用户关注的 Test-and-Set 是构建“自旋锁”或“标志位”的基础。它原子地执行“读取旧值”和“设置新值”两个动作。
  • test_and_set_bit(nr, addr)
    • 功能:原子地将地址 addr 的第 nr 位置为 1
    • 返回值:返回该位在修改前的值(0 或 1)。
    • 应用:如果返回 0,说明当前线程抢占成功(获得了锁/所有权);如果返回 1,说明已被占用,线程需要等待或重试。
其他位操作
  • set_bit(nr, addr): 仅设置位,不返回值。
  • clear_bit(nr, addr): 清除位。
  • test_and_clear_bit(nr, addr): 清除位并返回旧值(Test-and-Clear)。
  • change_bit(nr, addr): 翻转位。

3. CAS (Compare and Swap)

CAS 是无锁编程中最通用的原语。Linux 内核通过 cmpxchg 宏实现这一功能。
核心宏:cmpxchg
  • 逻辑:检查 ptr 指向的值。如果它等于 old,则将其原子地更新为 new;否则保持不变。
  • 返回值:总是返回 ptr 在操作的值。
  • 判断成功:调用者通过判断 返回值 == old 来确认更新是否成功。
典型应用模式
CAS 通常配合循环使用,实现无锁更新(Lock-free update):

4. 64位与通用长整型原子操作

随着 64 位架构的普及,内核引入了 atomic64_tatomic_long_t
  • atomic64_t:在 64 位系统上提供 64 位宽度的原子操作。接口与 atomic_t 类似,前缀为 atomic64_(如 atomic64_add)。
  • atomic_long_t:根据系统架构自动适配。在 32 位系统上等同于 atomic_t,在 64 位系统上等同于 atomic64_t。这在需要处理与指针长度或 long 类型一致的数据时非常有用。

5. 现代 CAS 变体:try_cmpxchg

在较新的内核版本中,推荐使用 try_cmpxchg 替代传统的 cmpxchg 循环。
  • 优势cmpxchg 失败时通常需要重新读取旧值,而 try_cmpxchg 会在失败时自动将当前值更新到 old 指针中,生成的汇编代码更高效(减少了寄存器溢出和不必要的内存读取)。

6. 本地原子操作 (local_t)

如果数据仅由单个 CPU 访问(例如 Per-CPU 变量),使用全局原子操作(带总线锁)开销过大。
  • local_t:定义在 <asm/local.h>
  • 特性:保证在当前 CPU 上的原子性(也就是对于中断上下文是安全的),但不保证跨 CPU 的原子性。
  • 场景:用于实现 Per-CPU 计数器等,性能远高于标准的 atomic_t

7. 内存屏障与原子性 (Ordering)

标准的 atomic_readatomic_set 不隐含内存屏障。
  • 不带返回值的操作(如 atomic_inc):通常不隐含内存屏障。如果需要保证顺序,需要显式调用屏障(如 smp_mb__before_atomic())。
  • 带返回值的操作(如 atomic_inc_return):通常隐含完整的内存屏障,保证操作前后的指令不会乱序跨越该操作。
  • Relaxed/Acquire/Release 变体:现代内核还提供了 _relaxed, _acquire, _release 后缀的原子接口,允许开发者根据需要精细控制内存序,以获得极致性能。

7.1 内存屏障到底解决什么问题

内存屏障(Memory Barrier)是用来约束编译器或 CPU 内存访问顺序的同步机制。编译器和 CPU 为了性能,可能重排彼此独立的读写;因此,源代码中的先后顺序,不一定等于其他 CPU 实际观察到的顺序。
内存屏障解决的不是“指令会不会被中断”,而是两个问题:
  • 顺序:保证某些读写不会跨过屏障重新排序。
  • 可见性:保证一个 CPU 发布的数据,按照规定顺序被另一个 CPU 观察到。
需要区分:原子性保证一次操作不可分割;内存序保证多个内存访问之间的先后关系。比如 atomic_inc() 可以保证计数器更新不丢失,但不代表它自动保证“普通数据已经写完,再设置 ready 标志”。
典型场景:先写数据,再发布标志
下面的代码不能简单假设消费者一定先看到完整的 data,再看到 ready
Linux 内核中,通常使用 release/acquire 语义表达这种“发布—订阅”关系:
含义是:消费者一旦通过 smp_load_acquire() 观察到生产者发布的 ready = 1,就必须能够看到 release 之前完成的 data = 42
这类顺序关系常见于:
  1. 生产者—消费者队列。
  1. 无锁链表、无锁队列和 RCU 等并发数据结构。
  1. 中断处理、工作队列与进程上下文之间的状态传递。
  1. 多核 CPU 间共享状态的发布与读取。
  1. 设备驱动中描述符、缓冲区与设备寄存器之间的访问排序。
常用接口
类型
常用接口
作用与注意事项
编译器屏障
barrier()
只阻止编译器重排,不生成 CPU 硬件屏障;不能单独解决多核同步。
完整屏障
mb()smp_mb()
约束屏障前后的读写顺序;smp_ 版本主要用于 CPU 间共享内存。
读屏障
rmb()smp_rmb()
约束读操作之间的顺序。
写屏障
wmb()smp_wmb()
约束写操作之间的顺序。
获取语义
smp_load_acquire()、带 _acquire 后缀的原子接口
常用于消费者:读取状态后,再读取关联数据。
释放语义
smp_store_release()、带 _release 后缀的原子接口
常用于生产者:写完关联数据后,再发布状态。
Relaxed 原子操作
_relaxed 后缀的原子接口
只要求原子性,不建立跨变量的访问顺序;不能用于需要同步关联数据的场景。
防止编译器误优化
READ_ONCE()WRITE_ONCE()
防止普通访问被合并、拆分或重复;它们本身不是完整的 acquire/release 屏障。
原子操作前后
smp_mb__before_atomic()smp_mb__after_atomic()
在需要把普通内存访问与某个原子操作建立顺序时使用。
接口选择原则
  • 只需要一个计数器的原子读-改-写:使用 atomic_t 或更合适的专用类型。
  • 需要“先写数据、后发布标志”:优先使用 smp_store_release()smp_load_acquire()
  • 只需要防止编译器把一次访问优化掉或重复加载:使用 READ_ONCE()/WRITE_ONCE(),但不要把它们当成内存屏障。
  • 需要保护一整个临界区:优先使用 spin_lock()/spin_unlock()mutex_lock()/mutex_unlock()。锁已经封装了排他访问和相应的内存序,不要手工拼接“状态位 + 屏障”来模拟锁。
  • 访问内存映射设备寄存器时,要遵循驱动 API 和架构相关的 I/O 排序要求;普通 smp_* 共享内存屏障不能机械替代 I/O 屏障。
最后特别纠正一个常见经验误区:不能仅凭“原子操作带返回值”就断定它一定提供完整内存屏障。具体内存序应以该接口的文档和 _relaxed/_acquire/_release 变体为准。
一句话总结:原子操作保证“这一次更新不可分割”,内存屏障保证“相关访问按正确顺序被观察到”,锁则封装了排他访问和必要的内存序。

8. 原子引用计数:接口差异、选型与硬件底层

原子引用计数:接口差异与选择
原子引用计数:接口差异与选择
引用计数是原子整数最典型的应用场景。本节把内核态与用户态的接口差异、选型逻辑和硬件原子指令底层串成一条线。
接口差异对比:内核态 vs 用户态
世界
接口
关键语义
说明
内核态
atomic_t
atomic_inc() / atomic_dec_and_test()
老接口,无防溢出/下溢保护
内核态
refcount_t(4.11+)
refcount_inc() / refcount_dec_and_test()
饱和计数(REFCOUNT_SATURATED = UINT_MAX/2),计数为 0 或饱和时 WARN,防溢出/下溢被利用
内核态
kref
kref_get() / kref_put()
"引用计数 + 释放回调"封装,kref_put 内部就是 refcount_dec_and_test,归零时调用 release 回调(DMA-BUF 计数即此层)
用户态
std::atomic
fetch_add() / fetch_sub()
C++11 标准,可控制内存序,首选
用户态
__atomic_* builtins
__atomic_fetch_add / __atomic_sub_fetch
GCC 4.7+,支持内存序,老代码/不用 C++11 时用
用户态
__sync_* builtins
__sync_fetch_and_add
更老,仅全屏障语义,无内存序控制
用户态
shared_ptr / intrusive_ptr
库自动管理
生命周期管理直接用库,别重复造轮子
如何选择
  • 写内核代码:新代码用 refcount_t(或 kref 封装);老代码/内核 API 约束用 atomic_t
  • 用户态 C++:
    • 只是计数器/统计/水位 → std::atomic&lt;unsigned&gt;relaxed 就够
    • 管理对象生命周期、要释放回调 → std::shared_ptr
    • 侵入式计数/对象池/跨模块传裸指针 → boost::intrusive_ptr 或手写原子计数
    • 嵌入式、避免 shared_ptr 的堆分配和 RTTI 开销 → 手写 std::atomic&lt;unsigned&gt; + 对象池
    • 老工具链(无 C++11)→ __atomic_*__sync_*
手写引用计数的标准模板(对应内核 dec_and_test)
⚠️
归零判断 + 释放必须是同一个原子操作fetch_sub 返回的是旧值,旧值 == 1 说明减完正好到 0,只有这一个线程执行销毁。如果先减、再单独 load() 判断,两个线程可能同时读到 0 都执行 release → double-free。
硬件原子指令底层
原子接口最终都落到 CPU 指令:
  • x86lock 前缀 + xadd/inc,单条指令完成读-改-写,通过总线锁/缓存锁保证原子。
  • ARMLDREX/STREX 独占访问对,STREX 失败则重试;ARMv8.1 LSE 提供 LDADD 等单指令原子操作。
  • 内存序:发布(写数据后递增计数)用 release,释放(递减计数后销毁)用 acquire/acq_rel,保证"持有者写入的数据在对象销毁前对销毁方可见"。纯计数场景用 relaxed 即可,别滥用 seq_cst。
设计层面的更高阶答案
好的设计是绕开共享引用计数:固定对象池 + 所有权转移(每个 buffer 任意时刻只有一个持有者、恰好归还一次)从设计上消除了"并发增减计数"这个竞态——不需要原子操作去救一个本来可以设计掉的共享状态。这正是 RK3568 视频流水线 DMA-BUF 的做法;内核层 DMA-BUF 的计数由 kref 管理。

9. 总结

机制
关键接口
核心语义
典型场景
原子整数
atomic_inc, atomic_dec_and_test
RMW (Read-Modify-Write)
引用计数、资源统计
位操作
test_and_set_bit
Test-and-Set
标志位、轻量级锁、位图管理
CAS
cmpxchg, try_cmpxchg
Compare-and-Swap
无锁队列、无锁链表、乐观锁实现
64位/长整型
atomic64_add, atomic_long_inc
64位/平台相关宽度的原子操作
大数值计数、指针相关的原子操作
本地原子
local_inc, local_add
Per-CPU Atomicity
Per-CPU 统计数据(高性能)
上一篇
Guide to Clion
下一篇
Linux Device Tree Overlay 机制详解

Comments
Loading...