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_bitclear_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 知识体系总览

为了让你更直观地理解本章内容,我画了一张图:

原子操作知识体系 原子操作 原子整型操作 原子位操作 实战应用 atomic_t / atomic_inc / atomic_dec atomic_add_return / atomic_cmpxchg set_bit / clear_bit / test_bit test_and_set_bit / test_and_clear_bit 引用计数 / 标志位管理 无锁数据结构 / 状态机 核心:不可分割 · 无锁安全 · 硬件级保证

这张图把原子操作分成了三个维度:整型操作、位操作和实战应用。你可以看到,它们最终都指向同一个核心——硬件级保证的不可分割性。

好了,关于原子操作就讲到这里。记住一句话:能用原子操作解决的问题,就不要用锁。它简单、高效,是并发编程的第一选择。


公众号:蓝海资料掘金营,微信deep3321