2. 中断上下文与进程上下文:中断处理程序的特点、中断上下文限制、为什么中断中不能睡眠

在实时内核中,中断处理程序运行在一种特殊的执行环境——中断上下文中。理解中断上下文与进程上下文的本质区别,是掌握中断线程化与锁替换的前提。本章将深入剖析中断处理程序的特点、中断上下文的严格限制,以及“中断中不能睡眠”这一核心规则背后的技术原理。

2.1 中断上下文 vs 进程上下文:核心差异

内核根据当前执行流的来源,将运行环境划分为两种上下文。下表从多个维度对比了二者的关键差异:

对比维度 中断上下文 进程上下文
触发来源 硬件中断信号(IRQ)或软中断(SoftIRQ) 系统调用、异常、内核线程主动调度
关联进程 无关联进程,current 宏指向被中断的进程 有明确的关联进程,current 指向当前进程
调度能力 不可调度,不能主动触发进程切换 可调度,可通过 schedule() 主动让出 CPU
睡眠能力 绝对禁止睡眠或阻塞 允许睡眠、等待锁、等待 I/O
栈空间 使用内核栈的固定中断栈(通常 4KB~16KB) 使用进程内核栈(通常 8KB)
锁机制 只能使用自旋锁(Spinlock) 可使用互斥锁(Mutex)、信号量、读写锁等
核心要点:中断上下文是“借壳运行”——它借用被中断进程的内核栈,但不拥有该进程的任何资源。因此,任何可能导致进程切换或阻塞的操作在中断上下文中都是非法的。

2.2 中断处理程序的特点

中断处理程序(Interrupt Handler)是内核响应硬件事件的“第一响应者”。其设计必须遵循以下特点:

  • 快速执行:中断处理期间,同级或更低优先级的中断被屏蔽,必须尽快完成关键操作(如清除中断标志、拷贝数据),将耗时工作推迟到下半部(Bottom Half)。
  • 不可重入:同一中断处理程序在同一个 CPU 上不会嵌套执行,但不同 CPU 上可能并行执行同一中断处理程序(SMP 系统)。
  • 无进程上下文:无法访问用户空间地址,无法使用 copy_from_user() 等与进程相关的函数。
  • 原子性要求:中断处理程序内部必须保证对共享资源的访问是原子的,通常使用自旋锁保护。
实战提示:在实时系统中,中断处理程序应遵循“快进快出”原则。典型做法是:中断处理程序只做硬件确认和数据入队,然后触发一个内核线程(中断线程化)完成剩余处理。

2.3 中断上下文的严格限制

中断上下文施加了以下严格的编程限制,违反这些限制将导致内核崩溃或死锁:

  1. 禁止睡眠或阻塞:不能调用 schedule()、wait_event()、msleep() 等任何可能触发调度的函数。
  2. 禁止使用互斥锁:互斥锁(Mutex)在锁冲突时会导致调用者睡眠,这在中断上下文中是致命的。
  3. 禁止访问用户空间:中断上下文与任何进程无关,无法通过 current 访问用户态内存。
  4. 禁止执行复杂的文件系统操作:如 printk() 在中断上下文中应谨慎使用(可能触发控制台输出调度)。
  5. 栈空间有限:中断栈通常只有 4KB~16KB,递归调用或大量局部变量可能导致栈溢出。
危险操作示例:在中断处理程序中调用 kmalloc(GFP_KERNEL) 是危险的,因为 GFP_KERNEL 标志允许睡眠等待内存。应使用 GFP_ATOMIC 标志,该标志保证不睡眠。

2.4 为什么中断中不能睡眠?—— 深度技术解析

中断中不能睡眠的根本原因在于:中断上下文没有独立的进程控制块(task_struct)和调度实体。下面通过一个 SVG 流程图展示睡眠操作在中断上下文中引发的连锁崩溃:

中断上下文中调用睡眠函数的崩溃流程 硬件中断发生 进入中断上下文 中断处理程序调用 sleep() 调度器尝试进程切换 内核崩溃 / 死锁 崩溃原因 中断上下文无 task_struct 调度器无法找到睡眠对象 内核栈无法恢复

具体来说,中断中不能睡眠的原因可归纳为以下三点:

原因 技术解释
1. 无独立调度实体 中断上下文没有自己的 task_struct,调度器无法将中断上下文放入等待队列或进行上下文切换。调用 schedule() 时,调度器会尝试操作一个不存在的进程控制块,导致内核访问非法内存。
2. 中断栈不可恢复 中断上下文使用被中断进程的内核栈。如果中断处理程序睡眠,被中断进程的栈帧会被破坏。当睡眠被唤醒时,内核无法正确恢复中断前的执行状态,导致栈损坏或指令指针错乱。
3. 中断屏蔽与死锁风险 中断处理程序执行期间,通常屏蔽了同级或更低优先级的中断。如果中断处理程序睡眠,中断屏蔽状态将持续,导致系统无法响应其他硬件事件。更严重的是,如果睡眠过程中需要获取的自旋锁被其他 CPU 上的中断处理程序持有,将形成死锁。
实时系统启示:中断线程化(Interrupt Threading)正是为了解决这一矛盾而设计的。它将中断处理程序的下半部移至一个可睡眠的内核线程中执行,从而允许使用互斥锁等更丰富的同步机制,同时避免中断上下文中的睡眠限制。

2.5 中断上下文中的锁选择:自旋锁的唯一性

由于中断上下文不能睡眠,因此自旋锁(Spinlock)是中断上下文中唯一安全的锁机制。自旋锁在锁冲突时不会让调用者睡眠,而是原地忙等(spin),直到锁被释放。下表对比了自旋锁与互斥锁在中断上下文中的适用性:

锁类型 锁冲突行为 中断上下文可用? 原因
自旋锁 忙等(spin),不睡眠 ✅ 可用 不触发调度,不依赖进程上下文
互斥锁 睡眠等待,触发调度 ❌ 不可用 睡眠操作在中断上下文中非法
读写锁(spin) 忙等(spin),不睡眠 ✅ 可用 自旋锁变体,适用于读多写少场景
信号量 可能睡眠 ❌ 不可用 信号量允许睡眠等待,违反中断上下文规则
自旋锁使用陷阱:在中断上下文中使用自旋锁时,必须使用 spin_lock_irqsave() / spin_unlock_irqrestore() 变体,以在获取锁的同时禁用本地 CPU 的中断,防止中断处理程序与进程上下文之间形成死锁。

2.6 总结:中断上下文的核心约束

中断上下文是内核中一个“受限”的执行环境,其核心约束可以概括为:

  • 无进程关联:不能访问用户空间,不能使用进程相关 API。
  • 不可调度:不能主动或被动触发进程切换。
  • 不可睡眠:任何可能导致睡眠的操作都是非法的。
  • 锁受限:只能使用自旋锁及其变体。
  • 栈受限:栈空间极小,禁止深度递归或大数组。