异常和中断的基本概念

前四节里,PC 的值只有两个来源:自增(顺序执行)和转移指令写入(跳跃)。两者的共同点是——都由程序自己决定。

这一节引入第三个来源:PC 被硬件强行改写,而当前程序对此一无所知。

“强行”和”无所知”这两个词就是异常与中断的定义性特征,也是本节所有辨析的判据来源。跨到操作系统那边,OS 1.3.2 讲的是同一个机制的软件视角——两科讲同一件事,CO 讲硬件怎样把控制权夺走,OS 讲夺走之后怎么用。

机制

定义与两个判据

异常(内中断)中断(外中断)
来源CPU 内部——当前指令执行过程中CPU 外部——I/O 设备、定时器
与当前指令的关系同步:同样的程序同样的输入,必然在同一条指令上发生异步:与当前执行到哪条指令无关
典型除零、缺页、地址越界、非法操作码、INT n键盘敲击、磁盘完成、时钟中断
能否屏蔽一般不能(自陷类除外)可屏蔽中断可以,NMI 不行
检测时机指令执行过程中发现每条指令执行结束时统一检测

两个判据,一个足够定性、一个足够可操作:

判据一(定性):来源在 CPU 内还是外。

判据二(可操作):把程序重跑一遍,它还会在同一个地方发生吗? 会 → 异常(同步);不一定 → 中断(异步)。

判据二在做题时更好用,因为”内 / 外”有时会有争议(比如时钟中断的定时器可能在片内),而”重跑一遍是否可复现”从不含糊。

术语的混乱与统一

这一节的术语在不同教材、不同体系里不一致,必须先把口径统一。

说法体系”中断”这个词指什么
广义(早期教材、408 常用)一切打断正常执行流的事件,内因外因都算。此时分”内中断 / 外中断”
狭义(现代体系、Intel 手册)只指外部异步事件。内部同步事件叫”异常”

408 两种口径都可能出现,判断方法是看题目有没有用”内中断 / 外中断”这对词:

  • 出现”内中断” → 广义口径,此时内中断 = 异常
  • 只说”异常和中断” → 狭义口径,二者并列

对照记忆表:

广义中断内中断异常外中断狭义的中断

中断的定义性特征:硬件强行改写 PC

这一条是全节最需要焊死的判据,因为下面那个跨科的疑问、以及 5.5.2 的所有分类,全靠它。

一次中断响应里,硬件做了三件程序完全无法察觉的事:

  1. 在指令边界抢下控制权——当前指令刚执行完,下一条还没取
  2. 把 PC 和 PSW 存起来——为了将来能回来
  3. 把 PC 改成中断服务程序的入口地址

关键在于:这三件事没有任何一条指令参与。 程序里找不到一条 CALL 中断服务程序——它是中断隐指令,一段没有操作码、不在主存里、由硬件直接执行的动作。

反过来说,凡是”程序自己调用的”,都不是中断。 这条否定判据比正面定义更常用。

疑问点: addEventListener 那种注册回调的写法算中断吗?是操作系统在管,还是可以直接挂到中断服务程序上

不算中断,而且用户态代码挂不到中断服务程序上。但这个联想的方向是对的——它们确实在同一条链上,只是隔了三层。

先用判据判一次:addEventListener 注册的回调,是被谁调用的?

是被应用程序自己的事件循环调用的。浏览器、Qt、Java Swing 内部都有一个形如

while (true) {
    event = 取出下一个事件();   // 队列空则阻塞
    分发给注册的回调();
}

的循环。回调是这个循环主动调用的一个普通函数——控制权从来没有被夺走过。 按上面那条否定判据:“程序自己调用的,不是中断”。

但从按下一个键到回调被执行,链条上确实有一次真正的中断。完整的六层:

层发生了什么是不是中断
1键盘控制器拉高 IRQ 线——
2CPU 在指令边界响应,硬件改写 PC✅ 这里,且只有这里
3内核中断服务程序读扫描码,放进内核事件队列内核态,ISR 内部
4窗口系统(X11 / Wayland / Windows 消息队列)取走,投递给某个窗口纯软件
5应用的事件循环从队列里取出纯软件
6分发给 addEventListener 注册的回调普通函数调用

“是操作系统在管吗”——是,第 3、4 层都是。 而且这正是分层的意义:硬件中断的处理必须极短(后面还有别的中断在等),所以 ISR 只做”把数据搬进队列”这一件事,真正的处理推迟到应用层慢慢做。这在 OS 里叫上半部 / 下半部的划分,OS 5.1.2 讨论 I/O 控制方式时是同一个思路。


“能不能直接挂到中断服务程序上”——用户态不能,理由是硬性的:

ISR 的运行环境用户态回调
内核态(OS 1.3.1)用户态
不能睡眠、不能缺页——它可能运行在关中断状态下随便睡、随便缺页
用内核栈,栈空间很小用户栈
地址必须在中断向量表里注册,只有内核能写应用自己的数据结构

用户态能拿到的最接近中断的东西是「信号(signal)」——内核在返回用户态时强行改写用户程序的执行流去跑 signal handler。它确实是”被强行插入”的,所以常被称作”软件中断”,但它仍不是硬件中断:投递时机由内核选择,而且只在返回用户态那一刻。


把三种”异步”并排放一次,界限就清楚了:

机制谁改写执行流何时可能发生运行在
硬件中断硬件任意指令边界内核态
信号内核返回用户态那一刻用户态
回调 / 事件循环程序自己事件循环取到事件时用户态

越往下,“异步”越弱、可控性越强。 addEventListener 在最底下一行——它看起来异步,是因为事件到达的时间不确定,而不是因为执行流被夺走。

中断请求的产生与传递

设备提出请求到 CPU 看见它,中间有三个环节:

环节部件说明
提出设备的中断请求触发器 INTR设备就绪时置 1
允许设备的中断屏蔽触发器 MASK被屏蔽的设备请求发不出去
汇总排队器 / 中断控制器多个请求同时到达时判优,见 5.5.3

CPU 侧还有一道总闸:PSW 中的中断允许位 IF(也写作 EINT)。

实际响应设备设备的一条指令刚执行完

四个条件缺一不可,这个乘积式是判断”某中断能否被响应”这类题的完整依据。

为什么中断必须在指令边界响应

这是本章前几节埋下的伏笔在这里收口。

5.1.3 说过:切换只需保存 ISA 状态(GPRs、PC、PSW),IR、MAR、暂存器都不用保存。这个结论成立的前提,就是切换发生在指令边界——那一刻上一条指令已经完整做完、下一条还没开始,那些微体系结构寄存器里没有任何跨指令有用的信息。

如果允许在指令中间响应中断,就必须把暂存器 Y、Z、MAR、MDR、当前微指令的执行进度全部保存下来——而这些东西在不同的 CPU 实现里完全不同,程序就不再可移植了。ISA 与微体系结构的分层(4.1.1)会当场崩塌。

所以”指令边界响应”不是一个实现上的方便,而是 ISA 抽象能够成立的必要条件。 这一点在流水线里会变成”精确异常”这个更难的要求,见 5.6.3 和 5.5.3。

边界

边界辨析:异常 / 中断

  • 异常:内部、同步、可复现——除零、缺页、非法指令、INT n
  • 中断:外部、异步、不可复现——键盘、磁盘、时钟

可操作判据:重跑一遍,还会在同一条指令上发生吗?

边界辨析:广义中断 / 狭义中断

题目出现”内中断”这个词 → 广义口径,内中断 = 异常。 题目只说”异常和中断” → 狭义口径,二者并列。

不要在同一道题里混用两套口径。

层次辨析:硬件中断 / 信号 / 回调

三者都”异步”,但改写执行流的主体不同:

  • 硬件中断:硬件改 PC,任意指令边界
  • 信号:内核改用户执行流,返回用户态时
  • 回调:程序自己的事件循环调用,完全同步

只有第一种是本章讲的中断。

边界辨析:系统调用是异常还是中断

是异常——具体说是自陷(trap)类异常。它由 INT n / syscall 这类指令主动触发,是内部的、同步的、可复现的。

“主动”不影响它是异常:判据是”来源在内还是外、是否同步”,不是”是否有意”。详见 5.5.2 和 OS 1.3.3。

对照速查

事件类型同步/异步
除数为 0异常(故障)同步
缺页异常(故障)同步
访问越界异常(故障或终止)同步
非法操作码异常(故障)同步
INT 0x80 / syscall异常(自陷)同步
断点调试 INT 3异常(自陷)同步
键盘按键中断异步
磁盘传输完成中断异步
时钟中断中断异步
电源故障中断(NMI)异步
Cache 未命中都不是硬件自行处理,程序不可见

最后一行值得单独看:Cache 缺失由硬件完全处理完,不改写 PC、不进入任何服务程序,所以它既不是异常也不是中断。而缺页是异常——因为它需要操作系统介入去读磁盘。判据仍是那一条:PC 有没有被强行改写。 两者的完整对比见 3.6.5。

考点

  • 异常与中断的分辨:给一个事件问类型。用”内/外 + 同步/异步”两条判据。
  • 系统调用属于哪一类:答异常中的自陷,不是中断。
  • 响应的四个条件:设备请求、未被屏蔽、CPU 开中断、一条指令执行结束。
  • Cache 缺失不是异常:高频陷阱,判据是”是否改写 PC 进入服务程序”。
  • 为什么在指令边界响应:答保证保存的状态是完整的 ISA 状态,微体系结构状态无需保存。

链接