2. 中断上下文:进程上下文与中断上下文的区别、中断上下文中的限制
中断处理程序运行在一个特殊的执行环境中,称为中断上下文。理解中断上下文与普通进程上下文的本质区别,是编写正确、高效中断处理程序的前提。任何在中断上下文中违反其限制的操作(如睡眠、调度)都会导致系统崩溃或死锁。
2.1 进程上下文 vs 中断上下文
Linux 内核将 CPU 的执行状态划分为两大类上下文。下表从多个维度对比了它们的核心差异:
| 对比维度 | 进程上下文 | 中断上下文 |
|---|---|---|
| 触发方式 | 系统调用、异常、内核线程 | 硬件中断信号(IRQ) |
| 关联进程 | 有明确的 current 进程(task_struct) | 没有进程概念,不可访问 current |
| 调度特性 | 可被调度、可抢占 | 不可调度、不可抢占(顶半部) |
| 睡眠能力 | 允许睡眠(调用 schedule()) | 绝对禁止睡眠 |
| 持有锁 | 可使用互斥锁(mutex) | 只能使用自旋锁(spinlock) |
| 栈空间 | 进程内核栈(通常 8KB/16KB) | 中断栈(通常 4KB/8KB,独立或共享) |
| 执行时间 | 无严格限制(用户态可长) | 必须极短(微秒级,顶半部) |
核心原则:中断上下文是异步闯入的,它“借用”了被打断进程的 CPU 时间片,但完全不依赖该进程的任何资源。因此,任何可能引发阻塞或睡眠的操作在中断上下文中都是致命的。
2.2 中断上下文中的三大限制
基于中断上下文的特殊性,内核强制规定了以下三条不可逾越的红线:
2.2.1 不可睡眠
- 原因:睡眠意味着主动让出 CPU 并等待某个事件。中断上下文没有进程结构(task_struct),无法被唤醒。一旦睡眠,该 CPU 核心上的中断处理将永久挂起,导致系统死锁或中断丢失。
- 后果:调用
schedule()、wait_event()、msleep()等函数会触发内核 BUG(如scheduling while atomic错误)。 - 避坑指南:顶半部中绝对禁止睡眠。底半部(如 tasklet、softirq)同样不可睡眠。只有工作队列(workqueue)运行在进程上下文,才允许睡眠。
2.2.2 不可调度
- 原因:调度器依赖进程的
task_struct进行上下文切换。中断上下文不属于任何进程,调度器无法找到合法的切换目标。 - 后果:显式调用
schedule()或触发抢占点会导致内核崩溃。 - 避坑指南:中断处理程序(尤其是顶半部)必须保持原子性,即从开始到结束不主动让出 CPU。如果需要延迟处理,应委托给底半部或工作队列。
2.2.3 不可使用互斥锁
- 原因:互斥锁(mutex)在锁冲突时会导致任务睡眠。中断上下文无法承受睡眠行为。
- 解决方案:中断上下文中只能使用自旋锁(spinlock)。自旋锁在锁被持有时会原地忙等(spin),不会睡眠,符合原子性要求。
- 注意:使用自旋锁时,必须同时禁用本地 CPU 的中断(
spin_lock_irqsave()),否则中断处理程序可能因重入而自旋死锁。
常见陷阱:在中断处理程序中调用
kmalloc(GFP_KERNEL) 是危险的,因为 GFP_KERNEL 可能触发内存回收而睡眠。应使用 GFP_ATOMIC 标志,该标志保证分配过程不睡眠。
2.3 中断上下文执行流与栈结构
下图展示了中断发生时,CPU 从进程上下文切换到中断上下文,以及中断栈与进程内核栈的关系:
2.4 中断上下文中的安全编程规则
基于上述限制,在中断上下文中编程时应遵循以下清单:
- 使用 GFP_ATOMIC 标志分配内存:如
kmalloc(size, GFP_ATOMIC),避免睡眠。 - 使用自旋锁保护共享数据:配合
spin_lock_irqsave()禁用本地中断,防止死锁。 - 避免访问用户空间:中断上下文无法访问用户态内存地址(
copy_from_user会睡眠)。 - 避免复杂运算:中断栈空间有限(通常 4KB),递归或大数组可能导致栈溢出。
- 委托耗时操作到底半部:顶半部只做最紧急的硬件应答,数据拷贝、协议解析等交给底半部或工作队列。
调试技巧:使用
in_interrupt() 宏可以在运行时判断当前是否处于中断上下文。在编写通用函数时,通过该宏选择不同的锁机制(自旋锁 vs 互斥锁)或内存分配标志(GFP_ATOMIC vs GFP_KERNEL)。
2.5 中断上下文与实时性
在实时系统中,中断上下文对系统延迟有直接影响:
- 中断关闭时间:中断上下文执行期间,本地 CPU 的中断是关闭的(或至少优先级低于当前中断)。过长的顶半部会阻塞其他中断,导致高优先级中断响应延迟。
- 优先级反转:中断上下文与低优先级任务共享自旋锁时,可能阻塞高优先级任务,造成优先级反转。
- 实时保障策略:将中断处理程序线程化(Threaded IRQ),让中断处理运行在可被调度的内核线程中,从而允许使用实时调度策略(如 FIFO、RR)管理中断处理优先级。
中断上下文是 Linux 内核中最高优先级的执行环境之一,但也是最受限的。理解并尊重这些限制,是编写稳定、高效、实时性良好的设备驱动的基础。