3. 原子操作基础:Linux内核atomic_t、原子加减、原子位操作、内存屏障简介

原子操作是并发编程中最基础的构建块,也是实现高效同步原语(如futex和读写锁)的基石。本章将深入剖析Linux内核中原子操作的核心机制,包括atomic_t类型、原子加减与位操作,以及内存屏障的关键作用。

3.1 为什么需要原子操作?

在多线程或多核环境下,对共享变量的简单“读-改-写”操作(如i++)并非不可分割。一个典型的i++在CPU层面会被分解为:

  1. 读取:从内存加载i的值到寄存器。
  2. 修改:在寄存器中将值加1。
  3. 写入:将新值写回内存。

如果两个CPU核心同时执行此操作,可能导致结果比预期少1(丢失更新)。原子操作通过硬件指令(如x86的LOCK前缀或ARM的LDREX/STREX)确保整个操作在单个指令周期内完成,或对其它核心表现为不可分割。

核心概念: 原子操作要么完全执行,要么完全不执行,中间状态对其它执行单元不可见。这是解决并发数据竞争的第一道防线。
原子操作 vs 非原子操作(i++) 非原子操作(可能丢失更新) CPU0: 读取 i=0 CPU1: 读取 i=0 CPU0: i++ → 1 CPU1: i++ → 1 CPU0: 写入 i=1 CPU1: 写入 i=1 结果:i=1(期望 i=2) 原子操作(保证正确) 硬件锁定:LOCK 前缀 或 LL/SC 对 原子指令:atomic_add(1, &i) 读取、修改、写入不可分割 结果:i=2(正确)

3.2 Linux内核的atomic_t类型

Linux内核提供了atomic_t类型来封装原子操作。它是一个简单的结构体,内部仅包含一个int类型的计数器。之所以使用结构体而非直接使用int,是为了防止开发者误用非原子操作(如=赋值)来修改它。

// include/linux/types.h
typedef struct {
    int counter;
} atomic_t;

// 初始化宏
#define ATOMIC_INIT(i) { (i) }
API函数 描述 返回值
atomic_read(v) 读取原子变量的值 int
atomic_set(v, i) 设置原子变量的值为i void
atomic_add(i, v) 原子地将i加到v上 void
atomic_sub(i, v) 原子地从v中减去i void
atomic_inc(v) 原子地将v加1 void
atomic_dec(v) 原子地将v减1 void
atomic_inc_and_test(v) 原子加1,若结果为0返回true bool
atomic_dec_and_test(v) 原子减1,若结果为0返回true bool
atomic_add_negative(i, v) 原子加i,若结果为负数返回true bool
atomic_cmpxchg(v, old, new) 比较并交换:若v==old则交换为new int(旧值)
使用建议: 在编写内核模块时,对于任何可能被多个CPU或中断上下文访问的整数计数器,都应优先使用atomic_t及其配套API,而不是普通的int加锁。

3.3 原子位操作

除了对整数的原子加减,内核还提供了一套原子位操作API,用于对内存中的位进行原子级的置位、清零和测试。这些操作在实现标志位、状态位和自旋锁的变体中非常有用。

// 原子位操作API(定义在 include/asm-generic/bitops/atomic.h)
void set_bit(int nr, volatile unsigned long *addr);   // 原子地将第nr位置1
void clear_bit(int nr, volatile unsigned long *addr); // 原子地将第nr位清零
void change_bit(int nr, volatile unsigned long *addr);// 原子地翻转第nr位
int test_and_set_bit(int nr, volatile unsigned long *addr);  // 原子置位并返回旧值
int test_and_clear_bit(int nr, volatile unsigned long *addr);// 原子清零并返回旧值
int test_bit(int nr, volatile unsigned long *addr);   // 非原子地测试第nr位

原子位操作与atomic_t操作在底层实现上高度相似,通常都依赖于相同的硬件原子指令。它们的主要区别在于操作粒度:位操作针对单个位,而atomic_t针对整个整数。

原子位操作示例:test_and_set_bit 位数组(32位):[0][1][2][3]...[15][16][17]...[31] 位15 1. 原子读取位15 2. 返回旧值(0或1) 3. 原子置位15为1

3.4 内存屏障简介

原子操作保证了操作的原子性,但并未完全解决内存可见性和执行顺序问题。现代CPU为了优化性能,会采用指令重排(Out-of-Order Execution)和缓存一致性协议。这可能导致代码的执行顺序与编写顺序不一致,从而引发难以调试的并发问题。

内存屏障(Memory Barrier) 是一种指令,用于强制CPU或编译器遵守内存访问的顺序约束。Linux内核提供了多种内存屏障宏:

屏障类型 宏定义 作用
通用屏障 mb() 保证屏障前后的所有读写操作不会跨越屏障重排
写屏障 wmb() 保证屏障前的所有写操作在屏障后的写操作之前完成
读屏障 rmb() 保证屏障前的所有读操作在屏障后的读操作之前完成
获取屏障 smp_acquire() 后续的读写操作不能重排到屏障之前(用于锁获取)
释放屏障 smp_release() 屏障前的读写操作不能重排到屏障之后(用于锁释放)
常见误区: 许多开发者认为使用了atomic_t就无需内存屏障。实际上,atomic_read()和atomic_set()在大多数架构上并不隐含内存屏障。只有带_return或_test后缀的原子操作(如atomic_add_return)才隐含完整的内存屏障。在实现自定义锁或无锁数据结构时,必须显式考虑屏障的使用。

3.5 原子操作与内存屏障的协同

在实现futex和读写锁时,原子操作与内存屏障通常协同工作:

  • 原子操作:用于修改锁的状态(如计数器的增减、标志位的置位),确保修改的原子性。
  • 内存屏障:用于保证锁状态修改与临界区数据访问之间的顺序关系。例如,在释放锁之前,必须确保所有临界区内的写操作已经完成(通过wmb()或释放语义)。

下面的流程图展示了读写锁写者获取锁时的典型操作序列:

写者获取读写锁:原子操作与屏障协同 步骤1:原子写锁标志位 atomic_set(&lock->write_locked, 1) 步骤2:插入写屏障 smp_wmb() 或 smp_store_release() 步骤3:原子等待读者释放 while (atomic_read(&lock->readers) > 0) cpu_relax() 步骤4:进入临界区 // 安全访问共享数据 关键点:写屏障确保“写锁标志”对其他核心可见后,临界区数据才被修改

3.6 总结与最佳实践

  • 首选原子操作:对于简单的计数器或标志位,优先使用atomic_t和原子位操作,避免使用自旋锁。
  • 理解隐含语义:熟悉不同原子API是否隐含内存屏障。例如,atomic_inc()通常不隐含屏障,而atomic_inc_return()隐含完整屏障。
  • 显式使用屏障:在实现复杂的同步原语时,不要依赖“巧合”的顺序,应显式插入smp_mb()、smp_wmb()或使用带有获取/释放语义的原子操作。
  • 利用KCSAN:内核提供了KCSAN(Kernel Concurrency Sanitizer)工具,可以动态检测数据竞争,帮助验证原子操作和屏障的使用是否正确。