一句话结论(30s)
中断是操作系统一切「异步响应」能力的硬件基础——CPU 放下手头的事去处理更紧急的事件,处理完再回来。因为外设(键盘/网卡/时钟)的外部中断和 CPU 自身的内部异常(缺页/除零/系统调用)都必须走「IDT 查表 → 保存现场 → 处理 → 恢复现场 → iret 返回」这条统一路径,所以理解中断就等于理解内核如何与硬件和异常打交道。
核心原理(2min)
- 来源两类:外部(硬件中断,异步)vs 内部(异常,同步)
- 外部中断:INTR(可屏蔽,
CLI/STI控制)vs NMI(不可屏蔽,硬件故障) - 内部异常三类:陷阱(
int 0x80/syscall,故意)、故障(缺页,可修复重执行)、终止(不可恢复) - IDT:256 项门描述符,中断门隐式关中断 vs 陷阱门不关中断
- 处理流程五步:硬件压栈(CS/RIP/RFLAGS 等)→ 查 IDT →
SAVE_ALL保存现场 →do_IRQ处理 →RESTORE_ALL+iretq - 上半部/下半部:上半部关中断只做紧急事,下半部开中断,三种机制 softirq/tasklet/workqueue
底层深入(5-10min)
中断的本质
中断是 CPU 与外部世界交互的核心机制,也是操作系统实现多任务调度、设备驱动、系统调用的硬件基础。从 CPU 的角度看,中断就是”放下手头的事,去处理一个更紧急的事件,处理完了再回来”。
中断可以来自两个方向:
- 外部(硬件中断):键盘按下、网卡收到数据包、时钟滴答
- 内部(异常/Exception):除零错误、缺页、断点调试、系统调用
虽然来源不同,但 CPU 对中断和异常的处理流程基本相同——通过中断向量表找到处理函数,保存现场,执行处理逻辑,恢复现场,返回。
思考:为什么 CPU 需要”中断”而不是”轮询”?如果让 CPU 反复问”键盘有输入吗?网卡有包吗?“,即使什么都没发生也要空转检查,浪费算力。中断把模型反转成”事件驱动”——没事就安心跑别的任务(或 idle 省电),有事才被打断。代价是处理过程要”保存现场/恢复现场”,但换来的是 CPU 利用率质的提升。
外部中断:INTR 与 NMI
可屏蔽中断(INTR)
通过 CPU 的 INTR 引脚传递,由外部设备(通过 8259A PIC 或 APIC)发出。CPU 可以通过 CLI 指令(关中断)暂时忽略 INTR 中断,通过 STI 指令(开中断)恢复响应。
可屏蔽中断是异步的——设备不知道 CPU 当前在做什么,只是发送中断信号然后等待处理。典型场景:键盘控制器检测到按键,通过 8259A 向 CPU 的 INTR 发送中断向量号,CPU 响应后执行键盘中断处理程序。
不可屏蔽中断(NMI)
通过 CPU 的 NMI 引脚传递,不能被 CLI 屏蔽。NMI 用于严重的硬件故障:内存 ECC 错误、电源故障、硬件看门狗超时。当内存条出现不可纠正的错误时,北桥芯片产生 NMI,CPU 必须立即处理,因为继续执行可能产生不可预期的结果。
💭 思考:为什么 NMI 要设计成”不可屏蔽”,偏偏绕过 CLI?——可屏蔽中断处理的是”晚一点处理也没关系”的事件(键盘、网卡、时钟),所以允许用 CLI 暂时屏蔽、把手头关键段跑完。NMI 对应的是内存 ECC 错误、电源故障这类”不马上处理就会数据损坏或硬件损坏”的事件——如果它能被屏蔽,系统就可能带着坏内存继续跑,算出来的结果全是错的。所以 NMI 必须直通 CPU、无视 CLI,牺牲一点”可调度性”换”必须兜住最坏情况”的确定性。
内部中断(异常)
异常是 CPU 在执行指令过程中同步产生的。分为三类:
陷阱(Trap)
指令执行完毕后触发,是故意的异常。最重要的例子是 int 0x80(32位系统调用)和 syscall(64位系统调用)。执行完后返回下一条指令继续执行。调试断点(int 3)也属于陷阱。
故障(Fault)
指令执行过程中发现无法完成,但可以修复。CPU 保存的是触发故障的那条指令的地址,修复后重新执行同一条指令。
**缺页异常(Page Fault)**是最典型的故障。当进程访问的虚拟地址对应的页表项标记为”不在物理内存中”时,MMU 触发缺页异常:
程序访问地址 0x7f...
→ MMU 查页表,发现 Present 位 = 0
→ CPU 触发 #PF(缺页异常,中断向量 14)
→ 跳转到 page_fault_handler
→ 判断:合法地址(从磁盘换入)还是非法地址(SIGSEGV)?
→ 合法:分配物理页框 + 从磁盘读入 + 更新页表
→ 重新执行触发缺页的那条指令(iret 返回的 RIP = 故障指令地址)
思考:为什么缺页被归为”故障(Fault)“而不是”陷阱(Trap)“?关键在于”重执行”——故障保存的是触发它的那条指令的地址,修好后把同一条指令再跑一遍;陷阱保存的是下一条指令的地址,跑完就直接往下走。缺页必须能重执行:把缺的页换进来后,原指令要原样再来一次才能继续,这正是”故障”语义。
终止(Abort)
不可恢复的严重错误,如硬件故障。CPU 无法可靠地保存返回地址,程序只能终止。典型例子:双重故障(在处理一个异常时又发生了另一个异常)。
中断向量表与 IDT
x86 使用中断描述符表(Interrupt Descriptor Table, IDT)来管理所有中断处理程序的入口。CPU 的 IDTR 寄存器存储 IDT 的基址和长度。
IDT 中每项(门描述符)对应一个中断向量号(0-255):
| 向量号 | 类型 | 描述 |
|---|---|---|
| 0 | 故障 | 除零错误 |
| 1 | 陷阱/故障 | 调试异常 |
| 3 | 陷阱 | 断点(int 3) |
| 6 | 故障 | 非法指令(#UD) |
| 13 | 故障 | 一般保护故障(#GP) |
| 14 | 故障 | 缺页异常(#PF) |
| 32-47 | 中断 | 8259A PIC 映射(IRQ 0-15) |
| 0x80 | 陷阱 | Linux 系统调用(32位) |
中断门与陷阱门的唯一区别:中断门会隐式执行 CLI(关中断),陷阱门不会。因此硬件中断使用中断门(防止嵌套中断打扰),系统调用使用陷阱门(允许在处理系统调用时响应更紧急的硬件中断)。
💭 思考:为什么硬件中断和系统调用要”差一门”,一个隐式关中断、一个不关?——从各自要保证什么反推:硬件中断处理的是同一优先级的事件,若处理中被同级中断嵌套打断,中断状态机和内核栈会乱套,所以进入中断门时自动 CLI 关门。系统调用是”主动”陷入,本身不紧急、可以被打断——如果处理系统调用时来了时钟/网卡中断,让更紧急的事件插进来才能保证实时性,所以用陷阱门不关中断。如果反过来(系统调用也关中断),一个慢系统调用就能把时钟中断推迟,系统整体响应变差。
完整的中断处理流程
当硬件中断发生时,CPU 硬件自动完成以下步骤(以 64 位长模式为例):
第一步:硬件自动压栈
CPU 将以下内容压入当前栈(注意,如果发生特权级切换,会先切换到 Ring 0 栈):
SS(仅跨特权级时)
RSP(仅跨特权级时)
RFLAGS
CS
RIP
如果是异常,还可能压入错误码(Error Code),帮助处理程序判断异常原因。
第二步:跳转到中断处理程序
CPU 从 IDT 中取出门描述符,加载新的 CS:RIP,开始执行中断处理入口代码。Linux 内核中所有中断和异常的入口统一在 entry_64.S 中,如 common_interrupt、page_fault。
第三步:保存寄存器现场
入口代码用 SAVE_ALL 宏将所有通用寄存器压入内核栈,构建完整的 pt_regs 结构,作为后续 C 语言处理函数的参数。
第四步:执行 do_IRQ 或具体处理函数
对于硬件中断:common_interrupt → do_IRQ → handle_irq_event → 设备驱动注册的处理函数。以网卡中断为例,最终调用驱动的 NAPI poll 函数,从接收环形缓冲区中取出数据包,交给网络协议栈。
对于缺页异常:page_fault → do_page_fault → handle_mm_fault → __handle_mm_fault,逐级处理 PGD/PUD/PMD/PTE 各级页表,最终分配物理页框。
第五步:恢复现场并返回
处理完成后,通过 RESTORE_ALL 恢复寄存器,执行 iretq 指令。CPU 硬件从栈中弹出 CS:RIP:RFLAGS:RSP:SS,恢复到被中断前的状态。
中断上半部与下半部
中断处理程序在执行时会关闭同级及更低级别的中断(或至少屏蔽当前 IRQ 线)。如果处理时间过长,会导致其他中断丢失,影响系统响应。为此 Linux 将中断处理分为两半:
- 上半部(Top Half):在关中断上下文中执行,只做最紧急的事(如从设备寄存器读数据、清除中断标志、调度下半部)。必须极快完成(微秒级)
- 下半部(Bottom Half):在开中断上下文中执行,完成剩余工作。Linux 提供了三种下半部机制:
- softirq:静态定义的软中断,在网络子系统中大量使用(NET_RX_SOFTIRQ 处理入站数据包)
- tasklet:基于 softirq 的动态下半部,同一 tasklet 不会同时在多核上执行
- 工作队列(workqueue):在内核线程上下文中执行,可以睡眠,适合需要访问磁盘或持有互斥锁的下半部逻辑
思考:为什么非要拆成”上半部/下半部”?因为中断处理程序会关中断(屏蔽同级或更低级中断),如果处理时间太长,后续中断就会丢失。所以上半部只做最紧急、最快的事(读寄存器、清标志、派发下半部),把耗时的活丢给开中断的下半部慢慢做。核心是”关中断的时间越短,系统响应越快”。
总结
中断机制是操作系统一切”异步”和”响应”能力的基础。从外部设备的异步信号到内部缺页异常的同步故障,从 int 0x80 的有意陷阱到除零错误的意外故障,CPU 通过统一的中断向量表机制优雅地处理了所有这些情况。理解中断处理流程(硬件压栈 → IDT 查表 → 保存现场 → 上半部 → 下半部 → 恢复现场 → 返回),是理解操作系统内核运作方式的关键拼图。
章末提问
追问 1:为什么中断要分上半部和下半部?
回答思路:结论先行——为了缩短关中断的时间,避免中断丢失。因为中断处理程序运行时会关中断(屏蔽同级或更低级中断),如果处理时间长,后续中断就被丢弃、系统响应变差;所以上半部只在关中断上下文做最紧急、微秒级的事(读寄存器、清标志、派发下半部),把耗时的活交给开中断的下半部(softirq/tasklet/workqueue)慢慢做。
追问 2:中断门和陷阱门有什么区别?
回答思路:结论先行——唯一区别是进入时是否隐式关中断。因为中断门会隐式执行 CLI(关中断),防止硬件中断嵌套打扰正在处理的中断;陷阱门不关中断。所以硬件中断用中断门,系统调用(int 0x80)用陷阱门——处理系统调用时仍能响应更紧急的硬件中断。
追问 3:缺页中断是故障还是陷阱?为什么它能重执行?
回答思路:结论先行——缺页是”故障(Fault)“,它最大的特点是可修复后重执行。因为故障保存的是触发它的那条指令的地址(而非下一条),内核把缺失的页换入、更新页表后,iret 返回的 RIP 指向原指令,CPU 原样再执行一遍就能继续;而陷阱保存的是下一条指令地址,跑完直接往下走,不具备”重执行”的语义。