中断赋予了 CPU 随时暂停当前执行流、去处理紧急事件的能力。没有中断机制,就无法实现“抢占式并发”,只能靠代码主动让出或轮询来切换任务,无法强制剥夺 CPU;有了中断,系统就能强制抢占,完全不用依赖代码自觉。另外,中断也让 CPU 核与核之间、CPU 与 IO 设备之间的交流变得更加主动、及时。
中断分为内部中断和外部中断。外部中断的出现,根源在于 CPU 太快而 IO 设备太慢——在 CPU 眼里,发出一条读指令后,往往要等上“半天、几天甚至几个月才能得到回应”,要是啥也不干干等着就太亏了。于是就有两种思路:
- 轮询:CPU 隔一会儿就去看一眼 IO 完成了没。看得太勤浪费算力,看得太稀延迟又高。
- 中断:让 IO 设备完成时主动通知 CPU,CPU 再来处理。这就是外部中断。
最原始的想法是每个 IO 设备单独接一根引脚到 CPU,但设备一多引脚完全不够用,所以后来就交给中断控制器这个中间人,由它统一汇总、仲裁,再用少量引脚通知 CPU。
一、 为什么需要中断机制?
中断机制赋予了 CPU 随时暂停当前执行流,转而处理异步事件的能力。这种能力是现代操作系统实现多任务并发和高效硬件交互的基石。
如果没有中断机制,CPU 与外设的交互或任务切换只能依赖轮询(Polling)或主动让出(Yield):
- 轮询的缺陷:CPU 必须不断查询设备状态。轮询间隔太短会白白消耗 CPU 算力;间隔太长则会导致极高的响应延迟。
- 主动让出的局限:要求执行流主动调用
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) 来触发。
- OS 为设备分配独立的中断向量号(Vector),并在内存中映射一块特定地址(通常是 Local APIC 的 MMIO 地址,如 x86 下的
FEE0_xxxx)。 - OS 为设备分配中断向量号,然后把目标地址和携带 Vector 的数据分别写入设备的 MSI-X 表项。这样设备就知道“往哪写、写什么”,触发中断时直接发起一次内存写操作即可。
- 当设备需要触发中断时,会直接向该内存地址发起一次内存写操作。
- CPU 的 APIC 硬件会拦截/识别这个特定的内存写操作,解析出中断向量号并直接触发中断响应流程,无需 CPU 核心去“监听”内存写入。
三、 中断处理的完整生命周期
当一个外部中断到达 CPU 时,硬件和内核软件会协同完成以下流程:
1. 硬件响应与上下文切换
- 指令边界:CPU 在当前指令执行完毕后,检测到中断控制器发来的信号。
- 保存现场:CPU 硬件自动将最关键的寄存器(如 PC、状态寄存器等)压入内核栈,确保知道“从哪里被打断”以及“被打断时的状态”。
- 关闭全局中断:硬件自动关闭本地 CPU 的全局中断(防止入口汇编代码被再次打断导致栈溢出)。
- 跳转入口:CPU 根据中断控制器提供的中断向量号,跳转到内存中预设的固定汇编入口地址,并切换到内核态。
2. 内核核心层处理
- 重开中断(视架构而定):在进入安全的 C 语言环境后,在某些架构(如 ARM)或特定配置下,内核会重新打开全局中断,以允许更高优先级的中断嵌套;(注:x86 架构默认通常不开启硬中断嵌套,而是依赖中断控制器的优先级机制)。
- 屏蔽当前中断线:内核会在中断控制器层面单独 Mask(屏蔽)当前正在处理的硬件中断线,防止该设备在处理期间再次疯狂发送中断(中断风暴)导致内核栈溢出。
- 分发处理:通过中断向量号找到对应的“中断描述符”(
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_SOFTIRQ 和 TASKLET_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 绑定需求。