2. 原子操作:整型原子操作与位原子操作、原子变量的API与使用场景、内存屏障与顺序保证、原子操作在驱动中的实战

说到并发控制,很多人第一反应就是锁。但锁的开销其实不小,尤其是自旋锁,忙等待的时候CPU都在空转。我个人的习惯是,能不用锁就不用锁。那不用锁怎么保证数据安全?原子操作就是第一道防线。

说白了,原子操作就是“不可分割”的操作。要么全部执行完,要么一点都没执行。不会出现执行到一半被中断的情况。这在单核时代很简单,关中断就行。但在多核系统里,事情就没那么简单了。

整型原子操作

整型原子操作是最基础的一类。Linux内核提供了atomic_t类型,专门用来做原子计数。你想想看,如果直接用普通的int变量做i++,在汇编层面其实是三条指令:读、改、写。多核同时执行时,数据就乱了。

核心API其实不多,我列几个常用的:

atomic_t v = ATOMIC_INIT(0);    // 定义并初始化为0
atomic_set(&v, 5);              // 设置值为5
atomic_read(&v);                // 读取当前值
atomic_inc(&v);                 // 原子加1
atomic_dec(&v);                 // 原子减1
atomic_add(2, &v);              // 原子加2
atomic_sub(2, &v);              // 原子减2
atomic_inc_and_test(&v);        // 加1后测试是否为0
atomic_dec_and_test(&v);        // 减1后测试是否为0
atomic_add_negative(1, &v);     // 加1后测试是否为负数

关键点atomic_t在32位系统上是32位,在64位系统上也是32位。如果需要64位原子操作,用atomic64_t

我在项目中遇到过一个问题:驱动里用了一个全局计数器统计中断次数。最开始用的普通int,结果发现统计值偶尔会少。排查了半天,最后发现是中断嵌套导致的。换成atomic_inc后问题就解决了。

位原子操作

除了整型,内核还提供了位级别的原子操作。这在管理资源位图时特别有用。比如你要管理一组硬件资源,每个资源用一个bit表示是否被占用。

unsigned long bitmap = 0;

set_bit(3, &bitmap);       // 将第3位置1
clear_bit(3, &bitmap);     // 将第3位清0
test_bit(3, &bitmap);      // 测试第3位的值
test_and_set_bit(3, &bitmap);   // 测试并置位
test_and_clear_bit(3, &bitmap); // 测试并清0

嗯,这里要注意:set_bitclear_bit是原子的,但如果你先test_bit再根据结果去set_bit,这两步之间就不是原子的了。这时候要用test_and_set_bit这种复合操作。

避坑指南:我曾经在写网卡驱动时,用test_bitset_bit的组合来分配DMA描述符。结果两个CPU同时执行,都以为资源是空闲的,都分配了同一个描述符。后来改成test_and_set_bit才搞定。记住,测试和设置必须是一个原子操作。

内存屏障与顺序保证

原子操作能保证操作的原子性,但不能保证内存访问的顺序。为什么?因为编译器和CPU都可能重排指令。编译器为了优化会调整代码顺序,CPU为了流水线效率也会乱序执行。

举个例子:

// 线程A
data = 100;
flag = 1;  // 假设flag是atomic_t

// 线程B
if (flag == 1) {
    printk("data = %d\n", data);  // 可能打印出0!
}

为什么会这样?因为CPU可能先把flag的写入执行了,data的写入还在store buffer里没刷到内存。线程B看到flag为1,但data还是旧值。

解决这个问题需要内存屏障。Linux内核提供了几种屏障:

屏障类型 宏定义 作用
通用屏障 mb() 读写操作都禁止重排
读屏障 rmb() 只禁止读操作重排
写屏障 wmb() 只禁止写操作重排

修正上面的代码:

// 线程A
data = 100;
wmb();           // 确保data写入先于flag写入
atomic_set(&flag, 1);

// 线程B
if (atomic_read(&flag) == 1) {
    rmb();       // 确保flag读取先于data读取
    printk("data = %d\n", data);
}

小技巧:其实很多原子操作API自带内存屏障语义。比如atomic_inc_return就包含了完整的屏障。如果你不确定,可以查一下内核文档里的"Documentation/atomic_t.txt"。

原子操作在驱动中的实战

说了这么多理论,来看看实际驱动里怎么用。我以一个字符设备驱动为例,展示原子操作如何管理设备打开次数。

#include <linux/atomic.h>
#include <linux/fs.h>

static atomic_t device_open = ATOMIC_INIT(0);

static int my_device_open(struct inode *inode, struct file *file)
{
    // 尝试将device_open从0变为1
    if (!atomic_cmpxchg(&device_open, 0, 1)) {
        // 成功获取设备
        return 0;
    }
    // 设备已被占用
    return -EBUSY;
}

static int my_device_release(struct inode *inode, struct file *file)
{
    atomic_set(&device_open, 0);
    return 0;
}

atomic_cmpxchg是个好东西。它比较旧值,如果相等就设置新值,整个过程是原子的。这比先读再写安全多了。

再来看一个更复杂的场景:多生产者、单消费者的环形缓冲区。我习惯用原子操作来管理头尾指针:

struct ring_buffer {
    char data[BUFFER_SIZE];
    atomic_t head;
    atomic_t tail;
};

// 生产者写入
int rb_write(struct ring_buffer *rb, char c)
{
    int head = atomic_read(&rb->head);
    int tail = atomic_read(&rb->tail);
    int next = (head + 1) % BUFFER_SIZE;
    
    if (next == tail) {
        return -ENOSPC;  // 缓冲区满
    }
    
    rb->data[head] = c;
    smp_wmb();  // 确保数据写入先于head更新
    atomic_set(&rb->head, next);
    return 0;
}

// 消费者读取
int rb_read(struct ring_buffer *rb, char *c)
{
    int head = atomic_read(&rb->head);
    int tail = atomic_read(&rb->tail);
    
    if (head == tail) {
        return -ENODATA;  // 缓冲区空
    }
    
    *c = rb->data[tail];
    smp_rmb();  // 确保数据读取后于tail更新
    atomic_set(&rb->tail, (tail + 1) % BUFFER_SIZE);
    return 0;
}

这个环形缓冲区是无锁的。多个生产者可以同时写,因为每个生产者操作的是不同的head值。但要注意,这里假设了只有一个消费者。如果多个消费者,就需要更复杂的机制了。

实战经验:我在做网络驱动时,用原子操作管理NAPI的调度状态。用test_and_set_bit来避免同一个设备被多次调度到软中断。这个模式在内核网络子系统中很常见,你可以看看net/core/dev.c里的napi_schedule实现。

最后说一句,原子操作虽然轻量,但不是万能的。复杂的同步场景还是得用锁。但能用原子操作的地方,尽量用原子操作。性能好,代码也清晰。

原子操作知识体系 原子操作 整型原子操作 atomic_t / atomic64_t atomic_inc / atomic_dec atomic_add / atomic_sub atomic_cmpxchg 位原子操作 set_bit / clear_bit test_bit test_and_set_bit test_and_clear_bit 内存屏障 mb() - 通用屏障 rmb() - 读屏障 wmb() - 写屏障 smp_rmb / smp_wmb 使用场景 计数器/统计 资源位图管理 设备打开计数 驱动实战 字符设备open/close 环形缓冲区 NAPI调度状态 原子操作是并发控制的基础,轻量且高效

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