# 中断和异常

By: [:material-github: howardlau1999](https://github.com/howardlau1999)

## 简介

在使用电脑的时候，你可以一边听音乐一边上网冲浪、一边在线聊天一边看视频，后台可能还挂着东西在下载。然而，真正同时运行的程序数量最多不会超过 CPU 的核心数，一般的电脑有 4 核，所以最多只能同时运行 4 个程序。另外，当你写的程序出 Bug 的时候，操作系统会帮你结束程序，并将程序结束的原因报告给你，而不会影响到其他程序的运行。

你或许已经知道，我们日常所使用的操作系统实际上是会不停地切换程序。然而，一旦程序开始运行，除非它主动跳转到其他程序，否则将永远独占一个核心，哪怕它是在死循环。这样的情况下，操作系统又如何获得 CPU 核心的使用权呢？答案是，我们日常所使用的 Windows 和 Linux 等操作系统是**抢占式调度**的。在操作系统启动的时候，它们会打开 CPU 上的定时器中断，也就是说，每隔一段时间，CPU 就会强行暂停当前运行的程序，并跳转到操作系统设置的中断处理程序中。这时，操作系统就有机会保存好进程的状态，并执行调度算法选择下一个需要运行的程序了。当然，除了抢占式调度，还有协作式调度，在这种调度方式中，需要程序员在代码中手动插入跳转到操作系统的代码，否则程序将一直运行下去。

而在程序出 Bug 的时候，例如访存越界、除零错误等，则会触发 CPU 中的异常，这时，CPU 同样会强行暂停目前的程序，跳转到操作系统预先设置好的异常处理程序中。这样，操作系统就可以记录下错误的原因，并调用程序员设置好的异常处理程序来处理错误。当然，你也很少编写自己的异常处理程序，那么，操作系统便会默认地将你的进程结束掉了。

在本实验中，你将通过实践了解 CPU 的中断和异常是如何实现的，CPU 是如何打断正常的执行流进入中断和异常处理程序的，又是如何告诉中断处理程序必要的信息，以及如何从中断处理程序返回到正常的执行流中。下面将带你初步了解 RISC-V 中中断和异常的相关概念知识。




## 中断、异常和陷入的定义

本实验中中断、异常和陷入的定义都以 RISC-V 非特权级手册中第一章的定义为准：

!!! quote "1.6. Exceptions, Traps, and Interrupts"
    We use the term exception to refer to an unusual condition occurring at run time associated with an instruction in the current RISC-V hart. 
    We use the term interrupt to refer to an external asynchronous event that may cause a RISC-V hart to experience an unexpected transfer of control. 
    We use the term trap to refer to the transfer of control to a trap handler caused by either an exception or an interrupt.

异常（exception）指运行时与某一指令相关的不寻常情况，例如整数除以零异常、指令地址未对齐异常等。

中断（interrupt）指来自CPU外部的事件，例如键鼠输入、定时器事件等。

陷入（trap）指由异常或中断导致的 CPU 控制权切换，例如键鼠中断发生后 CPU 暂时放下当前程序，陷入至处理键鼠输入的例程，以完成键鼠操作。



## 控制和状态寄存器

CSR (Control and Status Registers, 控制和状态寄存器) 用来控制和保存 CPU 其他功能的状态，例如中断使能状态、特权等级等。在非特权级手册的 "Zicsr" 扩展部分讲述了与 CSR 相关的指令，在特权级手册则详细描述了各个 CSR 的定义及作用。

RISC-V 中为每个CPU核心保留了 4096 个 CSR 寄存器地址（12 bits），这些地址中有的是 RISC-V 必须的，有些是可选实现的，还有些由CPU厂商自行选择。为简单期间，实验中我们仅考虑CPU处于 Machine 特权级，其拥有最高特权。User 和 Supervisor 特权级不做讨论。Machine 特权级下，相应的 CSR 名称以 m 开头，一些 RISC-V 必须的 CSR 如下所示。

mstatus, mtvec, mip&mie, mepc, mcause, 

### `mstatus` 寄存器

`mstatus` 寄存器用于记录机器模式下的状态，例如中断是否启用等。

### `mepc` 寄存器

`mepc` 寄存器保存了中断返回后需要执行的指令地址，当 CPU 执行中断时，`mepc` 寄存器被自动设置为当前指令的地址，如果 `EX` 阶段正在执行跳转，则设置为跳转的目标地址。

### `mcause` 寄存器

`mcause` 寄存器保存了中断的原因。
### `mtvec` 寄存器

`mtvec` 寄存器保存了陷入处理程序的地址和设置，当 CPU 执行中断时，`pc` 寄存器被自动设置为 `mtvec` 寄存器中保存的陷入处理程序的地址。

在中断发生的时候，CPU 需要清空并阻塞流水线，并在 CSR 寄存器写入中断相关的信息。由于 CSR 寄存器堆实现只有一个读写端口，因此，CPU 需要多个周期才能完成 CSR 寄存器的写入。在设置完 CSR 寄存器后，发出控制信号，CPU 跳转到 `mtvec` 中保存的指令地址，开始执行中断处理程序。

## 中断处理程序

为了尽快处理中断，CPU 仅会将最基本的中断发生地址和中断原因等保存到 CSR 寄存器中。更复杂的功能由中断处理程序实现。

程序正常执行的过程中，会使用到通用寄存器堆。为了能够让中断返回后，原来的执行流程不受影响，中断处理程序需要立刻先将当前所有寄存器的内容写入内存中，然后再调用我们自定义的函数处理中断。在函数返回后，我们需要将原来内存中的寄存器内容恢复到 CPU 中，并调用 `mret` 指令从中断中返回，恢复原来程序的执行。

此外，中断处理程序本身也需要栈来保存函数栈帧，所以中断处理程序中还需要切换栈。为了安全考虑，中断处理程序使用单独的内存空间作为其运行栈。上面所说的保存寄存器内容，可以直接保存在中断栈上。


## CSR 相关指令

`csrrw`、`csrrs`、`csrrc` 及其相应立即数版本指令（带 i 后缀），都能读出指定 CSR 的值并同时修改 CSR 的内容。相应的指令格式和具体作用可查询 非特权级手册 的 "Zicsr" 扩展部分。


