Appearance
异常与中断机制
概念
异常与中断是 CPU 在执行程序的正常流程中,被 CPU 内部或外部的某个事件打断,转而执行一段专门的处理程序,处理完再回到原来的位置。
一句话说清它是什么:中断机制让 CPU 有了"随时能被打断、还能准确地接回去"的能力。没有它,键盘按一下就丢失、硬盘传完没人知道、程序出错只能整机重启。
它与第 32 篇的关系:流水线让多条指令"叠着飞";中断恰恰要求"把飞在半空的指令理清楚"——这就是"精确异常"的由来。中断是流水线设计里最麻烦的一件事。
原理
一、两类来源:内中断与外中断
┌──────────────── 中断(广义)────────────────┐
│ │
┌───────────┴───────────┐ ┌───────────────┴───────────────┐
│ 内中断(异常 exception)│ │ 外中断(中断 interrupt) │
│ 来源 = CPU 内部/指令本身│ │ 来源 = CPU 外部(I/O、时钟) │
└───────────┬───────────┘ └───────────────┬───────────────┘
│ │
┌───────────┼───────────┐ ┌───────────────┴───────────────┐
故障 自陷 终止 可屏蔽 INTR 不可屏蔽 NMI
fault trap abort (IF=1 才响应) (无条件响应)| 类别 | 名称 | 触发者 | 典型例子 | 处理完的返回位置 |
|---|---|---|---|---|
| 内中断 | 故障 fault | 指令执行中出错 | 缺页、除零、越界、非法操作码 | 返回当前指令,重新执行 |
| 内中断 | 自陷 trap | 指令主动请求 | 系统调用、断点、单步 | 返回下一条指令 |
| 内中断 | 终止 abort | 硬件致命故障 | 校验错、总线故障、电源故障 | 不返回,直接停机或重启 |
| 外中断 | 可屏蔽 INTR | I/O 完成、时钟 | 键盘、磁盘、定时器 | 返回下一条指令 |
| 外中断 | 不可屏蔽 NMI | 紧急事件 | 电源故障、内存校验错 | 返回下一条指令 |
最容易混淆的一条:故障(fault)与自陷(trap)的返回地址不一样。
- 故障:出错指令没有完成,返回时要重执行它(缺页例外处理完就重执行
lw,除零则通常终止进程)。 - 自陷:指令是故意执行
syscall/int的,返回时要执行下一条(已完成的不能再做一遍)。 - 两者都保存"指令自身"的地址,但恢复时的用法不同:故障是"重新执行",自陷是"PC 加长"。
二、中断响应过程(中断隐指令)
中断响应条件(三条全满足才响应):
- 当前指令执行结束(中断只在指令边界响应,不会打断一条指令的执行)
- 有中断请求
- 对可屏蔽中断,IF = 1(开中断)
中断隐指令:CPU 响应中断后,由硬件自动完成的一串操作。它不是一条真正的指令(没有指令码、程序员不可见),所以叫"隐"指令:
① 关中断 IF <- 0 (防止响应过程本身被打断)
② 保存断点 (PC) -> 栈 (保存的是下一条要执行的指令地址)
③ 引出中断服务程序 中断源识别 -> 得到入口地址 -> (PC) <- 入口地址记忆钩子:"关、存、引"三个字——关中断、保存断点、引出服务程序。这三件事是硬件干的,接下来才是软件(中断服务程序)干的。
中断服务程序(软件)负责的部分:
④ 保存现场(通用寄存器、PSW 等 —— 断点 PC 已由硬件存了)
⑤ 开中断(★ 若允许多重中断,必须在这里提前开,见下)
⑥ 执行设备服务(读数据、清中断标志……)
⑦ 关中断
⑧ 恢复现场
⑨ 开中断 + 中断返回(IRET:从栈恢复 PC 与 PSW)中断隐指令(硬件) 中断服务程序(软件)
────────────────── ──────────────────────
关中断 保存通用寄存器/PSW
保存断点 PC 开中断 ← 多重中断的开关
取中断服务程序入口地址 设备服务
关中断 → 恢复现场 → 开中断 → IRET中断服务程序必须"保存现场"的原因:中断是随机插入的,主程序正在用 $t0,被中断者的 $t0 会被服务程序覆盖——不保存就回不去。而断点 PC 由硬件保存,因为硬件才知道"精确的返回位置"。
三、中断源识别(怎么知道是谁在请求)
| 方式 | 做法 | 特点 |
|---|---|---|
| 硬件排队器 | 所有请求线经一个优先权排队电路,直接输出优先级最高的源的编码 | 最快,纯硬件 |
| 链式查询 | INTA 应答信号沿链依次传递(第 40 篇的链式仲裁) | 结构简单,离控制器越近优先级越高,对故障敏感 |
| 软件查询 | 服务程序里逐个查询各设备的状态寄存器 | 慢,但优先级可灵活调整 |
四、中断向量与中断向量表
- 中断向量(interrupt vector)= 中断服务程序的入口地址。
- 中断向量表 = 把各中断源的入口地址按中断类型号顺序排列的一张表,放在内存的固定低地址区。
- 向量地址 = 表项在向量表中的存放地址。
以经典 x86 实模式口径为例:表项 4 字节、类型号 0~255:
⚠️ 口径提醒:不同教材对"中断向量"的用法不一致——有的把"中断服务程序入口地址"叫中断向量,有的把"表项地址"(即我这里的向量地址)叫中断向量。答题时先看题目给的量,跟着题目的用法走,并把自己的定义写清楚。
中断类型号怎么来:可屏蔽中断由中断控制器(如 8259A)给出;内部异常由 CPU 内部预先分配固定类型号(除零、缺页、非法指令各占一个)。
五、多重中断(中断嵌套)与中断屏蔽字
多重中断:一个中断服务程序还没执行完,就被更高级的中断打断,形成嵌套。
两个必要条件:
- 服务程序里提前开中断(执行
EI),否则当前服务期间 IF = 0,谁来都被挡住 - 有优先级 / 屏蔽机制,保证只有"更高级"的中断才允许打断
中断屏蔽字(interrupt mask):每个中断源配一个屏蔽字,位序对应各中断源,1 = 屏蔽,0 = 允许。
由屏蔽字读优先级的规则:
- 屏蔽字里 1 的个数越多 → 能屏蔽的源越多 → 优先级越高。
- 每级的屏蔽字必然把自己那一位写成 1——否则自己会打断自己,形成死循环。
- 处理次序(高 → 低)= 屏蔽字中 1 的个数从多到少排序。
一句话记忆:"屏蔽字写出了 '我在跑的时候谁不许动';谁能让更多人不许动,谁就最横。"
六、异常与中断的处理差异(对照表)
| 对比项 | 内中断(异常) | 外中断(硬件中断) |
|---|---|---|
| 来源 | CPU 内部(指令执行产生) | CPU 外部(I/O、时钟、电源) |
| 产生时刻 | 指令执行过程中,与指令同步 | 与当前指令无关,异步随机 |
| 响应时刻 | 通常在指令执行中/结束时立即 | 必须等当前指令执行完 |
| 返回地址 | 故障保存当前指令地址;自陷保存下一条 | 保存下一条指令地址 |
| 是否可屏蔽 | 不可屏蔽(是程序自身的错) | 可屏蔽(INTR)或不可屏蔽(NMI) |
| 典型响应 | 缺页 → 调入页面 → 重执行 lw | 磁盘读完 → 取数据 → 继续下一条 |
示例
例 1:由中断屏蔽字求处理次序(经典必考)
某机有 4 个中断源 A、B、C、D,位序为 A、B、C、D(从左到右)。各级的屏蔽字如下(1 = 屏蔽)。求各中断源的处理次序。
| 中断源 | 屏蔽字 |
|---|---|
| A | 1 0 0 0 |
| B | 1 1 0 0 |
| C | 1 1 1 0 |
| D | 1 1 1 1 |
完整推理过程:
第一步,逐级解读屏蔽字(每位的含义:该位为 1 表示"我在服务时,这个源被禁止"):
| 中断源 | 屏蔽字 | 屏蔽了谁 | 允许谁打断我 |
|---|---|---|---|
| A | 1 0 0 0 | A | B、C、D |
| B | 1 1 0 0 | A、B | C、D |
| C | 1 1 1 0 | A、B、C | D |
| D | 1 1 1 1 | A、B、C、D | 无 |
第二步,数屏蔽字里的 1 的个数(这直接决定优先级):
| 中断源 | 1 的个数 | 优先级 |
|---|---|---|
| A | 1 | 最低 |
| B | 2 | 次低 |
| C | 3 | 次高 |
| D | 4 | 最高 |
第三步,按 1 的个数从多到少排出处理次序:
自检:A 的屏蔽字是 1000,只屏蔽自己——意味着 A 在跑时 B、C、D 都能打断它,所以 A 优先级最低,与结论一致 ✓
⚠️ 三个高频错误:
- 把"1 = 允许"记反了。1 = 屏蔽(禁止),0 = 允许。记反了整道题全错。
- 忘了"各级都要屏蔽自己"——屏蔽字的对应位必然是 1(表中 A→第 1 位 1、B→第 2 位 1、C→第 3 位 1、D→第 4 位 1 都在),这一位是"防自我嵌套",不影响优先级判断,但漏写会被扣分。
- 把"处理次序"与"响应次序"混为一谈。响应次序看优先级(谁最高先响应),处理次序看屏蔽字(谁能屏蔽谁)。本题两者恰好一致(D>C>B>A),但若屏蔽关系与优先级不一致,处理次序以屏蔽字为准。
例 2:多重中断的嵌套时标图
承接例 1 的屏蔽字。主程序运行中,4 个中断按下表时刻到达。各中断的服务时间(ISR 执行所需时间)也已给出。画出从 t = 0 到 t = 40 的中断嵌套时标。
| 中断源 | 到达时刻 | 服务时间 |
|---|---|---|
| A | t = 5 | 10 |
| B | t = 8 | 6 |
| C | t = 12 | 4 |
| D | t = 30 | 5 |
完整推演过程(逐个时刻判断"能否打断"):
| 时刻 | 事件 | 判断依据 | 结果 |
|---|---|---|---|
| t = 5 | A 请求 | 主程序无屏蔽 | A 开始 |
| t = 8 | B 请求 | A 的屏蔽字 1000,B 位 = 0 → 允许 | B 打断 A,A 挂起(剩 7) |
| t = 12 | C 请求 | B 的屏蔽字 1100,C 位 = 0 → 允许 | C 打断 B,B 挂起(剩 2) |
| t = 16 | C 服务完毕(12+4) | C 的屏蔽字 1110 屏蔽 A、B、C;无挂起者被允许 | 回到 B(剩 2) |
| t = 18 | B 服务完毕(16+2) | 挂起者只剩 A | 回到 A(剩 7) |
| t = 25 | A 服务完毕(18+7) | 无挂起中断 | 主程序恢复运行 |
| t = 30 | D 请求 | D 的屏蔽字 1111,不可被任何源打断 | D 开始(打断主程序) |
| t = 35 | D 服务完毕(30+5) | — | 主程序恢复运行 |
时标图:
t: 0 5 8 12 16 18 25 30 35 40
| | | | | | | | | |
主程序 ████████████████████████████████████████████████████
│ │ │ │ │ │ │ │ │
A │ ├────────┤ │ │ │ │ │
│ │ A(3) │ │ │ │ │ │
│ │ ├────────┤ │ │ │ │
B │ │ │ B(4) ├───┤ │ │ │
│ │ │ │ │ B(2) │ │ │
C │ │ │ ├────┤ │ │ │ │
│ │ │ │C(4)│ │ │ │ │
A(续)│ │ │ │ │ ├───────┤ │ │
│ │ │ │ │ │ A(7) │ │ │
D │ │ │ │ │ │ │ ├────┤
│ │ │ │ │ │ │ │D(5)│核心账目校验:
结论:主程序被"5 → 25"这 20 个单位时间和"30 → 35"这 5 个单位时间占用,总共 25 个单位,恰好等于 ISR 的工作总量——嵌套不产生额外的时间损失,只是把 ISR 的执行拆成了碎片。
嵌套的关键机制:被打断者的剩余工作必须"原地保存"(例中 A 剩 7、B 剩 2),靠栈实现。这就是"中断隐指令保存断点"之外,服务程序还要保存现场的原因——多层嵌套时靠栈的后进先出自动实现"后打断的先返回"。
例 3:中断方式的开销与它的"体力上限"
某机主频 1 GHz。中断响应需 5 个时钟周期,保存现场 20 个周期,恢复现场 20 个周期,中断服务程序 100 条指令、CPI = 4。某 I/O 设备传输率 4 MB/s。分别求"每传 1 KB 中断一次"与"每传 1 个字(4 B)中断一次"时,中断机制占用的 CPU 时间比例。
完整计算过程:
第一步,单次中断的 CPU 开销(单位:时钟周期):
第二步,换算成时间(1 GHz → 1 周期 = 1 ns):
第三步,情形(1):每传 1 KB(1024 B)中断一次
每秒中断次数:
每秒中断开销时间:
CPU 时间占比:
第四步,情形(2):每传 1 个字(4 B)中断一次
对比:
| 中断粒度 | 每秒中断次数 | CPU 开销时间 | CPU 占比 |
|---|---|---|---|
| 每 1 KB 一次 | 3906 | 1.738 ms | 0.174% |
| 每 1 个字(4 B)一次 | 1 000 000 | 0.445 s | 44.5% |
结论:中断的频率决定了它的可行性。每 1 KB 一次时中断开销低到可以忽略(0.174%);但一旦细到每字一次,近半个 CPU 都用来做"进入/退出中断"这件事了——而设备传输率还是 4 MB/s,一点没变快。
这就是 DMA 存在的理由(见第 41 篇):中断方式的开销与"中断次数"成正比,而中断次数与"数据块大小"成反比。把块开大能降开销(每 1 KB 一次只有 0.174%),但块越大、响应延迟越长。DMA 把"逐字搬"这件事从 CPU 手里整个拿走,只留"块结束时一次中断"——既不用切细粒度,也不用 CPU 体力。
例 4:中断向量与向量地址
某机中断向量表放在内存 0 号地址开始的连续区域,每个表项 4 字节,中断类型号 0~255。求类型号 32、33 的表项地址;并求表的总长度。
完整计算过程:
| 中断类型号 | 十进制计算 | 向量地址 | 十六进制 |
|---|---|---|---|
| 0 | 0 | 0x000 | |
32(0x20) | 128 | 0x080 | |
33(0x21) | 132 | 0x084 | |
255(0xFF) | 1020 | 0x3FC |
表的总长度:
结论:类型号 32 的入口地址存在 0x080 起的 4 个字节里;类型号 33 存在 0x084 起。这两个地址相邻但不重叠(0x080 占 0x080~0x083,0x084 占 0x084~0x087)。
⚠️ 常见错误:
- 把"每个表项 4 字节"漏掉,直接答向量地址 = 32。类型号是"第几个",向量地址才是"在哪里",中间差一个"每项字节数"的乘法。
- 把 4 字节记成 2 字节:那样表长只有 512 B,
0x20的向量地址会算成 64——先确认题目给的表项长度。 - 把向量地址算成
类型号 × 表项字节数 × 4:那个 只出现在"表项 4 字节"的情形里,别重复乘。
例 5:从 C 侧看一次"除零异常"的完整链路
硬件异常是怎么一路走到"用户能看到"的?看这段 C 代码。它演示了 CPU 除零异常 → 内核捕获 → 转交用户态处理程序 的全过程。
#include <stdio.h>
#include <signal.h>
#include <stdlib.h>
/* 内核把"除零"翻译成信号后,交由这个用户态函数接管 */
static void on_sigfpe(int sig) {
/* SIGFPE = 8(Linux / x86-64 与 MSVC CRT 一致) */
printf("捕获到信号 sig=%d, 它对应 CPU 的除零异常\n", sig);
exit(1); /* 处理完就退出, 绝不返回后重执行那条除法 */
}
int main(void) {
signal(SIGFPE, on_sigfpe); /* 用户态登记处理程序(不是 CPU 的中断向量表) */
volatile int a = 1, b = 0; /* volatile: 逼编译器真做除法,不许常量折叠 */
printf("开始执行 a / b ...\n");
int c = a / b; /* ★ 这一句在 CPU 里触发除零异常 */
printf("这行永远不会执行: c = %d\n", c);
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
链路对照(代码 → 硬件 → 内核 → 代码):
| 层 | 发生了什么 | 对应本篇哪个概念 |
|---|---|---|
| C 源码 | a / b,除数恒为 0 | 触发源 |
| CPU | 除法器检测到除数为 0 → 产生内中断(故障 fault) | 故障 fault |
| CPU | 硬件做中断隐指令:关中断、保存断点、转处理程序 | 关、存、引 |
| 内核 | 把该异常翻译成信号 SIGFPE,记到进程的待处理信号里 | 中断服务程序 |
| 内核 | 返回用户态前检查到待处理信号 → 调用已登记的 on_sigfpe | 恢复现场后不返回原处 |
预期输出(Linux/x86-64 或 MSVC CRT 上编译运行):
开始执行 a / b ...
捕获到信号 sig=8, 它对应 CPU 的除零异常三个要点:
volatile是必需的。若写成const int b = 0;,编译器会在编译期就把它折叠掉(甚至直接报错),运行时根本不产生异常——"硬件异常"必须真的执行到那条指令才会发生。- 不返回原处。除零属于"无法修复"的故障,处理程序只能退出——它演示了"故障 fault 的默认结局:返回后重执行,但重执行还是错,所以干脆终止进程"。相比之下缺页是可修复的故障(把页调进来再重执行就成功了)。
signal()登记的处理程序在用户态,与 CPU 的中断向量表(内存固定低地址、由硬件直接跳转)是两回事:CPU 眼里只有一条"除零异常入口",具体怎么处理由内核决定。
例 6:Python——屏蔽字次序、嵌套时标与中断开销验算
# ===== 例 1: 由屏蔽字推处理次序 =====
srcs = ['A', 'B', 'C', 'D']
masks = {'A': '1000', 'B': '1100', 'C': '1110', 'D': '1111'}
print('=== 例 1: 屏蔽字 -> 处理次序 (位序 A B C D, 1 = 屏蔽) ===')
for s in srcs:
blocked = [srcs[i] for i, b in enumerate(masks[s]) if b == '1']
allow = [srcs[i] for i, b in enumerate(masks[s]) if b == '0']
print(' %s 屏蔽字 %s -> 屏蔽 %s ; 允许打断 %s'
% (s, masks[s], ','.join(blocked), ','.join(allow) if allow else '无'))
order = sorted(srcs, key=lambda s: masks[s].count('1'), reverse=True)
print(' 1 的个数 %s' % {s: masks[s].count('1') for s in srcs})
print(' 处理次序(高->低) = %s' % ' > '.join(order))
print(' 自检: 每级都把自己那位写成 1 -> 防自我嵌套死循环')
# ===== 例 2: 多重中断嵌套时标 =====
print('\n=== 例 2: 嵌套时标 (A@5/10, B@8/6, C@12/4, D@30/5) ===')
events = {'A': (5, 10), 'B': (8, 6), 'C': (12, 4), 'D': (30, 5)}
rem, running, pending, log = {}, None, [], []
arrivals = sorted(events.items(), key=lambda kv: kv[1][0])
ai, step, t = 0, 0.5, 0.0
while t < 60:
while ai < len(arrivals) and abs(arrivals[ai][1][0] - t) < 1e-9:
s, (_, dur) = arrivals[ai]
rem[s] = float(dur)
if running is None:
running = s
log.append((t, '主程序被 %s 打断, %s 开始' % (s, s)))
elif masks[running][srcs.index(s)] == '0': # 允许打断
pending.append(running)
log.append((t, '%s 打断 %s (%s 挂起, 剩 %g)' % (s, running, running, rem[running])))
running = s
else:
pending.append(s)
log.append((t, '%s 被 %s 屏蔽, 挂起待处理' % (s, running)))
ai += 1
if running:
rem[running] -= step
if rem[running] <= 1e-9:
log.append((t + step, '%s 服务完毕' % running))
running = None
cand = [x for x in pending if rem.get(x, 0) > 1e-9]
if cand:
best = max(cand, key=lambda x: masks[x].count('1'))
pending.remove(best)
running = best
log.append((t + step, '回到 %s (剩 %g)' % (best, rem[best])))
else:
log.append((t + step, '主程序恢复运行'))
t += step
for tt, what in log:
print(' t=%5.1f %s' % (tt, what))
print(' ISR 总工作量 = %d, 主程序被占用 5->25 (20) + 30->35 (5) = 25 -> 两数相等'
% sum(d for _, d in events.values()))
# ===== 例 3: 中断开销 =====
print('\n=== 例 3: 中断开销 (1 GHz, 响应5 + 现场20+20 + ISR 100条 x CPI 4) ===')
f, resp, save, n_instr, cpi, restore = 1e9, 5, 20, 100, 4, 20
per_irq = resp + save + n_instr * cpi + restore
print(' 单次开销 = 5 + 20 + 100x4 + 20 = %d 周期 = %d ns = %.3f us'
% (per_irq, per_irq, per_irq / 1000))
for rate, chunk in [(4e6, 1024), (4e6, 4)]:
n = rate / chunk
ov = n * per_irq / f
print(' 设备 %.0f MB/s, 每 %d B 一次 -> %9.0f 次/s -> CPU 开销 %.6f s/s = %6.3f%%'
% (rate / 1e6, chunk, n, ov, ov * 100))
print(' 结论: 中断次数越多, CPU 越累 -> 块要大; 但块大又响应慢 -> DMA 出场')
# ===== 例 4: 中断向量 =====
print('\n=== 例 4: 中断向量表 (每项 4 B, 类型号 0~255) ===')
for typ in [0, 0x20, 0x21, 255]:
print(' 类型号 %3d (0x%02X) -> 向量地址 = %3d x 4 = %4d = 0x%03X'
% (typ, typ, typ, typ * 4, typ * 4))
print(' 表长 = 256 x 4 = %d B = %d KB' % (256 * 4, 256 * 4 // 1024))
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
输出对照:例 1 得 A/B/C/D 的屏蔽位数为 1/2/3/4,处理次序 D > C > B > A;例 2 的关键时刻为 t = 5 进 A、8 被 B 打断、12 被 C 打断、16 回 B、18 回 A、25 主程序恢复、30 被 D 打断、35 恢复,且"主程序被占用 25 个单位 = ISR 总工作量 25";例 3 得单次开销 445 周期 = 445 ns,每 1 KB 一次占 0.174%、每字一次占 44.5%;例 4 得 0x20 → 128 = 0x080、0x21 → 132 = 0x084、表长 1 KB。全部与手算一致。
考点
考点
1. 必背结论
- 中断隐指令 = 硬件自动完成的"关中断、保存断点、引出中断服务程序",它不是真指令(无指令码、程序员不可见)。
- 中断只在"一条指令执行结束"后响应(外中断),不会打断一条指令的执行。
- 响应可屏蔽中断需 IF = 1;NMI 与内部异常不受 IF 影响。
- 故障(fault)返回后重执行当前指令;自陷(trap)返回后执行下一条——这是最常考的返回地址差异。
- 中断向量 = 服务程序入口地址;向量地址 = 入口地址在表中的存放地址(口径按题目走)。
- 向量地址 = 中断类型号 × 每表项字节数;经典口径:256 项 × 4 B = 1 KB。
- 多重中断两条件:① 服务程序中提前开中断 ② 有优先级 / 屏蔽机制。
- 屏蔽字 1 = 屏蔽(禁止)、0 = 允许;1 的个数越多优先级越高;每级必然屏蔽自己那位。
- 处理次序 = 屏蔽字中 1 的个数从多到少。
- 中断隐指令 vs 服务程序的分工:断点 PC 由硬件存;通用寄存器/PSW 由软件存。
2. 高频陷阱
- 把屏蔽字的 1/0 含义记反(1 = 允许)。记反整题全错,这是第一大失分点。
- 忘了"每级要屏蔽自己那位"——漏了这一位,屏蔽字结构就错了。
- 把"响应次序"当"处理次序":响应看优先级,处理看屏蔽字;两者不一致时以屏蔽字为准。
- 故障与自陷的返回地址搞反:缺页回来要重执行
lw,syscall回来要执行下一条。搞反了具体程序会死循环或跳过指令。 - 以为"中断可以打断一条正在执行的指令":外中断只在指令边界响应;能"打断指令"的是内部异常(且它是指令执行的一部分)。
- 把"NMI 也能被 IF 屏蔽":NMI 与 IF 无关,任何时刻都响应。这也是它用来报"电源故障"的原因。
- 向量地址算漏"× 每项字节数":类型号 32 的向量地址是 128(每项 4 B),不是 32。
- 忘了"中断服务程序要保存通用寄存器":中断随机插入,不保存现场就回不到被打断的现场。只保存 PC 是不够的。
- 以为"多重中断要一直开着中断":正确做法是关中断 → 保存现场 → 开中断 → 服务——在服务之前就开中断,否则整个服务期间无法被嵌套。
- 中断与 DMA 的混淆:中断是每传完一小块就打断 CPU,DMA 是DMA 控制器自己搬完一整块再中断一次(详见第 41 篇)。
3. 解题模板("中断屏蔽字题")
① 读屏蔽字表, 确认位序 (通常与中断源列表顺序一致)
② 逐级解读: 该位 = 1 -> 屏蔽该源 ; = 0 -> 允许该源打断我
③ 数每级 1 的个数 -> 个数越多优先级越高
④ 处理次序 = 按 1 的个数从多到少排
⑤ 若问嵌套时标: 逐时刻判断 "新源能否打断当前者"
-> 新源在当前者屏蔽字中对应位 = 0 则打断
-> 被打断者记下剩余工作量, 压栈
-> 当前者服务完毕后, 从挂起队列里取优先级最高的继续
⑥ 若问中断开销: 单次开销 = 响应 + 现场保护 + ISR(指令数 x CPI) + 现场恢复
总占比 = 每秒中断次数 x 单次开销 / 每秒周期数4. 与相邻章节的接口
- 第 32 篇(流水线):中断会打断流水线里"正在飞"的多条指令。要让"断点"精确(精确异常),必须把流水线里的指令理清——冲掉比断点晚的、等前面的完成。这直接决定了中断响应的最小延迟。
- 第 31 篇(控制器):中断请求是控制器的输入之一;微程序控制器需要一条"中断响应微程序",入口地址由判别测试 + 中断源编码共同决定。
- 第 41 篇(I/O 系统):中断方式是三种 I/O 控制方式的中间一档(查询 → 中断 → DMA),本篇例 3 的 44.5% 就是它被 DMA 取代的直接原因。
- 第 40 篇(总线):中断响应也要走总线(中断请求线、中断应答线 INTA);链式查询识别中断源与总线链式仲裁是同一套电路思想。
- L4 操作系统(
os):中断是操作系统的"心跳"——时钟中断驱动调度、I/O 中断唤醒进程、syscall用自陷进入内核。"用户态 → 内核态"的入口就是本篇的自陷机制,详见下一门课的os/02-syscall.md。
小结
- 内中断(异常)来自 CPU 内部:故障 fault(返回后重执行)、自陷 trap(返回后执行下一条)、终止 abort(不返回);外中断来自外部:可屏蔽 INTR(需 IF=1)与不可屏蔽 NMI。
- 中断隐指令 = 关中断 + 保存断点 + 引出服务程序,由硬件完成;现场(通用寄存器/PSW)由软件保存。
- 响应三条件:指令执行结束、有请求、可屏蔽中断需 IF = 1。
- 屏蔽字 1 = 屏蔽、0 = 允许;1 的个数越多优先级越高;每级必然屏蔽自己。例 1 中次序为 D > C > B > A。
- 多重中断两条件:服务程序中提前开中断 + 优先级/屏蔽机制。例 2 中主程序被占用 25 个单位,恰等于 4 个 ISR 的工作总量。
- 向量地址 = 类型号 × 每项字节数;表长 256 × 4 = 1 KB(类型号 32 →
0x080)。 - 中断的性价比由"频率"决定:例 3 中每 1 KB 中断一次只占 0.174%,每字一次就吃掉 44.5%——这个数字就是 DMA 存在的理由。
下一篇:总线:仲裁、定时、常见总线标准
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。