Revision | 计算机组成与体系结构(下)
流水线处理器
将单周期处理器流水线化是我们在 ICS 中已经学过的技术。做法是在 CPU 中插入流水线寄存器,这样串联的线路上可以提升指令的吞吐量。
历史上,Intel 80486 是第一台流水线 x86 CPU,采用五级流水线。Pentinium 是第一台超标量流水线,它有 U、V 两条流水线,每个时钟周期可以发送两条指令。流水线深度的纪录保持者是 2004 年的 Pentinium 4(Prescott)处理器,有 31 级流水线。现在的 Intel Core i7 是 4 发射,16 级流水线。
MIPS 的 CPU 可以被划分成以下 5 个阶段,因此可以被划分成五级流水线:
- 取指. 从存储器取指令,更新 PC;
- 译码. 指令译码,从寄存器读出寄存器的值;
- 执行. 进行算术逻辑运算或者计算存储器地址;
- 访存. 从存储器读入或向其中写入数据;
- 写回. 将数据写入寄存器堆。
现在,假设第 $i$ 个阶段线路的时延是 $D_i$,流水线寄存器本身有时延 $D_p$,则我们可以将时钟周期置为 $T = \max D_i + D_p$,一条指令的完整执行时间变为 $ST$,其中 $S$ 为流水线级数。显然 $ST$ 大于原来线路的延迟,但是现在指令的吞吐量将变为 $1 / T$,较原来实际上缩小了。
现代 CPU 通常采取的结构是超标量结构,即一个 CPU 里有几条流水线。这和流水线属于两个独立的结构。
容易发现,流水线的一个问题就是某一条指令没有执行完可能会导致后面的指令无法执行。比如前一条指令 $A$ 将运算结果写入 $1 寄存器,后一条指令 $B$ 需要 $1 作为运算数。此时在 $A$ 写回之前,$B$ 不应当运行。此类组织下一条指令在下一个时钟周期开始执行的情况,称为冒险。具体可以分为三类:
- 结构冒险. 所需的硬件不见正在为之前的指令工作;
- 假设指令和数据放在同一个存储器中。设当前指令是一个
lw,则三个周期后该指令运行到访存阶段,新的一条指令的读取无法执行。(解决方法是流水线停顿,插入 bubble,或者使用哈佛结构); - 还可能同时读寄存器和写寄存器。方法是前半个周期用来读,后半个周期用来写,寄存器堆中设置独立的读写口。
- 假设指令和数据放在同一个存储器中。设当前指令是一个
- 数据冒险. 需要等待之前的指令完成数据的读写;
- 有些指令要写回的数在执行阶段就算好了。此时可以进行数据前递;
- 如果一条指令需要之前的访存结果(Load-Use Harzard),则数据前递也不能解决。只能插入一些 bubble,尽量前递(让下一条指令的执行阶段对齐本条指令的写回阶段,可以节省一个 bubble)。
- 控制冒险. 需要根据之前指令的结果决定下一步的行为。
- 最 trivial 的方法就是直接 bubble。
- 高级的做法是进行分支预测。
接下来着重讨论分支预测。引发控制冒险的转移指令在程序中占比巨大。且流水线越深、超标量数越多,转移指令影响越大,因此转移预测是必要的。
当转移指令被执行,且确实发生转移时,将会产生开销。因为此时流水线需要将按顺序预取的指令全部废除(排空流水线)、从转移目标地址重新取指令。转移开销主要有两个来源:
- 转移条件判定引起的开销;
- 生成目标地址引起的开销。
降低转移开销的技术主要有两类。第一类称为延迟转移。其做法是,在编译过程中,通过编译器调度,在转移指令之后插入一条或者几条适当的指令。比如一段代码本来形如
1 | INC AX |
如果正常执行,会在 JNZ 后面插入两个 bubble。但是 AX 和 BX 和转移没有什么关系,因此编译器通过将 AX 和 BX 的修改调度到 JNZ 后面,便可以节省这两个 bubble。
另一类技术称为转移预测。须对转移条件判定和目标地址做出预测。
- 转移条件判定的预测. 分为静态的、动态的,硬件的、软件的。
- 硬件固定预测不转移 实现简单,但是效果不佳。
- 编译制导的预测. 转移指令增加一位,由编译器设置,通知硬件预测跳转还是不跳转。其优点是软件可以根据指令类型和历史信息对不同指令进行不同预测;但缺点是需要软件支持,需要修改 ISA,且可能不能适应多变的执行环境。
- 基于偏移的预测. 如果转移指令地址和转移目标地址的相对偏移为负,则转移(很可能是循环),否则不转移。
- 基于历史信息的预测(现在主流的技术). 一般的做法是搓一个 DFA 来判断要不要转移。Pentium 中,考察前两次转移是否发生,分为四个状态:00(强不发生转移),01(弱发生转移),10(发生转移),11(强发生转移),在强发生转移和弱发生转移是都预测转移,其他预测不发送转移。当然,你可以取更多历史位数。实践证明位数 $\geq 4$ 后也能继续提升,但是很微弱。
- 转移目标地址的预测.
- 转移目标缓冲器(BTB). 一张表,表项为 $(BTA_i, T/NT, BIA_1)$,其中 $BTA$ 是转移目标地址,$T / NT$ 是是否发生,$BIA$ 是转移指令地址。其工作方式为(查表得时机可以在取指时、译码完成后,或将其权衡地,预译码完成后,取值的同时。):
- 在表中查当前指令地址,如果没查到,新建一项;
- 若有一项匹配,称为 BTB 命中。若该指令转移条件的预测结果为“发生转移”,则将 $BTA$ 作为下一条指令地址。
- 若最终结果和 BTB 不匹配,更新 BTB。
- 转移目标缓冲器(BTB). 一张表,表项为 $(BTA_i, T/NT, BIA_1)$,其中 $BTA$ 是转移目标地址,$T / NT$ 是是否发生,$BIA$ 是转移指令地址。其工作方式为(查表得时机可以在取指时、译码完成后,或将其权衡地,预译码完成后,取值的同时。):
注意 RET 指令的返回地址都是从栈里拉的,因此有一个专门的预测部件返回地址栈 RAS用于 RET 的跳转地址预测。
存储器技术【FIXME】
中断和异常
异常处理起源于 UNIVAC。当发生算术运算溢出时,转向地址 $0$ 执行两条修复功能,或者停机。后来大家发现这个机制可以用来执行如下一般的机制:
- 程序运行时,系统外部、内部或者现行程序本身出现需要特殊处理的“事件”;
- CPU 立即强行终止现行程序的运行,改变机器的工作状态并启动相应的程序来处理这些“事件”。
- 处理完成后,CPU 恢复原来的程序运行。
这些事件就是所谓的“中断”或者“异常”。当然还有一些别的叫法,比如“外部中断”、“内部终端”,或者“硬件终端”、“软件终端”。此后我们会混用这些名词。
软件可以使用 INT n 指令调用中断服务程序。其应用就是
- BIOS 功能都由中断程序实现。
- 可以通过 DOS 中断调用诸如文件管理、存储管理、作业管理和设备管理等功能。
为了处理异常,需要向流水线中增加如下要素:
- 寄存器 EPC 和 Cause,存放出现异常的指令的地址,和产生异常的原因。
- PC update 中接入一根线,表示处理异常的程序入口地址。
在 MIPS CPU 中,如果发生异常,执行如下操作:
- 在 EPC 中保存出现异常的指令的地址;
- 清空流水线中之后的指令;
- 在 Cause 中记录产生异常的原因;
- 将控制权转交给操作系统的特定地址。
检测终端的办法是使用 CPU 内部的终端逻辑。这个模块连接了状态码(TF、OF 等)、译码电路(INT 3 指令等)、一条叫做 NMI 的线(传递外设的非屏蔽终端请求)和一条叫做 INTR 的线(传递外设的可屏蔽中断请求)。注意外设可能同时发出多个中断请求(比如网卡、显卡和键盘同时请求中断),此时为了决定这些中断的优先级,有以下几种办法:
软件查询确定中断优先级. 将所有中断信号 OR 起来通过 INTR 发送给 CPU。在这种模式下,中断服务程序的开始部分需要安排一段查询程序。一般来说,优先查询速度较快或者实时性较高的设备。
硬件中断优先级编码电路. 所谓的菊花链优先级排队电路。在每个设备接口设置一个简单的逻辑电路,根据优先级顺序来传递传递或者截留 CPU 发出的中断响应信号,如图所示:

其中菊花链逻辑需要实现:若 INTA 是 $1$,向后传递 $1$,并且将中断响应(低电平为有效)置为 $1$。否则,若中断请求是 $1$,向后传递 $1$,并将中断响应信号置为低电平。这容易通过几个 or 门实现。
可编程中断控制器(现代 PC 机常用,如 8295A). 这个控制器连接了诸外设的请求信号,并且可以通过系统总线和 CPU 与设备交互。现在 PC 机的中断控制器称为高级中断控制器(APIC),分布于南桥(IO APIC)和 CPU 中(Local APIC),APIC 之间互相连接。
至此我们已经接通了中断产生这一侧。当 CPU 侦测到中断,其将按顺序进行以下操作:
- 关中断. CPU 关闭中断响应,不再接受其他外部中断请求。
- 保存断点. 将发生中断处的指令地址压栈。
- 识别中断源. CPU 识别中断的来源,确定中断类型号,从而找到响应的中断服务程序的入口地址。
- 保护现场. 将有关寄存器和标志寄存器压栈。比如 CS/IP,还有中断服务程序可能用到的通用寄存器。
- 执行中断服务程序. 转到中断服务程序的入口开始执行。可以适时重新开放中断,以便允许响应较高优先级的外部中断。开放和关闭中断响应的方法是设置 IF 寄存器。(IF = 1 则允许 CPU 响应可屏蔽中断请求,否则不允许 CPU 响应可屏蔽中断请求,用 STI 和 CLI 指令设置该寄存器,注意 IF 对非屏蔽中断和内部终端不起作用)。
- 恢复现场并返回. 字面意思,归纳为 IRET 指令。
前三项一般是硬件电路,后三项都由软件完成。
其中,确定终端服务程序的地址使用所谓的中断向量表。以 Intel 8086 为例,存储器中保留了两个专用区域。地址最低的 1KB 用于存放中断向量表,地址最高的 16B 存放了一个初始化程序(CPU 复位后从这里取第一条指令)。
中断向量表由中断向量构成。一个中断向量指示中断服务程序的逻辑地址,可想而知一个中断向量有 4 字节(CS:IP),具体地从低到高依次是 IP 和 CS,低字节在前,高字节在后。
例子. 现在一个中断类型码是 20H。则中断向量存放在 0000:0080H 开始的 4 字节(注意一个中断向量 4 字节,因此要乘 4)。若上述四个字节内容为 10H,20H,30H,40H,则中断服务程序的入口地址为 4030:2010H。
在保护模式下,中断向量表的形式和全局描述符表类似,称为中断描述符表,其基地址存在 IDTR 寄存器中。
最后着重阐述一下几个内部中断。
- 类型 0:除法搓中断. 执行除法指令后,所得商超过了目标寄存器所能表示的范围,如除以 $0$,立刻产生一个类型 $0$ 中断。
- 类型 1:单步中断. 若标志寄存器中的跟踪标志 TF 设置为 1,则 CPU 处于单步工作模式。此时 CPU 没执行完一条指令,便自动产生一个类型 1 中断,进入中断服务程序。常用于调试。
- 类型 3:断电中断. 调试手段。
- 类型 4:溢出中断. 执行 INTO 指令时,若 OF 为 $1$,触发类型为 $4$ 的内部中断,否则空转。这在 sanitizer 里面很常用。
内部中断较外部中断的区别主要体现在:
- 中断类型号直接由 CPU 产生,而不是需要从外设中读取。
- 除了单步中断外,所有内部中断都不可以用软件方法禁止。(单步中断可以将 TF 清空嘞禁止)。
- 除单步中断外,所有内部中断优先级高于外部中断。
回忆有一个重要的信号是 SIG ALRM。这个信号的实现方法是有一个定时器连接到 PIC 上面。这一小节我们着重考察计数器和定时器的概念和实现。
现在的可编程定时器的内部主要包含数据总线缓冲器、读/写逻辑、控制字寄存器、多个计数器、内部总线这几个结构。首先着重讲计数器的外部接线和响应功能,我们将略过内部逻辑。一个计数器连接以下几个线:
- 时钟输入信号 CLK$_i$. 作为技术脉冲,可以是非周期性脉冲和频率精确的周期性脉冲;
- 门控输入信号 GATE$_i$. 对计数过程进行控制,具体作用视工作方式而定。
- 输出信号 OUT$_i$. 计数到 $0$ 或者定时时间到时输出。具体形式视工作方式而定。
回到完整的定时器其基本操作就是:
- WR 中写入控制字 CW 后,OUT 端进入已知初始状态;
- 写入计数初值 CR 后,经过一个 CLK 下跳沿,CR 装入 CE;
- 每输入一个 CLK 脉冲,CE 减 1;
- 中途用 GATE 信号进行控制。
以 8253 芯片为例,你可以通过控制字选择 3 个计数通道和 6 种工作方式,指定计数初值和装入顺序甚至计数值是二进制还是 BCD。某些具体的工作方式如下:
- 方式 0:计数到 $0$ 产生中断请求. 在这种工作方式下,OUT 初值为 $0$。计数结束后 OUT 设置为 $1$。可以用 GATE 控制计数过程:当 GATE 变低,暂停计数。当 GATE 变高,继续计数。
- 方式 2:分频器. 每输入 $N$ 个 CLK 脉冲,输出 $1$ 个 CLK 周期的负脉冲。主要用于 DRAM 的定时刷新等。
- 方式 3:方波发生器. 输出周期为 $N$ 的对称方波或者基本对称的矩形波($N$ 为奇数做不到完全对称)。主要用于系统时钟和扬声器发声控制。
输入输出设备
本节讨论计算机中输入输出设备的实现。理想中的输入输出接口应当拥有以下基本功能:
- 数据缓冲. 解决 CPU 和外设之间的速度差距;
- 提供联络信息. 协调与同步数据交换过程;
- 信号与信息格式的转换. 数字信号和模拟信号的转换、串行和并行转换、电平转换;
- 设备选择、中断管理、可编程功能. 一些附加要求。
这决定了其基本结构大致是:连接地址总线、数据总线和控制总线,内部有数据输入寄存器、数据输出寄存器、状态寄存器、控制寄存器、中断控制逻辑,并与外设相连。计算机就是通过从这个数据输入寄存器读东西、向数据输出寄存器写东西来实现读写。具体地,一个 I/O 接口内部包含一组称为 I/O 端口的寄存器。这两个寄存器不在 CPU 内部,所以 CPU 通过访问 I/O 端口的端口地址来访问 I/O 端口。这里就涉及到第一个问题:如何编排 I/O 接口的端口地址?
- 和存储器分开编址. x86 使用该模式;
- 优点. I/O 端口不占用存储器地址;I/O 指令编码短、执行速度快;I/O 指令的地址码较短,地址译码方便;I/O 指令独立,使程序中 I/O 操作和其他操作层次清晰,便于理解;
- 缺点. 课件没写,我觉得是有悖于 RISC 精神。
- 和存储器统一编址. ARM,MIPS 等使用该方式。
- 优点. 直接使用访存指令访问 I/O 端口;可以将 CPU 中的 I/O 操作与访存操作统一设计为一套逻辑,简化内部结构和 CPU 引脚数目。
- 缺点. 占用存储器地址空间;指令更长,执行时间更长。
分开编址模式下,程序通过如下两个指令来进行 I/O:
- IN AC, PORT 其中 AC 可以是 AL 或者 AX,PORT 可以是立即数或者 DX。如果是 DX 表示用 DX 的内容指定端口地址。将端口 PORT 的内容输入到 AC。
- OUT PORT, AC 其中 AC 可以是 AL 或者 AX,PORT 可以是立即数或者 D,表示将 AC 的内容输出到 PORT。
现在要将一列数据送到 I/O 设备实现输入输出。这里,输入输出的控制方式有以下三种:
程序控制方式. 进一步细分为无条件传送和程序查询传送。
无条件传送 假定外设已经准备好,直接使用指令与外设传送数据,不假设外设的工作状态。
程序查询传送方式. CPU 通过执行一段程序,不断查询外设的工作状态,在确定外设已经准备就绪时,才进行数据传送。以输出数据为例,程序查询方式的工作流程如下:
- CPU 将控制字写入接口的控制寄存器,设置接口的工作模式;
- CPU 执行指令,将数据写到接口的输出缓冲寄存器;
- 接口将数据发送到“并行数据输出”信号线上,并将“输出准备好”信号设置为有效;
- 外设发现“输出准备好”信号有效之后,从并行数据输出信号线上接受数据,并将“输出回答”信号置为有效。
- 接口发现“输出回答”有效后,将状态寄存器中的状态位“输出缓冲空”置为有效。
- 此过程中,CPU 反复查询“输出缓冲空”,该信号有效之后才继续输出新数据。
输入的过程是对称的。
- 系统初始化时,CPU 执行指令,将控制字写入接口的控制寄存器,设置接口的工作模式。
- 外设将数据发送到“并行数据输入”信号线上,并将“输入准备好”信号置为有效;
- 接口发现“输入准备好”信号有效之后,从“并行数据输入”接受数据,放入“输入缓冲寄存器”,并将“输出回答”信号置为有效,阻止外设输入新数据。
- 接口将“状态寄存器”中的状态位“输入缓冲满”置为有效;
- CPU 反复执行指令直到发现“输入缓冲满”,从“输入缓冲寄存器”中读出数据。
- 接口将“输入回答”信号置为无效,等待外设输入新设备。
这东西的优点是对外设的要求低,操作流程清晰;缺点是由 CPU 进行数据传送,占用了宝贵的运算资源。
中断控制方式. 直接把程序查询中的 CPU 反复查询改成外设通过“中断控制逻辑”,用中断提醒 CPU。
这种方式使得 CPU 和外设可以并行工作,外设可以主动申请服务,一定程度上满足了 I/O 处理的实时性要求;但是外设和存储器之间的数据交换仍然由 CPU 承担,进入和退出中断服务程序还需要额外的指令。
直接存储器访问方式(DMA). 一种数据传送不需要 CPU 干预的方式。专门控制这件事情的一个控制器叫做 DMAC。 以使用 DMAC 进行外设到内存的传送为例,其工作步骤为:
- CPU 设置 DMAC 内部配置寄存器;
- DMAC 处于空闲等待状态;
- I/O 接口向 DMAC 发出 DMA 传送申请;
- DMAC 响应 I/O 接口的申请;
- DMAC 向 I/O 接口发起总线读传输;
- DMAC 向存储器发起总线写传输;
- 重复 5~6 直至本次 DMA 传送完成;
- 返回 2 等待下一次 DMA 传送申请。
在第一步,CPU 设置的参数包括:
- 源地址(I/O 端口),以及传送时的地址增减方式;
- 目的地址(存储器地址),以及地址增减方式;
- 待传送数据的长度。
有的 I/O 设备也可以自带 DMAC。
最后在硬件层面上,值得说明一下并行通信和串行通信的区别。在古老计算机上,提供诸如 9 针 RS-232、25 针 RS-232 之类的串口和 25 针 IEEE-1284 之类的并口。串行通信系指数据在单条一位宽的传输线上按时间先后一位一位地进行传送;并行通信系指数据在多位宽地传输线上各位同时进行传送。其权衡是显而易见的:
| 串行通信 | 并行通信 |
|---|---|
| 传输线数量少 | 传输线数量多 |
| 同频率下,数据传输率较低 | 同频率下,数据传输率较高 |
| 需要进行复杂的串 / 并转换(用移位寄存器实现) | 无需串 / 并转换 |
| 避免信号线之间的串扰 | 存在信号线之间的串扰 |
串行通信抗干扰的一个操作是差分信号传输技术。这里,发送端在两根线上发送同幅反相的两个信号。接收端通过比较两个电压的差值,判断发送端发送的是 0 还是 1。这时候,串扰会发生抵消,从而实现抗干扰。
【FIXME:8255A】
总线
总线是多于两个模块之间传送信息的公共通路。要设计一条总线,需要设计传输信息的电路和管理信息的协议两个成分。在现代微信计算机上,有几种不同层次的总线:
- 片上总线. 如 CPU 内部的总线。
- 系统总线. 计算机系统个插件板之间的通路;
- 通信总线. 计算机系统之间或计算机系统与其他系统(仪器、仪表、控制装置)之间信息传输的通路。
拥有总线控制能力、在获得总线控制权之后能启动总线传输的模块称为总线主模块(CPU、DMAC);能够对总线传输做出响应,包括接受写数据、返回读数据、返回错误等,但是不具备总线控制能力的称为总线从模块(存储器)。以下是总线的几个关键部件:
- 总线译码器. 根据当前控制总线主模块提供的地址,选择作为本次总线传输目标的从模块;
- 总线仲裁器. 当总线上有多个主模块同时请求使用总线时,决定由哪个主模块获得总线控制权。
容易想象总线本身就是一堆 multiplexer(以译码器和仲裁器作为选择信号)。
最早的 80386DX 芯片上,仲裁器被收纳在 CPU 上。现在的计算机系统有多条总线,总线之间可以通过总线桥接器相连接。
总线标准(协议?)需要规定以下内容:
- 机械特性. 模块插件的机械尺寸、总线插头、插座的规格及位置;
- 电气特性. 规定总线信号的逻辑电平以及负载能力;
- 功能特性. 给出各总线信号的名称及功能定义;
- 规程特性. 对各总线信号的总动作过程及时序关系进行说明。
历史上的总线标准有:
- ISA 总线. 由 IBM PC/XT、PC/AT 计算机的总线规范标准化而来,16 位。
- EISA 总线. ISA 拓展到 32 位得到,用于 80386 CPU。2000 年前后退出市场。
- PCI 总线. 用于 80486 CPU。
- AGP 总线. Accelerated Graphics Port,专用于显卡的告诉点对点通道。基于 PCI 研发。
- PCI-E. 2002 年由 PCI-SIG 组织正是推出,串行、全双工、点对点连接。采用差分信号传输。
我们这里讲一个具体简单的协议,AHB 总线的内容。AHB 总线的核心结构就是三个 multiplexer,分别称为 address and contoll mux(由仲裁器控制)、write data mux(由仲裁器控制)、read data mux(由译码器控制)。