2. 原子操作
原子操作是Linux内核并发控制中最基础的构建块。与自旋锁或互斥体不同,原子操作直接在单条指令或指令序列中完成“读-改-写”过程,不会被其他执行流中断。理解原子操作API、内存屏障语义以及不同架构下的指令差异,是调试竞态条件的前提。
核心要点:原子操作保证操作的不可分割性;内存屏障保证操作的可见性与顺序性;不同CPU架构的原子指令实现差异直接影响驱动代码的可移植性。
2.1 原子整型操作API
Linux内核提供了一套与架构无关的原子整型操作接口,定义在 <linux/atomic.h> 中。核心数据类型为 atomic_t,它是一个封装了 int 的结构体,强制开发者只能通过专用API访问,避免直接赋值导致的竞态。
| API 函数 | 功能描述 | 返回值 |
|---|---|---|
atomic_set(v, i) |
将原子变量 v 设置为 i | 无 |
atomic_read(v) |
读取原子变量 v 的值 | int |
atomic_add(i, v) |
将 i 加到 v 上 | 无 |
atomic_sub(i, v) |
从 v 中减去 i | 无 |
atomic_inc(v) |
v 自增 1 | 无 |
atomic_dec(v) |
v 自减 1 | 无 |
atomic_inc_and_test(v) |
自增后判断结果是否为 0 | bool (true 为 0) |
atomic_dec_and_test(v) |
自减后判断结果是否为 0 | bool (true 为 0) |
atomic_add_return(i, v) |
加 i 并返回新值 | int |
atomic_sub_return(i, v) |
减 i 并返回新值 | int |
atomic_xchg(v, new) |
将 v 设置为 new,返回旧值 | int |
atomic_cmpxchg(v, old, new) |
如果 v == old,则设为 new,返回旧值 | int |
调试提示:在竞态调试中,
atomic_read 本身不保证内存屏障。如果需要读取其他CPU的最新值,应配合 smp_mb() 或使用带屏障的变体(如 atomic_add_return 隐含完整内存屏障)。
典型使用场景——引用计数:
struct my_device {
atomic_t refcount;
// ... 其他字段
};
// 初始化
atomic_set(&dev->refcount, 1);
// 获取引用
void get_device(struct my_device *dev) {
atomic_inc(&dev->refcount);
}
// 释放引用,返回 true 表示可销毁
int put_device(struct my_device *dev) {
return atomic_dec_and_test(&dev->refcount);
}
2.2 原子位操作API
除了整型变量,内核还提供了对内存中任意位的原子操作,常用于设备寄存器标志位或状态字的管理。这些API定义在 <linux/bitops.h> 中,操作对象是 unsigned long * 指针。
| API 函数 | 功能描述 | 是否带内存屏障 |
|---|---|---|
set_bit(nr, addr) |
将 addr 指向的第 nr 位置 1 | 否(非顺序版本) |
clear_bit(nr, addr) |
将 addr 指向的第 nr 位清 0 | 否(非顺序版本) |
change_bit(nr, addr) |
翻转 addr 指向的第 nr 位 | 否 |
test_and_set_bit(nr, addr) |
置位并返回原值 | 隐含完整内存屏障 |
test_and_clear_bit(nr, addr) |
清位并返回原值 | 隐含完整内存屏障 |
test_and_change_bit(nr, addr) |
翻转并返回原值 | 隐含完整内存屏障 |
test_bit(nr, addr) |
读取第 nr 位的值 | 无 |
常见陷阱:
set_bit 和 clear_bit 的“非顺序”版本不保证对其他CPU的可见性。如果需要在置位后立即被其他核心看到,应使用 smp_mb__before_atomic() 或 smp_mb__after_atomic() 显式插入屏障,或直接使用 test_and_set_bit 等带屏障的变体。
位操作典型用法——设备状态标志:
#define DEVICE_BUSY 0
#define DEVICE_ERROR 1
#define DEVICE_DONE 2
unsigned long dev_flags = 0;
// 原子标记设备忙
if (test_and_set_bit(DEVICE_BUSY, &dev_flags)) {
pr_info("设备正忙,无法操作\n");
return -EBUSY;
}
// 中断处理中标记完成
test_and_set_bit(DEVICE_DONE, &dev_flags);
// 清除忙标志
clear_bit(DEVICE_BUSY, &dev_flags);
2.3 内存屏障与顺序保证
原子操作保证了操作的不可分割性,但不保证操作的顺序。现代CPU为了性能会对内存访问进行重排序(Reordering)。内存屏障(Memory Barrier)就是用来约束这种重排序的指令。
Linux内核提供以下屏障原语:
| 屏障宏 | 语义 | 典型使用场景 |
|---|---|---|
smp_mb() |
完整内存屏障(读+写) | 临界区前后,保证所有之前的内存操作完成 |
smp_rmb() |
读内存屏障 | 保证屏障前的读操作先于屏障后的读操作 |
smp_wmb() |
写内存屏障 | 保证屏障前的写操作先于屏障后的写操作 |
smp_mb__before_atomic() |
原子操作前的屏障 | 配合 set_bit 等非屏障原子操作使用 |
smp_mb__after_atomic() |
原子操作后的屏障 | 配合 clear_bit 等非屏障原子操作使用 |
顺序保证模型:Linux内核遵循“因果一致性”(Causal Consistency)模型。如果CPU A在写操作W1之后执行了
smp_wmb(),然后执行写操作W2,那么所有其他CPU在看到W2时,必须也能看到W1(或W1之后的值)。但如果没有屏障,其他CPU可能看到W2但看不到W1。
经典生产者-消费者模式中的屏障使用:
// 生产者(CPU 0)
data = 100;
smp_wmb(); // 保证 data 写入先于 flag 写入
atomic_set(&flag, 1);
// 消费者(CPU 1)
while (atomic_read(&flag) != 1)
cpu_relax();
smp_rmb(); // 保证 flag 读取先于 data 读取
printk("data = %d\n", data); // 安全:一定看到 100
2.4 ARM64与x86架构下的原子指令差异
不同CPU架构的原子指令实现差异巨大。理解这些差异对于编写可移植的内核代码和调试跨平台竞态问题至关重要。
| 特性 | x86 (x86_64) | ARM64 (AArch64) |
|---|---|---|
| 原子指令 | LOCK 前缀 + 普通指令(如 LOCK ADD, LOCK XCHG) |
专用原子指令 (如 LDXR/STXR 独占加载/存储对) |
| 实现机制 | 总线锁或缓存锁(MESI协议) | 独占监控(Exclusive Monitor) 失败后重试(LL/SC 循环) |
| 内存顺序 | LOCK 前缀隐含完整内存屏障(所有 LOCK 指令自带 smp_mb()) |
需显式指定顺序语义 ( LDAR = acquire, STLR = release) |
| 性能特点 | 原子操作开销相对稳定 (缓存行锁定) |
低竞争时开销小 高竞争时可能多次重试 |
| 对调试的影响 | 原子操作自带屏障, 不易出现顺序问题 |
容易因缺少屏障导致 可见性问题 |
调试警示:在ARM64上,如果使用
atomic_set 后直接 atomic_read,没有插入任何屏障,另一个CPU可能看到旧值。x86上由于强内存模型(TSO),这种问题较少见,但并非不存在(特别是涉及非临时存储指令时)。
ARM64 原子操作的内核实现示例(简化):
// ARM64 atomic_add_return (简化伪代码)
static inline int atomic_add_return(int i, atomic_t *v)
{
unsigned long tmp;
int result;
asm volatile(
"1: ldaxr %w0, %2\n" // 独占加载(acquire语义)
" add %w0, %w0, %w3\n" // 加法
" stlxr %w1, %w0, %2\n" // 独占存储(release语义)
" cbnz %w1, 1b\n" // 如果存储失败,重试
: "=&r" (result), "=&r" (tmp), "+Q" (v->counter)
: "Ir" (i)
: "memory");
return result;
}
x86 原子操作的内核实现示例(简化):
// x86 atomic_add_return (简化伪代码)
static inline int atomic_add_return(int i, atomic_t *v)
{
int __i = i;
asm volatile(
"lock; xaddl %0, %1\n" // LOCK前缀 + XADD(交换并相加)
: "+r" (__i), "+m" (v->counter)
:
: "memory");
return __i + i;
}
2.5 实战调试要点
- 检查原子操作是否带屏障:在ARM64上,
atomic_inc和atomic_add不带屏障,而atomic_inc_return带完整屏障。误用可能导致可见性问题。 - 使用
READ_ONCE/WRITE_ONCE:对于非原子变量的共享访问,使用这两个宏可以防止编译器优化导致的撕裂读/写,但不提供硬件屏障。 - KCSAN 工具:内核的KCSAN(Kernel Concurrency Sanitizer)可以动态检测数据竞争,包括原子操作使用不当的情况。
- 调试打印中的原子性:在
printk中读取共享变量时,应使用atomic_read或READ_ONCE,避免读取到撕裂值。
最佳实践:在编写新代码时,优先使用
atomic_*_return 或 test_and_*_bit 等带屏障的变体。只有在性能关键路径且明确理解内存模型时,才使用非屏障版本并手动插入 smp_mb()。