4. 中断上下文死锁场景1

在单CPU(单核)系统中,虽然不存在多核同时访问临界区的硬件并发,但中断上下文与进程上下文之间的抢占仍然会导致严重的死锁问题。这是自旋锁使用中最经典、最容易踩中的陷阱之一。

4.1 场景描述

假设驱动中有一个全局数据结构 dev_data,使用自旋锁 lock 保护。进程上下文(如系统调用 write())和中断处理程序(如设备中断 IRQ)都需要访问该数据。

  • 进程上下文:持有 lock,正在操作 dev_data。
  • 中断触发:在进程持有锁期间,硬件中断到达,CPU 立即切换到中断处理程序。
  • 中断处理程序:尝试获取同一把 lock,但锁已被进程持有,导致自旋等待。

由于是单CPU,中断处理程序自旋时,进程上下文无法继续执行(被中断抢占了),因此永远无法释放锁。系统陷入死锁。

关键结论: 在单CPU上,如果中断处理程序与进程上下文共享同一自旋锁,且进程上下文在持有锁时允许中断,则必然死锁。

4.2 死锁流程图解

下图展示了单CPU上中断上下文与进程上下文争用同一自旋锁的完整死锁路径:

单CPU中断上下文死锁流程 单CPU中断上下文死锁流程 进程上下文 持有 lock,操作 dev_data 硬件中断触发 中断处理程序 尝试获取 lock 锁已被持有 自旋等待(死循环) CPU 100% 占用 进程被中断抢占,无法释放锁 系统死锁 进程与中断互相等待,系统无响应 💡 死锁条件: 1. 进程持有锁时,中断被使能 2. 中断处理程序尝试获取同一把锁 3. 单CPU下,中断自旋导致进程无法继续执行

4.3 死锁条件分析

该场景的死锁满足以下四个必要条件:

条件 在本场景中的体现
互斥 自旋锁保证同一时刻只有一个执行流可以持有锁
持有并等待 进程持有锁,中断等待锁;中断持有CPU,进程等待CPU
不可剥夺 自旋锁不会主动释放,必须由持有者显式释放
循环等待 进程等待中断完成,中断等待进程释放锁
注意: 在多CPU系统中,该场景不一定死锁,因为中断可以在一个CPU上自旋,而进程在另一个CPU上继续执行并释放锁。但单CPU系统是100%死锁的。

4.4 代码示例:死锁复现

以下代码展示了典型的死锁场景:

#include <linux/spinlock.h>
#include <linux/interrupt.h>

static DEFINE_SPINLOCK(dev_lock);
static struct device_data dev_data;

/* 进程上下文:write 系统调用 */
static ssize_t dev_write(struct file *filp, const char __user *buf,
                         size_t count, loff_t *f_pos)
{
    unsigned long flags;
    
    spin_lock_irqsave(&dev_lock, flags);   // 持有锁,同时关闭本地中断
    // 操作 dev_data ...
    // 此时如果中断到来,由于本地中断已关闭,不会触发死锁
    spin_unlock_irqrestore(&dev_lock, flags);
    
    return count;
}

/* 中断处理程序 */
static irqreturn_t dev_interrupt(int irq, void *dev_id)
{
    unsigned long flags;
    
    spin_lock_irqsave(&dev_lock, flags);   // 尝试获取锁
    // 操作 dev_data ...
    spin_unlock_irqrestore(&dev_lock, flags);
    
    return IRQ_HANDLED;
}
关键点: 上述代码中,dev_write() 使用了 spin_lock_irqsave(),它在持有锁的同时关闭了本地中断,因此不会触发死锁。但如果错误地使用 spin_lock()(不关中断),则死锁必然发生。

4.5 错误用法对比

用法 是否死锁 原因
进程:spin_lock()
中断:spin_lock()
是 进程持有锁时中断使能,中断自旋死锁
进程:spin_lock_irq()
中断:spin_lock()
否 进程持有锁时关闭本地中断,中断无法触发
进程:spin_lock_irqsave()
中断:spin_lock_irqsave()
否 两者都保存并关闭中断,安全嵌套

4.6 调试与检测方法

  • 内核配置:开启 CONFIG_DEBUG_SPINLOCK 和 CONFIG_PROVE_LOCKING,内核会在死锁发生时输出警告。
  • 动态检测:使用 lockdep 工具,它能在运行时检测到锁的非法使用顺序。
  • 代码审查:检查所有在中断上下文中使用的自旋锁,确保进程上下文在持有锁时关闭了本地中断。
  • 性能监控:如果CPU占用率突然飙升至100%且系统无响应,可能是中断自旋死锁。
最佳实践: 只要自旋锁被中断处理程序使用,进程上下文必须使用 spin_lock_irqsave() / spin_unlock_irqrestore() 来持有锁,确保在临界区内关闭本地中断。

4.7 总结

单CPU上中断上下文与进程上下文争用同一自旋锁是驱动开发中最常见的死锁场景。其根本原因是中断的优先级高于进程,导致进程在持有锁时被中断抢占,而中断自旋等待锁释放,形成循环等待。解决该问题的核心策略是:在进程上下文持有锁期间,禁止本地中断,从而避免中断处理程序在错误的时间点介入。