1. 并发基础:并发与并行、竞态条件与内核并发源
在深入自旋锁与互斥体之前,必须厘清并发与并行的本质区别,理解竞态条件的触发机制,并系统梳理 Linux 内核中所有可能的并发源。这是调试竞态条件的理论基石。
1.1 并发与并行的区别
并发(Concurrency)与并行(Parallelism)常被混用,但在内核开发中必须严格区分:
| 维度 | 并发(Concurrency) | 并行(Parallelism) |
|---|---|---|
| 核心定义 | 多个任务在逻辑上同时执行(交替执行) | 多个任务在物理上同时执行(真正同时) |
| 硬件条件 | 单核 CPU 即可实现(通过时间片轮转) | 必须多核 SMP 或多处理器 |
| 执行视角 | 微观串行,宏观并行 | 微观并行,宏观并行 |
| 内核关注点 | 抢占、中断、软中断导致的交错 | 多核同时访问共享数据 |
| 竞态触发 | 单核上执行流被中断/抢占 | 多核同时读写同一变量 |
关键认知: 并发是竞态条件的充分条件,并行是竞态条件的强化条件。即使单核系统,只要存在中断或抢占,竞态依然存在。
1.2 竞态条件(Race Condition)定义
竞态条件是指多个执行流(进程、中断、软中断、内核线程)在没有适当同步的情况下,同时访问共享资源,导致最终结果依赖于执行流的时间交错顺序,从而产生不可预测的错误。
竞态三要素:
- 共享资源:全局变量、设备寄存器、链表、缓存等被多个执行流访问的数据结构。
- 非原子操作:读-改-写、检查-执行等操作在汇编层面被拆分为多条指令。
- 交错执行:两个执行流在操作共享资源时发生时间上的交错。
典型竞态示例(内核中常见的“检查-使用”模式):
/* 竞态示例:检查-使用模式 */
if (list_empty(&head)) {
/* 在此处可能被其他执行流插入节点 */
/* 此时 head 不再为空,但条件判断已通过 */
return NULL; /* 错误:实际有数据但返回空 */
}
data = list_entry(head.next, struct my_data, node);
1.3 内核并发源分析
Linux 内核是一个高度并发的系统,其并发源主要来自以下四个方面。理解这些并发源是正确选择锁机制的前提。
1.3.1 硬件中断(Hardware IRQ)
硬件中断是最高优先级的并发源。当外设触发中断时,CPU 会立即暂停当前执行流,跳转到中断处理程序(ISR)。中断处理程序与被打断的进程共享内核数据,这是最常见的竞态来源。
- 特点:异步、不可预测、优先级最高。
- 竞态场景:进程正在修改链表时,中断处理程序也访问同一链表。
- 防护手段:
local_irq_disable()/spin_lock_irqsave()。
1.3.2 软中断与 Tasklet(SoftIRQ / BH)
软中断是中断的下半部机制,在硬件中断返回后、进程恢复前执行。软中断可以抢占当前进程,但软中断之间在同一个 CPU 上不会嵌套执行。
- 特点:在中断上下文中执行,可睡眠的函数不能调用。
- 竞态场景:进程上下文与软中断共享数据;或同一软中断在不同 CPU 上同时执行。
- 防护手段:
spin_lock_bh()/local_bh_disable()。
1.3.3 内核抢占(Kernel Preemption)
当内核配置了 CONFIG_PREEMPT,一个在内核态运行的进程可以被另一个更高优先级的进程抢占。这意味着即使单核系统,进程上下文之间也会产生并发。
- 特点:进程级抢占,发生在内核态。
- 竞态场景:进程 A 持有锁并修改数据,被进程 B 抢占,进程 B 尝试获取同一锁。
- 防护手段:
preempt_disable()/spin_lock()(隐含禁止抢占)。
1.3.4 多核 SMP(Symmetric Multi-Processing)
多核系统是并发源的“放大器”。两个或更多 CPU 核心可以同时执行内核代码,同时访问同一共享变量。这是最直观也最危险的并发源。
- 特点:真正的并行执行,需要原子操作或锁来保证互斥。
- 竞态场景:Core 0 和 Core 1 同时执行
counter++(非原子操作)。 - 防护手段:
spin_lock()/atomic_t/mutex_lock()。
实战经验: 调试竞态条件时,首先要确定当前代码路径可能被哪些并发源干扰。一个简单的判断方法:
中断路径 → 用 spin_lock_irqsave()
软中断路径 → 用 spin_lock_bh()
进程上下文(可能被抢占)→ 用 spin_lock() 或 mutex_lock()
多核共享 → 必须用自旋锁或互斥体
中断路径 → 用 spin_lock_irqsave()
软中断路径 → 用 spin_lock_bh()
进程上下文(可能被抢占)→ 用 spin_lock() 或 mutex_lock()
多核共享 → 必须用自旋锁或互斥体
1.4 并发源优先级与锁选择对照表
| 并发源 | 优先级 | 能否睡眠 | 推荐锁类型 | 关键 API |
|---|---|---|---|---|
| 硬件中断 | 最高 | 否 | 自旋锁(关中断) | spin_lock_irqsave() |
| 软中断 / Tasklet | 高 | 否 | 自旋锁(关下半部) | spin_lock_bh() |
| 内核抢占 | 中 | 是(进程上下文) | 自旋锁 / 互斥体 | spin_lock() / mutex_lock() |
| 多核 SMP | 持续 | 视上下文而定 | 自旋锁 / 互斥体 / RCU | spin_lock() / atomic_* |
1.5 本章小结
- 并发 ≠ 并行:并发是逻辑上的同时,并行是物理上的同时。单核系统同样存在竞态。
- 竞态条件:多个执行流交错访问共享资源,导致结果不可预测。
- 四大并发源:硬件中断、软中断、内核抢占、多核 SMP。每个并发源对锁的选择有不同约束。
- 锁的选择:中断上下文只能用自旋锁,进程上下文可以用互斥体;关中断的锁开销最大,应谨慎使用。