4. 中断上下文死锁场景1:中断处理函数中获取已被进程上下文持有的自旋锁
这是自旋锁在中断上下文中使用时的经典死锁场景。当进程上下文(如系统调用、内核线程)已经持有一个自旋锁,而中断处理函数(ISR)试图获取同一个自旋锁时,系统将陷入死锁。理解这一场景的触发机制、排查方法和优化策略,是掌握中断安全自旋锁使用的第一步。
4.1 死锁触发机制
死锁的触发需要满足以下条件:
- 进程上下文持有锁:进程A在运行过程中,通过
spin_lock(&lock)获取了自旋锁。 - 中断发生:在进程A持有锁期间,硬件中断触发,CPU 切换到中断上下文执行 ISR。
- ISR 尝试获取同一锁:ISR 中执行
spin_lock(&lock),发现锁已被进程A持有。 - 自旋等待:ISR 进入自旋等待状态,但进程A因被中断抢占而无法继续执行,无法释放锁。
最终结果:ISR 永远等不到锁释放,系统挂死。
关键点: 中断上下文无法被调度,一旦自旋等待,CPU 将永久卡在 ISR 中。这是与进程上下文自旋等待的本质区别。
4.2 死锁场景 SVG 流程图
下图展示了进程上下文持有锁后,中断处理函数尝试获取同一锁导致的死锁路径:
4.3 代码示例:死锁复现
以下代码模拟了上述死锁场景:
#include <linux/spinlock.h>
#include <linux/interrupt.h>
static DEFINE_SPINLOCK(my_lock);
/* 中断处理函数 */
static irqreturn_t my_isr(int irq, void *dev_id)
{
spin_lock(&my_lock); /* 死锁!锁已被进程上下文持有 */
/* 临界区操作 */
spin_unlock(&my_lock);
return IRQ_HANDLED;
}
/* 进程上下文函数 */
static void my_process_func(void)
{
spin_lock(&my_lock); /* 进程持有锁 */
/* 临界区操作,此时可能发生中断 */
/* 如果中断触发,ISR 将尝试获取同一锁,导致死锁 */
spin_unlock(&my_lock);
}
static int __init my_driver_init(void)
{
/* 注册中断 */
request_irq(IRQ_NUM, my_isr, IRQF_SHARED, "my_device", &my_dev);
return 0;
}
⚠️ 注意: 上述代码在实际运行中会导致系统挂死。切勿在生产环境中直接使用。此代码仅用于教学演示死锁场景。
4.4 排查方法
当系统出现疑似此类死锁时,可通过以下方法排查:
| 排查手段 | 操作方式 | 预期输出/现象 |
|---|---|---|
| 查看 /proc/lock_stat | cat /proc/lock_stat |
显示锁的等待次数、自旋时间,可发现异常高的自旋计数 |
| 查看 /proc/lockdep | cat /proc/lockdep |
如果内核开启了 lockdep,会直接报告“possible deadlock”警告 |
| 使用 Magic SysRq | Alt+SysRq+W 或 Alt+SysRq+D | 显示当前 CPU 上所有进程的堆栈,可发现进程卡在 spin_lock 处 |
| 使用 ftrace | echo function > /sys/kernel/debug/tracing/current_tracer |
追踪 spin_lock 调用路径,确认锁的持有者和等待者 |
4.5 优化策略
解决此类死锁的核心原则是:中断上下文中获取自旋锁时,必须使用 spin_lock_irqsave() 变体。该变体在获取锁的同时会关闭本地 CPU 的中断,从而避免死锁。
💡 最佳实践: 如果自旋锁可能被中断上下文访问,则进程上下文也必须使用
spin_lock_irqsave() 来获取锁,以确保中断状态的一致性。
优化后的代码示例:
static DEFINE_SPINLOCK(my_lock);
/* 中断处理函数 */
static irqreturn_t my_isr(int irq, void *dev_id)
{
unsigned long flags;
spin_lock_irqsave(&my_lock, flags); /* 安全:关闭本地中断 + 获取锁 */
/* 临界区操作 */
spin_unlock_irqrestore(&my_lock, flags);
return IRQ_HANDLED;
}
/* 进程上下文函数 */
static void my_process_func(void)
{
unsigned long flags;
spin_lock_irqsave(&my_lock, flags); /* 安全:关闭本地中断 + 获取锁 */
/* 临界区操作,即使发生中断,ISR 也无法获取锁 */
spin_unlock_irqrestore(&my_lock, flags);
}
4.6 优化前后对比
| 对比维度 | 优化前(死锁) | 优化后(安全) |
|---|---|---|
| 锁获取方式 | spin_lock() |
spin_lock_irqsave() |
| 中断状态 | 中断可能随时触发 | 获取锁时关闭本地中断 |
| 死锁风险 | 高(ISR 自旋等待) | 无(ISR 无法抢占) |
| 适用场景 | 仅进程上下文使用 | 进程 + 中断上下文共享 |
4.7 总结
- 死锁本质:中断上下文自旋等待已被进程上下文持有的锁,而进程被中断抢占无法释放锁。
- 核心原则:中断上下文中获取自旋锁,必须使用
spin_lock_irqsave()关闭本地中断。 - 排查工具:lockdep、lock_stat、Magic SysRq、ftrace 是定位此类死锁的有效手段。
- 代码规范:只要自旋锁可能被中断上下文访问,所有获取锁的路径都应使用
spin_lock_irqsave()。