3. 自旋锁在中断上下文中的使用规则

3.1 中断上下文定义

中断上下文(Interrupt Context)是 Linux 内核中一种特殊的执行环境,与进程上下文有本质区别。理解中断上下文是正确使用自旋锁的前提。

对比维度 进程上下文 中断上下文
执行主体 进程(task_struct) 中断处理函数(ISR / SoftIRQ / Tasklet)
可睡眠 是(可调用 schedule()) 否(禁止睡眠、阻塞)
可被抢占 是(内核抢占开启时) 否(中断优先级固定)
持有锁限制 可使用 mutex / spinlock 只能使用 spinlock(且需关本地中断)
典型场景 系统调用、文件读写、驱动 ioctl 硬件中断响应、下半部处理
关键判断准则:在中断上下文中,in_interrupt() 返回非零值。驱动开发中,若不确定当前上下文,应使用 might_sleep() 或 lockdep_assert_irqs_disabled() 进行运行时检查。

3.2 自旋锁在中断中的限制

自旋锁在中断上下文中使用时,必须遵守以下三条硬性限制,否则将直接导致死锁或系统崩溃:

  1. 禁止睡眠:自旋锁的持有者会关闭内核抢占,若在持有期间睡眠,其他 CPU 等待该锁的进程将一直自旋,造成 CPU 空转死锁。
  2. 禁止调用可能导致调度的函数:如 copy_from_user()、kmalloc(GFP_KERNEL)、mutex_lock() 等。
  3. 必须关本地中断:若中断处理函数与进程上下文共享同一自旋锁,必须在获取锁之前禁止本地 CPU 的中断,否则会触发递归死锁。
典型死锁场景:进程 A 持有锁 L,此时本地中断到来,中断处理函数尝试获取锁 L。由于进程 A 被中断抢占且未释放锁,中断处理函数自旋等待,而进程 A 无法继续执行释放锁,形成单 CPU 递归死锁。
中断上下文自旋锁递归死锁示意图 中断上下文自旋锁递归死锁(单 CPU) 进程 A 持有 spinlock L 中断处理函数 尝试获取 spinlock L 中断触发(抢占) 自旋等待 L(死锁) 死锁成因 进程 A 持有锁 L 时被中断抢占 → 中断处理函数自旋等待锁 L → 进程 A 无法继续执行释放锁 → 系统永久死锁

3.3 禁止本地中断的必要性

禁止本地中断(Local IRQ Disable)是解决上述死锁问题的核心手段。其本质是:在获取自旋锁之前,确保当前 CPU 上的中断处理函数不会执行,从而避免递归锁竞争。

Linux 内核提供了以下 API 组合来实现“锁 + 关中断”的原子操作:

API 函数 功能描述 适用场景
spin_lock_irq() 关本地中断 + 获取自旋锁 确定中断状态已知(默认开中断)
spin_unlock_irq() 释放自旋锁 + 开本地中断 与 spin_lock_irq 配对
spin_lock_irqsave() 保存中断状态 + 关中断 + 获取锁 推荐:不确定当前中断状态
spin_unlock_irqrestore() 释放锁 + 恢复中断状态 与 spin_lock_irqsave 配对
最佳实践:在中断上下文与进程上下文共享自旋锁时,始终使用 spin_lock_irqsave() / spin_unlock_irqrestore() 配对。这能安全保存并恢复中断标志,避免因中断状态不一致导致的隐蔽 bug。
禁止本地中断解决死锁流程对比 禁止本地中断解决死锁:流程对比 错误做法:spin_lock() 未关中断 进程 A 执行 spin_lock(L) 中断到来,抢占进程 A 中断处理函数 spin_lock(L) → 死锁 结果:CPU 永久自旋,系统挂死 正确做法:spin_lock_irqsave() 关中断 进程 A 执行 spin_lock_irqsave(L, flags) 本地中断被禁止(IF=0) 中断被屏蔽,不会抢占当前 CPU 进程 A 安全释放锁后恢复中断 结果:无死锁,系统正常运行

3.4 中断上下文自旋锁使用规则总结

综合以上分析,在中断上下文中使用自旋锁必须遵循以下四条核心规则:

  • 规则一:中断上下文只能使用自旋锁,禁止使用 mutex 或信号量。
  • 规则二:持有自旋锁期间,禁止调用任何可能睡眠或调度的函数。
  • 规则三:若锁被进程上下文和中断上下文共享,必须使用 spin_lock_irqsave() / spin_unlock_irqrestore() 禁止本地中断。
  • 规则四:若锁仅在中断上下文之间共享(不同中断优先级),使用 spin_lock() / spin_unlock() 即可,无需关中断。
代码模板:中断上下文与进程上下文共享自旋锁的标准写法
// 共享数据结构
static spinlock_t my_lock;
static int shared_data;

// 进程上下文访问
void process_access(void)
{
    unsigned long flags;
    spin_lock_irqsave(&my_lock, flags);
    shared_data++;
    spin_unlock_irqrestore(&my_lock, flags);
}

// 中断上下文访问
irqreturn_t irq_handler(int irq, void *dev_id)
{
    unsigned long flags;
    spin_lock_irqsave(&my_lock, flags);
    shared_data--;
    spin_unlock_irqrestore(&my_lock, flags);
    return IRQ_HANDLED;
}