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 自旋锁在中断中的限制
自旋锁在中断上下文中使用时,必须遵守以下三条硬性限制,否则将直接导致死锁或系统崩溃:
- 禁止睡眠:自旋锁的持有者会关闭内核抢占,若在持有期间睡眠,其他 CPU 等待该锁的进程将一直自旋,造成 CPU 空转死锁。
- 禁止调用可能导致调度的函数:如
copy_from_user()、kmalloc(GFP_KERNEL)、mutex_lock()等。 - 必须关本地中断:若中断处理函数与进程上下文共享同一自旋锁,必须在获取锁之前禁止本地 CPU 的中断,否则会触发递归死锁。
典型死锁场景:进程 A 持有锁 L,此时本地中断到来,中断处理函数尝试获取锁 L。由于进程 A 被中断抢占且未释放锁,中断处理函数自旋等待,而进程 A 无法继续执行释放锁,形成单 CPU 递归死锁。
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。
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;
}