3. 中断上下文定义:什么是中断上下文、中断上半部与下半部、中断上下文中的锁约束
在 Linux 内核驱动开发中,中断上下文是理解锁机制与死锁问题的核心概念。本章将系统性地定义中断上下文,剖析上半部与下半部的分工,并明确在中断上下文中使用自旋锁时必须遵守的约束条件。
3.1 什么是中断上下文
中断上下文是指 CPU 在执行中断服务例程(ISR)时所处的特殊执行环境。与进程上下文不同,中断上下文不属于任何进程,没有进程控制块(task_struct)关联,也不能主动睡眠或调度。
| 对比维度 | 进程上下文 | 中断上下文 |
|---|---|---|
| 关联进程 | 有,current 指针有效 | 无,current 指向被中断进程 |
| 可睡眠 | 是(通过 schedule()) | 否(禁止睡眠) |
| 可被抢占 | 是(内核抢占开启时) | 否(除非嵌套中断) |
| 锁约束 | 可使用 mutex/semaphore | 只能使用 spinlock 且需关中断 |
| 典型场景 | 系统调用、文件操作 | 硬件中断、软中断、tasklet |
关键判断:在驱动代码中,通过
in_interrupt() 宏可以检测当前是否处于中断上下文。若返回非零值,则必须使用自旋锁且不能调用任何可能睡眠的函数。
3.2 中断上半部与下半部
Linux 将中断处理拆分为两个阶段,以平衡实时性与吞吐量:
- 上半部(Top Half):直接注册为 IRQ 处理函数,执行时间极短(通常微秒级),只完成最紧急的硬件操作(如清除中断标志、拷贝寄存器数据)。
- 下半部(Bottom Half):推迟执行的剩余工作,可被调度且允许被新中断打断。常见机制包括软中断(SoftIRQ)、tasklet、工作队列(workqueue)。
上半部 vs 下半部速查表
| 特性 | 上半部 | 下半部 |
|---|---|---|
| 执行时机 | 中断触发后立即执行 | 上半部返回后,由内核调度 |
| 中断屏蔽 | 默认关闭本地 CPU 中断 | 软中断/tasklet 可被新中断打断 |
| 执行时间 | 极短(< 100 μs) | 相对较长(ms 级) |
| 可睡眠 | 否 | tasklet 不可;workqueue 可 |
| 锁类型 | spinlock_irqsave | spinlock_bh 或 spinlock |
3.3 中断上下文中的锁约束
在中断上下文中使用自旋锁,必须遵循以下三条硬性约束,否则将直接导致死锁或数据损坏:
- 禁止睡眠:中断上下文不能调用
schedule()、wait_event()、mutex_lock()等可能睡眠的函数。自旋锁的“自旋等待”本质是忙等,不涉及睡眠,因此是唯一可用的锁类型。 - 关中断保护:若中断处理函数与进程上下文共享同一自旋锁,必须在获取锁前关闭本地 CPU 中断。否则,当进程持有锁时发生中断,中断处理函数尝试获取同一锁,将导致单 CPU 死锁。
- 下半部锁变体:在下半部(软中断/tasklet)中访问共享数据时,应使用
spin_lock_bh(),该函数在获取锁的同时禁止软中断,防止下半部与自身或进程上下文形成死锁。
死锁风险场景:进程上下文持有锁 A,此时发生中断,中断处理函数尝试获取锁 A。由于进程上下文不会主动释放锁,中断处理函数将永远自旋等待——这就是经典的中断上下文死锁。解决方案:在进程上下文中使用
spin_lock_irqsave() 保存并关闭中断。
3.4 锁选择速查表
根据上下文类型,选择正确的锁变体是避免死锁的第一步:
| 上下文组合 | 推荐锁变体 | 说明 |
|---|---|---|
| 进程上下文 vs 进程上下文 | spin_lock() / mutex_lock() |
可睡眠时优先使用 mutex |
| 进程上下文 vs 上半部 | spin_lock_irqsave() |
必须关本地中断,防止死锁 |
| 进程上下文 vs 下半部 | spin_lock_bh() |
关软中断,避免与 tasklet 死锁 |
| 上半部 vs 上半部 | spin_lock() |
同一 CPU 不会嵌套,不同 CPU 安全 |
| 下半部 vs 下半部 | spin_lock_bh() |
防止同一软中断实例重入 |
实践建议:当不确定中断上下文是否会与当前路径冲突时,优先使用
spin_lock_irqsave()。虽然会略微增加中断延迟,但能从根本上避免死锁风险。在性能敏感路径中,再通过代码审查逐步降级为 spin_lock_bh() 或 spin_lock()。