1. 并发基础:并发与并行、竞态条件与内核并发源

在深入自旋锁与互斥体之前,必须厘清并发与并行的本质区别,理解竞态条件的触发机制,并系统梳理 Linux 内核中所有可能的并发源。这是调试竞态条件的理论基石。

1.1 并发与并行的区别

并发(Concurrency)与并行(Parallelism)常被混用,但在内核开发中必须严格区分:

维度 并发(Concurrency) 并行(Parallelism)
核心定义 多个任务在逻辑上同时执行(交替执行) 多个任务在物理上同时执行(真正同时)
硬件条件 单核 CPU 即可实现(通过时间片轮转) 必须多核 SMP 或多处理器
执行视角 微观串行,宏观并行 微观并行,宏观并行
内核关注点 抢占、中断、软中断导致的交错 多核同时访问共享数据
竞态触发 单核上执行流被中断/抢占 多核同时读写同一变量
关键认知: 并发是竞态条件的充分条件,并行是竞态条件的强化条件。即使单核系统,只要存在中断或抢占,竞态依然存在。
并发 vs 并行 执行对比 并发(单核时间片轮转) CPU 执行流: Task A Task B Task A Task B Task A → 微观上任务交替执行,宏观上“同时”推进 → 单核系统即可产生竞态条件 并行(多核 SMP 同时执行) Core 0: Task A Task C Core 1: Task B Task D → 两个核心在同一时刻执行不同任务 → 共享数据不加锁时必然产生竞态 结论:并发是竞态的必要条件,并行使竞态更容易暴露和触发

1.2 竞态条件(Race Condition)定义

竞态条件是指多个执行流(进程、中断、软中断、内核线程)在没有适当同步的情况下,同时访问共享资源,导致最终结果依赖于执行流的时间交错顺序,从而产生不可预测的错误。

竞态三要素:
  1. 共享资源:全局变量、设备寄存器、链表、缓存等被多个执行流访问的数据结构。
  2. 非原子操作:读-改-写、检查-执行等操作在汇编层面被拆分为多条指令。
  3. 交错执行:两个执行流在操作共享资源时发生时间上的交错。

典型竞态示例(内核中常见的“检查-使用”模式):

/* 竞态示例:检查-使用模式 */
if (list_empty(&head)) {
    /* 在此处可能被其他执行流插入节点 */
    /* 此时 head 不再为空,但条件判断已通过 */
    return NULL;  /* 错误:实际有数据但返回空 */
}
data = list_entry(head.next, struct my_data, node);

1.3 内核并发源分析

Linux 内核是一个高度并发的系统,其并发源主要来自以下四个方面。理解这些并发源是正确选择锁机制的前提。

Linux 内核四大并发源 共享资源 硬件中断 IRQ Handler 软中断 / Tasklet SoftIRQ / BH 内核抢占 preempt_enable/disable 多核 SMP 同时执行 并发源影响范围 中断 > 软中断 > 抢占 > SMP 优先级越高,对锁的约束越严格

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()
多核共享 → 必须用自旋锁或互斥体

1.4 并发源优先级与锁选择对照表

并发源 优先级 能否睡眠 推荐锁类型 关键 API
硬件中断 最高 否 自旋锁(关中断) spin_lock_irqsave()
软中断 / Tasklet 高 否 自旋锁(关下半部) spin_lock_bh()
内核抢占 中 是(进程上下文) 自旋锁 / 互斥体 spin_lock() / mutex_lock()
多核 SMP 持续 视上下文而定 自旋锁 / 互斥体 / RCU spin_lock() / atomic_*

1.5 本章小结

  • 并发 ≠ 并行:并发是逻辑上的同时,并行是物理上的同时。单核系统同样存在竞态。
  • 竞态条件:多个执行流交错访问共享资源,导致结果不可预测。
  • 四大并发源:硬件中断、软中断、内核抢占、多核 SMP。每个并发源对锁的选择有不同约束。
  • 锁的选择:中断上下文只能用自旋锁,进程上下文可以用互斥体;关中断的锁开销最大,应谨慎使用。