4. 中断上下文死锁场景1:中断处理函数中获取已被进程上下文持有的自旋锁

这是自旋锁在中断上下文中使用时的经典死锁场景。当进程上下文(如系统调用、内核线程)已经持有一个自旋锁,而中断处理函数(ISR)试图获取同一个自旋锁时,系统将陷入死锁。理解这一场景的触发机制、排查方法和优化策略,是掌握中断安全自旋锁使用的第一步。

4.1 死锁触发机制

死锁的触发需要满足以下条件:

  1. 进程上下文持有锁:进程A在运行过程中,通过 spin_lock(&lock) 获取了自旋锁。
  2. 中断发生:在进程A持有锁期间,硬件中断触发,CPU 切换到中断上下文执行 ISR。
  3. ISR 尝试获取同一锁:ISR 中执行 spin_lock(&lock),发现锁已被进程A持有。
  4. 自旋等待:ISR 进入自旋等待状态,但进程A因被中断抢占而无法继续执行,无法释放锁。

最终结果:ISR 永远等不到锁释放,系统挂死。

关键点: 中断上下文无法被调度,一旦自旋等待,CPU 将永久卡在 ISR 中。这是与进程上下文自旋等待的本质区别。

4.2 死锁场景 SVG 流程图

下图展示了进程上下文持有锁后,中断处理函数尝试获取同一锁导致的死锁路径:

中断上下文死锁场景1:进程持有锁 + ISR 获取同一锁 中断上下文死锁场景1:进程持有锁 + ISR 获取同一锁 进程上下文 持有 spin_lock(&lock) 硬件中断触发 中断上下文 (ISR) 尝试获取 spin_lock(&lock) 锁状态:已被进程上下文持有 死锁!ISR 自旋等待,进程被中断无法释放锁 CPU 永久卡死 💡 说明: 中断上下文无法被调度,一旦自旋等待,CPU 将永久卡在 ISR 中。进程上下文因被中断抢占而无法继续执行,锁永远无法释放。

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()。