原子操作(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_t 和 atomic_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_read 和 atomic_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。这类顺序关系常见于:
- 生产者—消费者队列。
- 无锁链表、无锁队列和 RCU 等并发数据结构。
- 中断处理、工作队列与进程上下文之间的状态传递。
- 多核 CPU 间共享状态的发布与读取。
- 设备驱动中描述符、缓冲区与设备寄存器之间的访问排序。
常用接口
类型 | 常用接口 | 作用与注意事项 |
编译器屏障 | 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<unsigned>,relaxed就够 - 管理对象生命周期、要释放回调 →
std::shared_ptr - 侵入式计数/对象池/跨模块传裸指针 →
boost::intrusive_ptr或手写原子计数 - 嵌入式、避免
shared_ptr的堆分配和 RTTI 开销 → 手写std::atomic<unsigned>+ 对象池 - 老工具链(无 C++11)→
__atomic_*→__sync_*
手写引用计数的标准模板(对应内核 dec_and_test)
归零判断 + 释放必须是同一个原子操作:
fetch_sub 返回的是旧值,旧值 == 1 说明减完正好到 0,只有这一个线程执行销毁。如果先减、再单独 load() 判断,两个线程可能同时读到 0 都执行 release → double-free。硬件原子指令底层
原子接口最终都落到 CPU 指令:
- x86:
lock前缀 +xadd/inc,单条指令完成读-改-写,通过总线锁/缓存锁保证原子。
- ARM:
LDREX/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 统计数据(高性能) |
- Author:felixfixit
- URL:http://www.felixmicrospace.top/article/linux_kernel_atomic_operations
- Copyright:All articles in this blog, except for special statements, adopt BY-NC-SA agreement. Please indicate the source!












