2. 自旋锁API详解
自旋锁(Spinlock)是Linux内核中最基础的同步原语之一,特别适用于保护短临界区。其核心行为是:如果锁已被持有,申请者会原地“自旋”等待,直到锁被释放。这种忙等机制避免了上下文切换的开销,但要求临界区必须极短且执行路径不可睡眠。
Linux内核提供了三组自旋锁API,分别对应不同的中断上下文场景。选择错误的API是导致死锁和性能问题的常见根源。
2.1 三组API的核心差异
下表从中断使能状态、适用上下文和性能开销三个维度对比三组API:
| API 组 | 获取锁时对本地中断的影响 | 适用上下文 | 保存/恢复中断状态 | 典型使用场景 |
|---|---|---|---|---|
spin_lock / spin_unlock |
不操作中断使能位 | 进程上下文(无中断竞争) | 否 | 纯进程上下文保护,或已确保中断不会访问共享数据 |
spin_lock_irq / spin_unlock_irq |
获取锁时关闭本地中断,释放锁时无条件开启 | 进程上下文 + 中断上下文(硬中断) | 否(硬开关) | 明确知道进入临界区前中断是开启的,且退出后可以无条件开启 |
spin_lock_irqsave / spin_unlock_irqrestore |
获取锁时保存当前中断状态并关闭本地中断;释放锁时恢复原状态 | 任何上下文(包括中断嵌套、软中断、NMI) | 是(通过 unsigned long flags) |
不确定中断状态、中断嵌套、或需要保持调用者中断状态不变的场景 |
关键原则: 在中断上下文中访问被自旋锁保护的共享数据时,必须使用
spin_lock_irqsave 或 spin_lock_irq,否则当前进程持有锁时被中断打断,中断处理程序尝试获取同一把锁,将导致自我死锁。
2.2 API 原型与行为详解
2.2.1 spin_lock / spin_unlock
#include <linux/spinlock.h>
spinlock_t my_lock;
spin_lock_init(&my_lock);
spin_lock(&my_lock);
/* 临界区:仅限进程上下文,不可睡眠 */
spin_unlock(&my_lock);
- 行为: 仅执行原子性的“测试并设置”(TAS)或“加载链接/条件存储”(LL/SC)操作,不修改中断标志。
- 风险: 如果临界区被中断处理程序访问,且中断处理程序也调用
spin_lock获取同一把锁,则发生死锁。 - 性能: 开销最小,仅涉及内存屏障和原子操作。
2.2.2 spin_lock_irq / spin_unlock_irq
spin_lock_irq(&my_lock);
/* 临界区:本地中断已关闭,可安全被中断上下文访问 */
spin_unlock_irq(&my_lock);
- 行为:
spin_lock_irq在获取锁之前执行local_irq_disable(),spin_unlock_irq在释放锁之后执行local_irq_enable()。 - 风险: 如果调用
spin_lock_irq之前中断已经是关闭状态,那么spin_unlock_irq会错误地开启中断,破坏调用者的中断状态预期。这是常见的隐蔽 bug。 - 适用: 仅在明确知道调用时本地中断处于开启状态时使用。
2.2.3 spin_lock_irqsave / spin_unlock_irqrestore
unsigned long flags;
spin_lock_irqsave(&my_lock, flags);
/* 临界区:中断状态被保存,无论之前中断是开还是关,退出后都能正确恢复 */
spin_unlock_irqrestore(&my_lock, flags);
- 行为:
spin_lock_irqsave将当前处理器的中断使能状态保存到flags变量中,然后关闭本地中断并获取锁。spin_unlock_irqrestore释放锁后,从flags恢复中断状态。 - 安全性: 这是最安全的变体,适用于所有上下文,包括中断嵌套、软中断(softirq)和 NMI 上下文。
- 性能: 比
spin_lock_irq多一次寄存器保存/恢复操作,但避免了因错误开启中断导致的潜在死锁。
常见陷阱: 在中断处理函数中,如果已经处于中断上下文(本地中断默认关闭),此时使用
spin_lock_irq 而非 spin_lock_irqsave,虽然不会立即死锁,但 spin_unlock_irq 会重新开启中断,可能导致中断嵌套失控。始终优先使用 spin_lock_irqsave 是一种安全的编码习惯。
2.3 中断上下文死锁场景:SVG 流程图
下图展示了使用 spin_lock 在进程上下文与中断上下文之间引发死锁的典型路径:
2.4 API 选择决策树
在实际驱动开发中,可以依据以下决策树快速选择正确的API:
- 共享数据是否会被中断上下文访问?
- 否 → 使用
spin_lock/spin_unlock(最低开销) - 是 → 进入第2步
- 否 → 使用
- 调用者是否明确知道当前中断处于开启状态?
- 是,且无嵌套中断风险 → 使用
spin_lock_irq/spin_unlock_irq - 否,或不确定,或存在中断嵌套 → 使用
spin_lock_irqsave/spin_unlock_irqrestore
- 是,且无嵌套中断风险 → 使用
- 临界区是否包含可能睡眠的操作?
- 是 → 不能使用自旋锁,应改用互斥锁(mutex)或信号量
- 否 → 继续使用自旋锁
2.5 三组API的指令流对比:SVG 时序图
下图从CPU指令序列的角度,对比三组API在获取/释放锁时的行为差异:
2.6 最佳实践与避坑指南
推荐策略: 在驱动开发中,除非你100%确定中断状态且无嵌套风险,否则始终使用
spin_lock_irqsave / spin_unlock_irqrestore。这多出的几条指令开销在绝大多数场景下可以忽略,但能避免一类极其隐蔽的死锁 bug。
- 禁止在持有自旋锁时睡眠: 包括
msleep()、copy_from_user()、kmalloc(GFP_KERNEL)等可能导致调度的操作。这会导致系统崩溃或死锁。 - 临界区应极短: 自旋锁的忙等特性决定了临界区应控制在几十条指令以内。超过100微秒的临界区应考虑其他同步机制。
- 注意
flags变量的作用域:flags必须是局部变量,不能是全局或静态变量,否则在中断嵌套场景下会被覆盖。 - 对称使用: 确保
spin_lock_irqsave和spin_unlock_irqrestore使用同一个flags变量,且成对出现。