2. 内核并发源:SMP多核并发、中断上下文、软中断与tasklet、内核抢占
在嵌入式Linux驱动开发中,理解并发源是正确选择和使用锁机制的前提。驱动代码运行在内核空间,面临多种并发执行路径的交叉访问。本章系统梳理四大内核并发源,帮助开发者建立并发场景的全局视图。
核心认知:驱动中的每一个共享数据结构,都必须明确其会被哪些并发路径访问。遗漏任何一个并发源,都可能导致难以复现的竞态条件(Race Condition)。
2.1 并发源全景图
内核中的并发执行流可以抽象为以下四种基本类型。它们可以独立出现,也可以组合叠加,形成复杂的并发环境。
2.2 SMP多核并发
对称多处理(SMP)是现代嵌入式SoC的标准配置。两个或更多CPU核心共享同一内存空间,各自独立执行指令流。这是最直观的并发源:两个进程分别在CPU0和CPU1上同时运行,同时访问驱动中的同一个全局变量。
关键影响:SMP并发是spinlock设计的主要驱动力。spinlock的核心机制就是让等待锁的CPU核心执行“自旋等待”(忙等待),直到锁被持有者释放。对于短临界区,自旋比睡眠-唤醒的开销更低。
SMP并发带来的竞态条件具有以下特征:
- 真正的同时执行:两个核心在同一个时钟周期内访问共享数据。
- 缓存一致性问题:每个核心有私有L1缓存,修改后需要通过缓存一致性协议(如MESI)同步。
- 原子性缺失:对变量的“读-改-写”操作在汇编层面不是原子的,可能被另一个核心打断。
2.3 中断上下文
硬件中断触发后,CPU立即保存当前执行现场,跳转到中断处理程序(ISR)。中断上下文与进程上下文完全不同:
| 对比维度 | 进程上下文 | 中断上下文 |
|---|---|---|
| 可睡眠 | 可以(调用 schedule()) | 禁止(不能使用 mutex) |
| 当前进程 | 有 current 指针 | 无(current 不可靠) |
| 可被抢占 | 是(内核抢占开启时) | 否(中断优先级固定) |
| 锁选择 | spinlock / mutex 均可 | 只能使用 spinlock |
中断上下文是驱动开发中最危险的并发源之一。如果进程上下文持有spinlock时中断发生,而中断处理程序也试图获取同一把spinlock,就会产生死锁。解决方案是使用 spin_lock_irqsave() 在获取锁时关闭本地CPU中断。
// 错误示例:中断中自旋等待进程持有的锁 → 死锁
spinlock_t my_lock;
// 进程上下文
spin_lock(&my_lock);
// 此时中断发生,ISR 也尝试获取 my_lock
// ISR 自旋等待,进程无法释放锁 → 死锁
// 正确做法:关本地中断
unsigned long flags;
spin_lock_irqsave(&my_lock, flags);
// 临界区操作
spin_unlock_irqrestore(&my_lock, flags);
2.4 软中断与tasklet
软中断(SoftIRQ)和tasklet是内核下半部机制的核心组件,用于处理中断处理程序推迟的非紧急工作。它们运行在软中断上下文,具有以下并发特性:
- 软中断:同一类型的软中断可以在多个CPU上同时执行。网络层的
NET_RX_SOFTIRQ和NET_TX_SOFTIRQ是典型例子。 - tasklet:同一tasklet在同一时刻只能在一个CPU上执行,但不同tasklet可以在不同CPU上并行。
- 优先级:软中断的优先级高于用户态进程和内核线程,但低于硬件中断。
注意:软中断和tasklet中不能使用mutex,因为它们不可睡眠。如果软中断访问的共享数据也被进程上下文访问,必须使用spinlock保护,并且进程侧需要关闭软中断(使用
spin_lock_bh())。
2.5 内核抢占
当内核配置了 CONFIG_PREEMPT(抢占式内核),一个在内核态运行的进程可以被另一个更高优先级的进程抢占。这意味着即使只有一个CPU核心,进程上下文内部也存在并发:当前进程执行到临界区中间时,可能被调度出去,另一个进程进入并访问同一共享数据。
内核抢占带来的并发场景:
- 单核抢占:进程A持有锁进入临界区,被抢占;进程B运行,尝试获取同一锁 → 进程B自旋等待(单核spinlock不会自旋,但需要抢占保护)。
- 抢占与中断叠加:进程A被抢占后,中断发生,ISR访问共享数据 → 需要spinlock配合关中断。
spinlock在实现上隐式地禁止了内核抢占:
// spin_lock() 的简化实现逻辑(ARM64 架构)
static __always_inline void spin_lock(spinlock_t *lock)
{
// 1. 禁止内核抢占(preempt_disable())
// 2. 执行原子加锁操作(ldaxr / stxr)
// 3. 如果锁已被持有,自旋等待
}
// 这意味着持有spinlock的代码段不会被其他进程抢占
// 从而保证了临界区的原子性(相对于本CPU的进程上下文)
2.6 并发源与锁选择对照
下表总结了不同并发源场景下推荐的锁机制选择:
| 并发场景 | 推荐锁 | 关键注意事项 |
|---|---|---|
| 进程 vs 进程(SMP) | spinlock / mutex | 临界区短用spinlock,长用mutex |
| 进程 vs 中断 | spinlock_irqsave | 必须关闭本地中断,防止死锁 |
| 进程 vs 软中断 | spinlock_bh | 关闭本地软中断,防止软中断抢占进程 |
| 中断 vs 中断 | spinlock | 同一CPU上中断不会嵌套(除非中断优先级不同) |
| 软中断 vs 软中断 | spinlock | 同一软中断可在多核并行,必须加锁 |
| tasklet vs tasklet | spinlock | 不同tasklet可并行,同一tasklet串行 |
经验法则:在驱动开发中,如果不确定并发场景,最安全的做法是使用
spin_lock_irqsave()。它同时关闭本地中断和内核抢占,能防御除SMP多核之外的所有单核并发源。但要注意,过度使用会增大中断延迟,影响系统实时性。