3. 原子操作基础:Linux内核atomic_t、原子加减、原子位操作、内存屏障简介
原子操作是并发编程中最基础的构建块,也是实现高效同步原语(如futex和读写锁)的基石。本章将深入剖析Linux内核中原子操作的核心机制,包括atomic_t类型、原子加减与位操作,以及内存屏障的关键作用。
3.1 为什么需要原子操作?
在多线程或多核环境下,对共享变量的简单“读-改-写”操作(如i++)并非不可分割。一个典型的i++在CPU层面会被分解为:
- 读取:从内存加载
i的值到寄存器。 - 修改:在寄存器中将值加1。
- 写入:将新值写回内存。
如果两个CPU核心同时执行此操作,可能导致结果比预期少1(丢失更新)。原子操作通过硬件指令(如x86的LOCK前缀或ARM的LDREX/STREX)确保整个操作在单个指令周期内完成,或对其它核心表现为不可分割。
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(旧值) |
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针对整个整数。
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()或释放语义)。
下面的流程图展示了读写锁写者获取锁时的典型操作序列:
3.6 总结与最佳实践
- 首选原子操作:对于简单的计数器或标志位,优先使用
atomic_t和原子位操作,避免使用自旋锁。 - 理解隐含语义:熟悉不同原子API是否隐含内存屏障。例如,
atomic_inc()通常不隐含屏障,而atomic_inc_return()隐含完整屏障。 - 显式使用屏障:在实现复杂的同步原语时,不要依赖“巧合”的顺序,应显式插入
smp_mb()、smp_wmb()或使用带有获取/释放语义的原子操作。 - 利用KCSAN:内核提供了KCSAN(Kernel Concurrency Sanitizer)工具,可以动态检测数据竞争,帮助验证原子操作和屏障的使用是否正确。