异常和中断
异常和中断是处理器用来处理非预期事件或来自外部设备请求的重要机制。这些机制对于实现稳定的系统行为和高效的I/O通信至关重要。
异常(Exception)
-
定义: 异常是由处理器内部检测到的错误或特定事件触发的, 例如算术溢出、无效的内存访问、除以零等。
-
处理: 当异常发生时, 处理器会暂停当前指令的执行, 保存当前的状态(如寄存器、程序计数器PC等), 然后跳转到特定的异常处理代码(异常处理程序)去处理这个异常。
-
重要性: 异常处理机制是处理器稳定性的关键, 因为它允许处理器在出现错误时不会崩溃, 而是能够以一种可控的方式响应并尝试恢复或通知用户。
中断(Interrupt)
-
定义: 中断是由外部设备(如I/O设备)或处理器的内部事件触发的, 用于通知处理器有某个特定的事件需要处理。
-
类型: 中断可以是可屏蔽的(可以被其他中断或处理器内的逻辑禁用)或不可屏蔽的 (必须立即处理)。
-
处理: 当中断发生时, 处理器会保存当前的状态(如寄存器、程序计数器PC等), 然后跳转到特定的中断服务程序(ISR)去处理这个中断。处理完中断后, 处理器会恢复之前的状态并继续执行。
-
重要性: 中断处理机制是处理器与外部设备通信的关键, 它允许处理器在需要时暂停当 前的任务去处理外部事件, 从而提高了系统的响应性和效率。
异常和中断作为控制流中的非顺序转移事件, 对处理器的设计提出了特别的挑战, 主要体现在以下几个方面:
-
精确性: 处理器需要能够精确地识别何时发生异常或中断, 并准确记录当前程序状态(如程序计数器PC值、寄存器内容等), 以便在处理完异常或中断后能恢复到正确的执行点继续执行。这要求有高效的上下文保存和恢复机制。
-
及时响应: 中断尤其是外部中断(如I/O完成、硬件故障等)需要处理器能够迅速响应, 以确保系统的实时性和交互性。这要求处理器设计中有快速的中断处理路径和低延迟的中断服务例程(ISR)执行机制
-
优先级处理: 在多中断环境下, 处理器需要有能力根据中断的紧急程度或优先级来决定处理顺序, 这就涉及到中断嵌套和优先级仲裁机制的设计。
-
同步与异步事件的统一处理: 异常通常是程序执行过程中的同步事件(如除零错误), 而中断往往是异步的(如外部输入)。处理器必须设计有机制能够统一高效地处理这两种不同性质的事件。
-
安全性与一致性: 异常和中断处理过程中要确保内存访问的一致性, 避免数据竞争和一致性问题, 尤其是在多核处理器环境中, 这要求有严格的内存管理策略和一致性协议。
-
兼容性与标准化: 为了支持操作系统和软件的跨平台运行, 处理器的异常和中断机制需要遵循一定的标准或规范(如x86架构中的中断向量表、ARM架构的异常模型等), 这在设计时也需要考虑进去。
在处理器设计中, 异常处理通常是一个复杂且关键的部分, 因为它需要处理器能够识别、响应并可能地恢复从各种异常情况(如算术溢出、非法指令、访问违规等)中。
关键路径(Critical Path) 是处理器中决定时钟周期长度的路径。这个路径上的操作必须在一个时钟周期内完成, 否则处理器就需要更长的时钟周期来执行指令, 从而降低性能。检测异常情况并采取适当举措, 通常处于处理器的关键路径上。
异常处理对关键路径的影响主要体现在以下几个方面:
- 性能开销 2. 设计复杂性 3. 实现复杂性
异常处理过程: 算术溢出为例
我们使用add $1,$2,$1
指令作为例子来探讨算术溢出类型的异常处理时, 我们可以详细解析处理器需要执行的基本步骤。
算术溢出通常发生在加法、减法、乘法等运算中, 结果超出了目标寄存器能表示的范围。
在大多数体系结构中, 比如MIPS或类RISC架构, 处理这种异常的一般流程如下:
异常处理过程
-
检测异常: 在执行
add$1, $2, $1
指令前或执行过程中, 处理器的算术逻辑单元(ARLU)会检查操作结果是否会导致溢出。如果发现有溢出条件, 硬件会生成一个异常信号。 -
保存现场: 一旦检测到异常, 处理器首先需要保存当前执行状态, 以便异常处理完毕后能恢复到出错前的状态。这通常包括:
- 保存通用寄存器的内容, 特别是那些包含重要数据或指针的寄存器。
- 保存程序计数器(PC)的值, 即下一条指令的地址。但在算术溢出这类异常中, 更关键的是保存导致异常的指令地址, 因此:
-
设置EPC: 将导致异常的指令地址(即
add$1, $2, $1
的地址)保存到异常程序计数器(EPC)中。这样, 操作系统在处理完异常后, 可以知道应该从哪里重新开始执行。(后面我们会讲到这个) -
切换到内核模式: 为了安全地处理异常, 处理器需要从用户模式切换到内核模式(或称为特权模式、超级用户模式), 这样操作系统可以访问所有资源来妥善处理异常。(到时候操作系统会详细讲这两个状态)
-
跳转到异常处理程序: 根据异常类型(在这个例子中是算术溢出), 处理器将控制权转移给操作系统预先定义好的异常处理入口点。这个入口地址通常是固定的, 操作系统在此处放置了异常处理代码。
-
异常处理: 在操作系统层面, 异常处理程序会根据EPC中的地址分析异常原因, 可能执行的操作包括: 记录日志、执行特定的错误恢复操作、或者干脆终止引起异常的任务。7.恢复现场与返回: 异常处理完成后, 操作系统需要恢复之前保存的处理器状态, 包括从EPC中恢复PC值, 从而回到出错前的指令位置或跳转到一个安全的恢复点继续执行。如果异常不能被完全恢复, 则可能会执行进程终止或系统重启等操作。
操作系统的响应方式
从以上步骤可以看出, 操作系统在面对算术溢出这类异常时, 需要多种策略来应对, 每种策略的选择取决于异常的性质、程序的要求以及系统的安全策略。我们可以得出操作系统几种响应方式:
- 提供服务或修正操作
- 警告用户并请求输入
- 日志记录与监控
- 终止程序并报告错误
- 继续执行
异常原因记录
为了处理异常, 操作系统除了要知道是哪条指令引起异常之外, 还必须知道引起异常的原因。就好像报错bug一样, 要告诉你哪里有问题。主要有两种方法用于表示产生异常的原因:
-
MIPS架构的Cause寄存器方法在MIPS架构中, 处理异常的一个关键组件是Cause寄存器。当异常或中断发生时, 处理器会自动将异常原因编码到Cause寄存器中的特定字段。这个寄存器不仅记录了异常或中断发生的事实, 还精确指明了其原因, 如是否是因为算术错误、地址越界、非法指令等。操作系统随后可以通过读取Cause寄存器来确定具体异常类型, 并采取相应的处理措施。此外, MIPS架构还利用EPC(ExceptionProgram Counter)寄存器来保存异常发生时的下一条指令地址, 以便异常处理结束后恢复执行。
-
向量中断方法向量中断是一种经典的异常处理机制, 广泛应用于各种处理器架构中。在这种方法中, 每个异常或中断都有一个预定义的、固定的中断向量地址。当异常或中断发生时, 硬件会自动将程序计数器(PC)设置为对应异常或中断类型的向量地址, 从而立即将控制权转移至该地址处的中断服务例程(ISR)。这意味着异常处理的起始点直接由异常或中断的类型决定, 无需通过查询寄存器来确定异常原因, 从而加快了响应速度。
在流水线中实现的异常: 以算数异常为例
-
异常识别与响应: 当检测到算术溢出这类异常时, 首先需要中止当前的指令执行流程, 确保不会因异常而破坏预期的程序状态。
-
流水线清理:
-
ID级清理: 通过引入
ID.FIush
信号- 目的: 在指令译码(ID)阶段发现异常需要处理时, 防止该指令及其后续指令继续向下流动, 以免对系统状态造成不必要的影响或错误的改变。
- 机制: 通过一个控制信号
ID.Flush
, 这个信号连接到ID级的多路复用器。当ID.Flush
被激活时, 它会阻塞"ID级的输出, 意味着不向前级(如执行阶段)发送任何指令信息。这样, 后续的流水线阶段(如执行、内存访问、写回等)就不会接收到来自当前异常指令的数据或控制信号, 从而避免了潜在的错误传播。
-
EX级清理: 使用
EX.Flush
信号- 目的: 在执行(EX)阶段检测到异常时, 确保异常指令的执行结果不会被错误地写回到寄存器文件中, 保护了异常发生前的原始寄存器状态。
- 机制: 引入
EX.Flush
信号, 当它被激活时, 控制执行阶段到寄存器文件的写回路径被关闭。这意味着, 尽管执行阶段可能已经计算出了结果, 但这个结果不会被写回到寄存器中覆盖原有的数据, 从而保护了异常发生前寄存器的值, 这对后续的错误恢复和处理至关重要。
-
指令替换: 从异常处理地址开始执行
- 目的: 确保在异常处理完成后, 能够从异常处理程序的入口地址开始执行, 而不是继续执行原本的指令序列。
- 机制: 在程序计数器(PC)的多路复用器中添加一个额外的输入选项, 这个输入对应于异常处理程序的起始地址(例如MIPS中的
8000 0180g
)。当异常发生时, 通过控制信号选择这个特定的地址作为PC的新值, 从而使流水线从异常处理程序的第一条指令开始取指执行。这个操作有效地"重置"了流水线的取指点, 确保了异常处理流程的正确启动。
-
-
原始数据保护: 异常处理的一个关键考虑是保护异常发生时的原始数据。通过在EX级检测异常并阻止结果写回(通过
EX.Flush
), 可以确保目标寄存器中的原始值不受影响, 这对于后续的调试和正确处理异常至关重要。 -
异常处理后重执行: 许多情况下, 需要在处理完异常后重新执行引起异常的指令。这通常涉及清除该指令、完成异常处理流程, 然后基于EPC寄存器中的地址(已调整为异常前的指令地址)重新取指执行。
-
EPC更新: 异常处理机制还需负责将引起异常的指令地址(通常是该指令的下一条指令地址, 即PC+4)保存到EPC(Exception Program Counter)寄存器中。这样做是为了在异常处理完毕后能够恢复执行。注意, 由于保存的是"下一条指令"的地址, 异常处理代码在真正处理异常指令之前需要对EPC的值进行调整(减4), 以指向真正的异常源指令。
操作系统与硬件在处理异常时是协同工作的。旨在确保系统的健壮性和稳定性。
硬件的角色
-
暂停指令流: 一旦检测到异常或中断, 硬件会立刻停止当前指令的执行, 防止错误状态的进一步传播。
-
执行已完成指令: 在停止当前指令前, 硬件会确保所有已经进入流水线并完成执行的指令其效果得到固化, 这保证了系统状态的一致性。
-
清除后续指令: 尚未执行或正在执行中的后续指令需要被清除或取消, 确保它们不对系统状态产生影响。
-
设置异常寄存器: 硬件会设置特定的寄存器(如MIPS架构中的CauSe和EPC寄存器)记录异常原因和发生异常的指令地址, 为操作系统提供必要信息。
-
跳转执行: 硬件会将控制权转移给操作系统预定义的异常处理程序, 通常是一个特定的地址, 开始执行异常处理流程。
操作系统的作用
-
异常分析: 操作系统首先会检查异常寄存器, 确定异常类型, 比如是非法指令、算术溢出、I/0请求还是页面错误。
-
程序终止或恢复: 对于致命异常(如未定义指令、硬件错误、算术溢出), OS可能会终止引起异常的进程, 并记录错误信息。而对于I/O请求或系统调用, 操作系统会保存当前进程的状态(如寄存器内容、堆栈指针), 执行I/O操作或服务调用, 完成后恢复进程状态, 让其继续执行。
-
任务调度: 在处理I/O请求时, 操作系统可能利用这段时间调度其他就绪任务执行, 实现并发和资源的有效利用, 这也是现代操作系统多任务处理的关键特性。
-
页缺失处理: 特别地, 页缺失和TLB(Translation Lookaside Buffer)异常需要操作系统介入, 通过查找页表, 将所需页面从磁盘加载到内存, 更新页表和TLB, 然后重新执行导致异常的指令。
保存和恢复:
保存和恢复任务状态是异常处理和多任务操作系统的核心机制。它确保了任务可以在中断后从精确的断点恢复, 无论是由于I/O等待、页面错误处理还是响应其他中断。状态的正确保存和恢复是实现程序执行连贯性、系统可靠性和响应性的基础, 同时也是实现高效资源管理和并发执行的前提。
因为流水线的特性使得多条指令同时处于不同的执行阶段, 而异常可能在任意时刻发生。精确异常要求能够准确识别并报告导致异常的具体指令, 即使它与其他正常指令混合在流水线中。这就意味着处理器需要有复杂的机制来跟踪和管理指令的执行状态, 确保异常处理时能够正确地回溯到引起异常的那条指令。
非精确异常(Imprecise Exception)处理机制则在某些设计中作为一种折衷方案被采用, 特别是在早期或资源受限的处理器中。这种机制在检测到异常时, 可能无法精确指出是哪条指令导致异常, 而是报告一个范围或最近的检查点。因为那条指令可能仍然在流水线的某个阶段中。因此, 处理器可能会将当前程序计数器(PC)的值, 即下一个将要执行的指令的地址, 作为异常程序计数器(EPC)的值。这可能导致操作系统在异常处理时难以准确地确定是哪条指令引起的异常。
MIPS架构以及现今多数高性能处理器追求精确异常处理, 确保了更好的程序调试能力和系统稳定性。它们通过复杂的硬件设计, 如指令流水线的停顿机制、结果转发控制、以及精确的异常报告逻辑, 来实现这一点。这些设计使得处理器能够在异常发生时, 准确地识别异常指令, 并将正确的PC值(即导致异常的指令地址)保存至EPC寄存器中, 从而减轻了操作系统在异常处理上的负担, 并提高了整体系统的确定性和效率。
指令级并行
流水线挖掘了指令间潜在的并行性。这种并行性被称为指令级并行 。指令级并行(的两种主要方法:
- 流水线深度的增加(也称为流水线级数的增加)和多发射技术。
超流水线技术: 增加流水线深度
首先, 通过增加流水线的阶段数量(即流水线深度), 可以更细粒度地分解指令的执行过程, 使得更多的指令在不同的执行阶段并行处理。在处理器设计中, 这要求每个阶段的时间尽可能相近, 以避免瓶颈, 确保流水线的顺畅运转。增加的流水线深度理论上能够提升吞吐量, 但可能会增加流水线的延迟(即从开始到完成一条指令的时间), 并可能引入了更多控制和数据相关的风险, 需要复杂的解决策略。
但是, 仅仅增加流水线的级数并不一定能提高处理器的性能。为了获得最佳的性能提升, 需要确保所有流水级都有相同的执行时间(即需要“平衡”流水线)。如果某个流水级的执行时间比其他级长得多, 那么整个流水线的性能就会被这个最慢的级所限制(称为“瓶颈”)。
多发射技术: 是指处理器在每个时钟周期能够同时启动多个指令进入流水线的不同阶段。在处理器设计中, 这意味着拥有多个执行单元(如ALU、浮点运算单元、负载/存储单元等)且它们能够并行工作。通过同时发射多条指令, 处理器能够更充分地利用计算资源, 尤其是在存在指令间独立性较好的代码上, 可以显著提升性能。多发射技术的实现需要先进的指令调度和分配逻辑, 以处理指令间的依赖关系, 确保数据和控制的一致性。
多发射技术可以分为两种类型: 静态多发射
和动态多发射
:
-
静态多发射(Static Multi-issue): 在编译时确定哪些指令可以在同一个时钟周期内并行执行。这通常要求编译器具有足够的智能来识别和利用指令间的并行性。
-
动态多发射(Dynamic Multi-issue): 在运行时动态地决定哪些指令可以在同一个时钟周期内并行执行。这通常需要一个复杂的调度器来跟踪指令的依赖性和资源使用情况。多发射技术能够显著提高处理器的性能, 因为它能够同时执行多条指令, 从而提高了指令的吞吐量。然而, 多发射技术也增加了设计的复杂性, 并且需要更多的硬件资源来实现。
静态多发射: 超长指令字技术
静态多发射: 在每个时钟周期内, 处理器能够固定数量的指令同时进入执行单元进行处理, 这个数量在设计时就被预先确定且在运行时不可更改。与之相对的是动态多发射, 后者在每个时钟周期内可以依据实际指令的依赖关系和资源情况动态决定发射指令的数量。
工作原理
-
指令预处理: 在执行前, 处理器的前端会对指令进行解码, 分析指令间的依赖关系(数据依赖、控制依赖), 并进行指令调度, 准备好的指令被送入发射队列。
-
固定数量发射: 在每个时钟周期, 不论当前指令的依赖关系如何, 处理器都会尝试发射预设数量的指令到执行单元。例如, 一个4路静态多发射处理器会在每个周期试图发射4条指令, 如果存在依赖无法满足, 则发射槽位可能空缺或用NOP填充。
-
执行与退休: 发射出去的指令在各自的执行单元中并行处理, 完成计算后, 若没有数据冲突或异常, 指令将被"退休", 即更新架构状态(如寄存器、内存)。
特点:
- 设计简单, 可预测性能, 利用率可能不高。
局限性:
- 效率问题, 灵活性不足, 资源分配固定。
动态多发射: 超标量流水线技术
动态多发射: 允许处理器在每个时钟周期内根据当前指令的依赖关系和可用资源动态地决定发射指令的数量和类型。
与静态多发射相比, 动态多发射更加灵活和高效, 但同时也带来了更高的设计复杂度。
工作原理
-
指令预处理与分析: 首先, 处理器的前端会进行指令预取、解码, 并通过指令调度器分析指令之间的数据依赖(ReadAfterWrite, WriteAfterRead,WriteAfterWrite)和控制依赖。这一阶段还会考虑到指令对资源的需求, 比如ALU、浮点单元、访存单元等。
-
动态资源分配: 根据当前指令队列中指令的依赖关系和各个执行单元的可用状态, 动态多发射处理器在每个时钟周期内智能地决定可以并行执行多少条指令。如果有足够的资源且没有数据依赖, 处理器可能会发射多条指令; 如果资源紧张或存在依赖, 发射数量则会减少。
-
乱序执行: 动态多发射通常伴随着乱序执行(Out-of-OrderExecution)机制, 允许理器在不影响程序结果的前提下, 不按程序顺序执行指令, 进一步提高并行度和性能。
-
指令完成与退休: 当指令执行完毕, 且所有依赖解决, 数据一致性和异常处理完毕, 指令被视为"退休"。这一步会更新架构状态, 如更新寄存器、内存等。
特点:
- 高度灵活性, 高效利用资源, 提高ILP。
局限性:
- 设计复杂, 预测错误风险, 功耗问题。
推测技术
推测(Speculation) 用于提高处理器的指令级并行性(ILP)。通过预判未来可能的执行路径来提前执行指令, 从而提升处理器的指令级并行性(ILP), 是现代高性能计算领域的一项重要创新。它既可以在编译器级别实施, 也能在硬件层面实现, 旨在挖掘并行性, 加速程序执行。
编译器推测与硬件推测
-
编译器推测: 在编译阶段, 编译器通过分析代码结构, 重排指令顺序, 如提前执行分支后的指令(基于分支预测)或交换load与store指令(假设地址不冲突), 以提升并行度。这种推测可能需要插入验证推测正确性的额外检查代码, 并准备好错误恢复逻辑。
-
硬件推测: 处理器在运行时动态实施推测, 如分支预测、乱序执行、store-load转发等, 硬件会缓存推测结果, 直到确定推测正确与否。错误推测会导致缓存的清除和指令的重新执行。硬件还需处理推测引发的异常管理, 确保仅在推测指令实际被执行时才触发异常处理。
静态与动态推测技术
-
静态推测: 编译器在编译时进行的推测。它基于代码的静态分析, 识别出可以进行推测的指令或代码段, 并对它们进行优化。静态推测的优点是可以在编译时完成大部分工作, 减少运行时的开销; 缺点是它无法适应程序运行时的动态变化。
-
动态推测: 处理器在运行时进行的推测。它基于处理器的状态和程序的动态行为, 动态地决定哪些指令可以进行推测。动态推测的优点是能够适应程序的动态变化, 提高推测的准确性; 缺点是增加了处理器的复杂性和功耗。
错误恢复机制
-
软件层面: 编译器插入的检查点和修复代码用于验证推测的正确性, 如果推测错误, 则执行特定的回退逻辑, 恢复到正确状态, 这可能涉及指令的重执行和状态的回滚。
-
硬件层面: 处理器通过保留推测执行的状态, 如使用保留站(ReservationStations)、重命名寄存器等机制, 来快速响应推测错误。一旦推测被证明错误, 硬件会撤销错误的执行效果, 恢复到正确状态, 并重新执行正确的指令序列。
推测引发的异常处理
-
异常的不当触发: 推测执行可能导致本不应发生的异常, 如非法地址访问。这要求有机制区分推测执行时的异常与实际执行时的异常。
-
编译器处理: 通过更复杂的推测逻辑, 暂时忽略在推测阶段产生的异常, 直至确认异常确实发生。
-
硬件处理: 硬件缓存异常状态, 直到推测指令确定被执行。若推测正确, 则正常处理异常; 若推测错误, 异常被忽略, 避免误报。
处理器设计经历了从指令集结构和工艺技术出发, 到流水线和多发射技术的应用, 再到面对功耗墙转向多处理器并行, 最终面临存储器层次挑战的演变过程。这一系列发展体现了计算机体系结构不断寻求性能提升的探索和优化路径, 以及解决随之而来的新问题的历程。