2. 自旋锁API详解:spin_lock/spin_unlock、spin_lock_irq/spin_unlock_irq、spin_lock_irqsave/spin_unlock_irqrestore
自旋锁(Spinlock)是Linux内核中最基础的互斥原语之一,特别适用于短临界区和不可睡眠上下文。内核提供了三组核心API,分别对应不同的中断上下文场景。选择错误的变体是导致死锁和性能劣化的常见根源。
2.1 API家族总览与核心差异
三组API的核心区别在于是否操作本地CPU中断使能状态以及是否保存中断标志。下表从功能、适用场景和性能开销三个维度进行对比:
| API 组合 | 中断使能操作 | 保存中断标志 | 适用上下文 | 性能开销 |
|---|---|---|---|---|
spin_lock / spin_unlock |
不操作 | 否 | 进程上下文(无中断竞争) | 最低 |
spin_lock_irq / spin_unlock_irq |
关/开本地中断 | 否 | 进程上下文 + 中断上下文 | 中等 |
spin_lock_irqsave / spin_unlock_irqrestore |
关/恢复本地中断 | 是 | 任何上下文(推荐通用) | 略高(保存标志) |
关键原则:如果在中断处理程序(ISR)中访问了被自旋锁保护的共享数据,则必须在加锁前关闭本地CPU中断。否则,当前进程持有锁时被中断打断,ISR尝试获取同一把锁,将导致自旋死锁。
2.2 逐组API深度解析
2.2.1 spin_lock / spin_unlock —— 纯自旋,不关中断
这是最基本的变体,仅实现忙等待轮询。它不修改本地CPU的中断使能状态。因此,它只能在确定不会与中断上下文发生竞争的场景下使用,例如:
- 临界区仅在进程上下文中被访问(如系统调用、workqueue)。
- 临界区仅在同一个中断处理程序的不同实例间共享(此时中断已隐式关闭)。
// 示例:仅进程上下文竞争
spinlock_t my_lock;
spin_lock_init(&my_lock);
void process_context_func(void) {
spin_lock(&my_lock);
// 临界区:操作共享数据
shared_data->counter++;
spin_unlock(&my_lock);
}
风险提示:如果在持有
spin_lock 锁时发生了中断,且该中断处理程序也尝试获取同一把锁,系统将立即死锁。这是新手最常见的错误之一。
2.2.2 spin_lock_irq / spin_unlock_irq —— 关中断,但不保存状态
该变体在加锁时调用 local_irq_disable() 关闭本地CPU的所有中断,解锁时调用 local_irq_enable() 重新开启。它解决了中断上下文竞争问题,但存在一个隐含陷阱:
- 如果调用前中断已经是关闭状态,解锁时无条件开启中断,可能导致系统中断状态被破坏。
- 不能在中断上下文(ISR)中调用,因为ISR本身可能是在中断关闭状态下执行的。
// 示例:进程上下文 + 中断上下文竞争(仅限进程侧加锁)
spinlock_t dev_lock;
void process_side_func(void) {
spin_lock_irq(&dev_lock);
// 临界区:此时本地中断已关闭,ISR不会抢占
dev->status = BUSY;
spin_unlock_irq(&dev_lock); // 注意:无条件开启中断
}
// 中断处理程序侧(ISR中中断已隐式关闭)
irqreturn_t dev_isr(int irq, void *dev_id) {
spin_lock(&dev_lock); // ISR中只需spin_lock
dev->status = IDLE;
spin_unlock(&dev_lock);
return IRQ_HANDLED;
}
使用限制:
spin_lock_irq 只能在已知中断状态为“开启”的上下文中使用(如系统调用入口)。在中断上下文或中断关闭后调用它是不安全的。
2.2.3 spin_lock_irqsave / spin_unlock_irqrestore —— 安全通用型
这是最推荐的变体。它在关闭中断前,使用 local_irq_save(flags) 将当前中断使能状态保存到 unsigned long flags 变量中;解锁时通过 local_irq_restore(flags) 恢复到加锁前的状态,而不是无条件开启。这保证了:
- 无论加锁前中断是开还是关,解锁后都能正确恢复。
- 可以在任何上下文(进程、中断、软中断、NMI安全区域)中安全调用。
// 示例:通用安全模式,适用于所有上下文
spinlock_t global_lock;
void safe_critical_section(void) {
unsigned long flags;
spin_lock_irqsave(&global_lock, flags);
// 临界区:安全操作共享资源
shared_pool->allocated = true;
spin_unlock_irqrestore(&global_lock, flags);
// 中断状态恢复为加锁前的值
}
最佳实践:在编写驱动时,除非你能100%确定中断状态且不会在中断上下文中调用,否则始终使用
spin_lock_irqsave / spin_unlock_irqrestore。额外的 flags 变量开销微乎其微,但能避免难以调试的竞态条件。
2.3 API选择决策流程
下图展示了在实际驱动开发中,如何根据上下文快速选择正确的自旋锁API变体:
2.4 常见错误模式与排查要点
| 错误模式 | 表现 | 根因 | 修复方案 |
|---|---|---|---|
| 中断上下文死锁 | 系统挂起,CPU占用100% | 进程持有 spin_lock 时被中断,ISR尝试获取同一锁 |
改用 spin_lock_irqsave |
| 中断状态破坏 | 随机中断丢失,外设超时 | 在中断关闭状态下调用 spin_lock_irq,解锁时误开中断 |
改用 spin_lock_irqsave |
| 递归加锁 | 进程自旋死锁 | 同一上下文递归调用 spin_lock |
检查代码路径,或使用 spin_lock_nested |
| flags变量未初始化 | 编译警告或随机行为 | spin_lock_irqsave 的 flags 未定义 |
确保 unsigned long flags 在栈上定义 |
调试技巧:当怀疑自旋锁死锁时,可以通过
/proc/lock_stat 或 lockdep 内核配置(CONFIG_PROVE_LOCKING)来检测潜在的死锁路径。在开发阶段始终开启 LOCKDEP 和 SPINLOCK_DEBUG。
2.5 性能对比:指令开销与缓存影响
从汇编指令层面看,三组API的差异主要体现在对中断标志的操作上。以下是在ARM64架构下的典型指令序列对比:
| API | 关键指令 | 额外延迟(周期) | 适用场景 |
|---|---|---|---|
spin_lock |
ldaxr / stlxr (独占加载/存储) |
~10-20 | 高频短临界区,无中断竞争 |
spin_lock_irq |
上述 + msr daifset, #2 (关中断) |
~30-50 | 进程上下文,已知中断开启 |
spin_lock_irqsave |
上述 + mrs x0, daif (读中断标志) + msr daifset, #2 |
~40-60 | 通用安全,推荐默认选择 |
性能建议:对于绝大多数驱动场景,
spin_lock_irqsave 多出的20-40个CPU周期完全可以忽略。不要为了微小的性能提升而牺牲安全性。只有在经过性能分析确认锁竞争是瓶颈时,才考虑降级为 spin_lock_irq 或 spin_lock。