2. 原子操作:从底层理解并发安全的基石
大家好,我是你们的Linux内核讲师。今天我们来聊聊原子操作。
说实话,很多做应用层开发的朋友,第一次接触「原子操作」这个概念时,会觉得有点玄乎。什么叫「原子」?是不是跟物理里的原子一样不可分割?
嗯,差不多就是这个意思。在并发编程里,原子操作就是指——一个操作要么全部执行完,要么一点都不执行。中间不可能被任何其他线程或CPU核心打断。
你想想看,如果两个CPU核心同时对一个变量做「加1」操作,会发生什么?
正常流程是:读变量 → 加1 → 写回变量。但如果两个核心同时读到了同一个值,比如都是5,各自加1后写回6。结果本该是7,却变成了6。这就是经典的「丢失更新」问题。
我在早期做嵌入式驱动开发时,就踩过这个坑。当时调试一个计数器,怎么都对不上,查了两天才发现是并发更新导致的。从那以后,我对原子操作就格外上心。
2.1 原子操作的本质
原子操作的核心思想很简单:让CPU在执行某条指令时,锁定内存总线或缓存行,确保其他核心无法同时访问。
现代CPU都提供了专门的原子指令,比如x86的LOCK前缀、ARM的LDREX/STREX指令对。Linux内核把这些指令封装成了统一的API,让我们不用关心底层架构差异。
关键点:原子操作是其他所有同步机制(自旋锁、互斥锁、RCU)的基础。没有原子操作,锁机制本身就无法实现。
2.2 原子整型操作 API
内核里最常用的原子类型是 atomic_t。别看它只是个结构体,里面就一个 int counter 字段,但所有操作它的函数都是原子的。
我习惯把原子整型API分成三类:
2.2.1 初始化与设置
atomic_t v = ATOMIC_INIT(0); // 静态初始化
atomic_set(&v, 10); // 动态设置值
int val = atomic_read(&v); // 读取当前值
这里要注意:atomic_read 虽然看起来像普通读取,但它用了 READ_ONCE 宏,防止编译器优化导致读取到过时的值。
2.2.2 加减操作
atomic_add(5, &v); // v += 5
atomic_sub(3, &v); // v -= 3
atomic_inc(&v); // v++
atomic_dec(&v); // v--
这些函数都是原子的。但有个细节:它们不返回修改后的值。如果你需要知道操作后的结果,要用下面这些:
int old = atomic_fetch_add(5, &v); // 返回旧值,v += 5
int new = atomic_add_return(5, &v); // 返回新值,v += 5
2.2.3 条件操作
int old = atomic_cmpxchg(&v, old_val, new_val); // 比较并交换
int old = atomic_xchg(&v, new_val); // 交换并返回旧值
bool ok = atomic_try_cmpxchg(&v, &old, new); // 简化版cmpxchg
atomic_cmpxchg 是CAS(Compare-And-Swap)操作,它是实现无锁数据结构的核心。我在实现一个无锁队列时,就大量用到了这个API。
我的经验:如果你只是做简单的计数器或标志位,用 atomic_inc/atomic_dec 就够了。但如果你要实现更复杂的同步逻辑,atomic_cmpxchg 才是真正的利器。
2.3 原子位操作 API
除了整型,内核还提供了对单个bit的原子操作。这在管理标志位、状态位时特别有用。
unsigned long bits = 0;
set_bit(3, &bits); // 将第3位设为1
clear_bit(3, &bits); // 将第3位清0
change_bit(3, &bits); // 翻转第3位
bool val = test_bit(3, &bits); // 测试第3位的值
bool old = test_and_set_bit(3, &bits); // 设置并返回旧值
bool old = test_and_clear_bit(3, &bits); // 清0并返回旧值
我曾经在调试一个网络驱动时,发现状态标志位被并发修改导致逻辑混乱。用 set_bit 和 clear_bit 替换普通位操作后,问题立刻解决。
注意:普通位操作(如 bits |= (1 << 3))不是原子的!它包含读-改-写三步,在多核环境下会出问题。一定要用内核提供的原子位操作API。
2.4 原子操作实战案例
光说不练假把式。我们来看一个真实场景:多核环境下的引用计数。
假设我们有一个共享资源,多个CPU核心可以获取和释放它。我们需要记录当前有多少个使用者。
#include <linux/atomic.h>
#include <linux/kernel.h>
struct shared_resource {
void *data;
atomic_t refcount;
};
// 获取资源
struct shared_resource *get_resource(struct shared_resource *res)
{
// 原子增加引用计数
atomic_inc(&res->refcount);
return res;
}
// 释放资源
void put_resource(struct shared_resource *res)
{
// 原子减少引用计数,并检查是否归零
if (atomic_dec_and_test(&res->refcount)) {
// 引用计数为0,可以安全释放资源
kfree(res->data);
kfree(res);
pr_info("Resource freed\n");
}
}
这个例子很简单,但很实用。atomic_dec_and_test 这个函数特别棒——它原子地减1,并返回结果是否为0。这样我们就不需要额外的锁来判断了。
我在做内存管理模块时,就用这种模式管理共享缓冲区的生命周期。效果非常好,从来没有出现过use-after-free的问题。
2.5 原子操作的性能考量
原子操作虽然安全,但也不是免费的午餐。它比普通操作要慢,因为涉及CPU总线锁定或缓存一致性协议。
| 操作类型 | 相对耗时 | 说明 |
|---|---|---|
| 普通内存访问 | 1x | 直接读/写,无同步开销 |
| 原子读 (atomic_read) | 约1-2x | 只是防止编译器优化,硬件开销小 |
| 原子加/减 | 约10-20x | 需要锁定内存总线 |
| 原子CAS (cmpxchg) | 约20-40x | 最重的原子操作,涉及比较和交换 |
所以我的建议是:能用简单原子操作就别用复杂的。比如计数器用 atomic_inc 就够了,别用 atomic_cmpxchg 去实现。
核心原则:原子操作是并发编程的「最小公分母」。它简单、高效、可靠。在你需要更复杂的同步机制之前,先想想能不能用原子操作解决问题。
2.6 知识体系总览
为了让你更直观地理解本章内容,我画了一张图:
这张图把原子操作分成了三个维度:整型操作、位操作和实战应用。你可以看到,它们最终都指向同一个核心——硬件级保证的不可分割性。
好了,关于原子操作就讲到这里。记住一句话:能用原子操作解决的问题,就不要用锁。它简单、高效,是并发编程的第一选择。
公众号:蓝海资料掘金营,微信deep3321