中断赋予了 CPU 随时暂停当前执行流、去处理紧急事件的能力。没有中断机制,就无法实现“抢占式并发”,只能靠代码主动让出或轮询来切换任务,无法强制剥夺 CPU;有了中断,系统就能强制抢占,完全不用依赖代码自觉。另外,中断也让 CPU 核与核之间、CPU 与 IO 设备之间的交流变得更加主动、及时。

中断分为内部中断和外部中断。外部中断的出现,根源在于 CPU 太快而 IO 设备太慢——在 CPU 眼里,发出一条读指令后,往往要等上“半天、几天甚至几个月才能得到回应”,要是啥也不干干等着就太亏了。于是就有两种思路:

  • 轮询:CPU 隔一会儿就去看一眼 IO 完成了没。看得太勤浪费算力,看得太稀延迟又高。
  • 中断:让 IO 设备完成时主动通知 CPU,CPU 再来处理。这就是外部中断。

最原始的想法是每个 IO 设备单独接一根引脚到 CPU,但设备一多引脚完全不够用,所以后来就交给中断控制器这个中间人,由它统一汇总、仲裁,再用少量引脚通知 CPU。

一、 为什么需要中断机制?

中断机制赋予了 CPU 随时暂停当前执行流,转而处理异步事件的能力。这种能力是现代操作系统实现多任务并发和高效硬件交互的基石。

如果没有中断机制,CPU 与外设的交互或任务切换只能依赖轮询(Polling)主动让出(Yield)

  1. 轮询的缺陷:CPU 必须不断查询设备状态。轮询间隔太短会白白消耗 CPU 算力;间隔太长则会导致极高的响应延迟。
  2. 主动让出的局限:要求执行流主动调用 yield 让出 CPU,这依赖于代码的自觉性,无法实现真正的强制抢占。

中断机制将“CPU 主动查询”转变为“设备主动通知”,不仅实现了真正的抢占式并发,还极大地提升了 CPU 与低速 I/O 设备之间的并行效率。

二、 中断的分类与硬件基础

中断分为内部中断(异常、陷阱、系统调用,由 CPU 内部执行指令触发)和外部中断(由外部硬件设备触发)。本文主要探讨外部中断。

1. 传统外部中断与中断控制器

由于 CPU 的处理速度远快于 I/O 设备,CPU 发出指令后往往需要等待较长时间。为了及时接收 I/O 请求,最原始的设计是让每个设备通过独立的物理引脚直接连接 CPU。但随着设备增多,这种方式会耗尽 CPU 引脚。

因此,现代架构引入了中断控制器(Interrupt Controller,如 x86 的 APIC,ARM 的 GIC) 作为中间人:

  • 设备发出中断信号。
  • 中断控制器负责接收信号、进行仲裁和优先级排序。
  • 中断控制器通过单一(或少数)引脚向 CPU 发送最终的中断请求。

2. MSI / MSI-X 中断机制

随着 PCIe 总线的普及,传统引脚中断(Line-based Interrupt)带宽不足且容易共享冲突(传统的一个中断线后可以有多个中断处理程序共享中断线),因此引入了消息信号中断(MSI / MSI-X)

MSI-X 的工作原理: MSI-X 不再依赖物理中断线,而是通过 PCIe 总线上的内存写事务(Memory Write TLP) 来触发。

  1. OS 为设备分配独立的中断向量号(Vector),并在内存中映射一块特定地址(通常是 Local APIC 的 MMIO 地址,如 x86 下的 FEE0_xxxx)。
  2. OS 为设备分配中断向量号,然后把目标地址和携带 Vector 的数据分别写入设备的 MSI-X 表项。这样设备就知道“往哪写、写什么”,触发中断时直接发起一次内存写操作即可。
  3. 当设备需要触发中断时,会直接向该内存地址发起一次内存写操作。
  4. CPU 的 APIC 硬件会拦截/识别这个特定的内存写操作,解析出中断向量号并直接触发中断响应流程,无需 CPU 核心去“监听”内存写入。

三、 中断处理的完整生命周期

当一个外部中断到达 CPU 时,硬件和内核软件会协同完成以下流程:

1. 硬件响应与上下文切换

  1. 指令边界:CPU 在当前指令执行完毕后,检测到中断控制器发来的信号。
  2. 保存现场:CPU 硬件自动将最关键的寄存器(如 PC、状态寄存器等)压入内核栈,确保知道“从哪里被打断”以及“被打断时的状态”。
  3. 关闭全局中断:硬件自动关闭本地 CPU 的全局中断(防止入口汇编代码被再次打断导致栈溢出)。
  4. 跳转入口:CPU 根据中断控制器提供的中断向量号,跳转到内存中预设的固定汇编入口地址,并切换到内核态。

2. 内核核心层处理

  1. 重开中断(视架构而定):在进入安全的 C 语言环境后,在某些架构(如 ARM)或特定配置下,内核会重新打开全局中断,以允许更高优先级的中断嵌套;(注:x86 架构默认通常不开启硬中断嵌套,而是依赖中断控制器的优先级机制)
  2. 屏蔽当前中断线:内核会在中断控制器层面单独 Mask(屏蔽)当前正在处理的硬件中断线,防止该设备在处理期间再次疯狂发送中断(中断风暴)导致内核栈溢出。
  3. 分发处理:通过中断向量号找到对应的“中断描述符”(irq_desc,OS 对中断线的软件抽象),遍历并依次调用挂载在该中断线上的所有中断处理函数(ISR)。

3. 上半部执行(Hard IRQ)

设备驱动注册的核心中断处理函数在此阶段执行。

  • 核心任务:读取硬件状态确认中断源、清除硬件中断标志(ACK,告诉硬件“已收到,请停止发送”)、读取极少量的关键数据。
  • 触发下半部:如果还有大量数据需要处理,上半部会标记一个“下半部”任务,然后迅速返回,以尽快恢复被中断的用户态或内核态代码。

四、 中断下半部机制

Linux 将中断处理分为“上半部”和“下半部”。下半部负责执行耗时较长、可以延迟完成的工作。在硬中断返回用户态或内核调度点之前,内核会检查并执行待处理的下半部任务。

常用的下半部机制有三种:软中断(softirq)、tasklet 和工作队列(workqueue)。注:tasklet已经标记弃用,将用BH工作队列代替

1. 软中断(softirq)

软中断是 Linux 中最基础、优先级最高的下半部机制。

  • 运行环境:在“软中断上下文”中执行,绝对不能阻塞或睡眠
  • 静态定义:软中断在编译时静态确定,最大索引为 31(最多 32 种),无法在运行时动态添加。常用类型及优先级如下(序号越小优先级越高):
优先级 软中断名称 用途说明
0 HI_SOFTIRQ 高优先级 tasklet
1 TIMER_SOFTIRQ 定时器处理
2 NET_TX_SOFTIRQ 网络数据包发送
3 NET_RX_SOFTIRQ 网络数据包接收
4 BLOCK_SOFTIRQ 块设备 I/O 完成
5 IRQ_POLL_SOFTIRQ 中断轮询(低延迟 I/O)
6 TASKLET_SOFTIRQ 普通 tasklet
7 SCHED_SOFTIRQ 调度器相关
8 HRTIMER_SOFTIRQ 高精度定时器
9 RCU_SOFTIRQ RCU 回调处理
  • 触发与执行:上半部通过 raise_softirq()pending 位图中对应的位设为 1。CPU 会从序号 0 开始依次检查并执行已置位的处理函数。
  • 并发特性:执行软中断时本地 CPU 的硬中断是开启的(可被硬中断抢占)。同一种软中断可以在多个 CPU 上并发执行,因此处理函数内部必须做好数据保护(如使用自旋锁或 per-CPU 变量)。

2. tasklet

tasklet 是构建在软中断(HI_SOFTIRQTASKLET_SOFTIRQ)之上的易用机制,由内核提供封装。

  • 调度机制:每个 CPU 维护两个 tasklet 链表(普通和高优先级)。驱动通过 tasklet_schedule() 将任务挂入当前 CPU 的链表。
  • 无锁并发控制:tasklet 保证同一个 tasklet 在同一时刻只能在一个 CPU 上执行,且不会重复调度。它通过两个原子状态位实现互斥,无需使用重量级锁:
    • TASKLET_STATE_SCHED:表示“已被调度”。调度时原子设置,防止重复加入链表。
    • TASKLET_STATE_RUN:表示“正在执行”。执行前原子设置,其他 CPU 发现该位已置位则直接跳过,保证单 CPU 独占执行。

3. ksoftirqd 内核线程

软中断在硬中断返回时会被处理,但为了防止软中断长期占用 CPU 导致用户进程“饥饿”,单次处理有时间和次数限制(如累计超过 2 个 jiffies 或循环 10 次)。 如果达到限制后 pending 位图仍有未处理的软中断,内核会唤醒 ksoftirqd 内核线程。该线程在进程上下文中处理剩余的软中断,既保证了系统的公平性,又能在系统空闲时快速响应。

4. 工作队列(workqueue)

如果下半部任务需要执行耗时操作,甚至需要阻塞/睡眠(如等待 I/O 完成、获取信号量),则必须使用工作队列。

  • 运行环境:工作队列将任务推迟到进程上下文中执行,由专门的内核线程(如默认的 system_wq)负责处理。
  • 特点
    • 运行在进程上下文,可以阻塞和睡眠
    • 可被内核调度器抢占和调度。
    • 相比软中断有一定额外开销,但使用最灵活。
  • 驱动可以使用系统默认工作队列,也可以创建专用的工作队列以满足严格的串行化或特定 CPU 绑定需求。