# 中断/异常处理机制
作者:王仕鸿
日期:2023年4月14日
> 本文主要讲述了`RISC-V`的中断/异常处理机制,而后介绍了`X2W-OS`的中断/异常处理模块
## 一、RISC-V的中断/异常处理机制
### 1 中断/异常处理的概念
#### 1.2 问题的根本:外部随机事件
一般来说,软件控制硬件的运行。我们的程序要求`CPU`运行什么样的指令,`CPU`就会运行什么样的指令,例如:
- 现在软件运行了一个`LD`指令,那么`CPU`就会解析指令编码,而后从内存中加载数据到寄存器中。
**但是我们的程序通常没有办法处理外部随机事件,因为我们写代码的时候是不知道什么时候会发生随机事件的**。
#### 1.2 纯软件?
如果单纯的依靠软件是无法处理外部发生的随机事件,或者说只依靠软件处理外部随机事件的效率很低。例如:
- 软件需要在用户按下按键之后,接受用户按下的按键的值
- 纯软件的解决方法就是`轮询`,即每隔10毫秒去检测用户是否按下按键
- 然而轮询会浪费大量的`CPU`时间,并且实时性不高(由软件的轮询周期决定)
#### 1.3 软硬件结合!
因此,为了提升响应的实时性和处理的效率,更好的解决方案是`软硬件结合`,具体来说:
- 由外部硬件电路负责检测用户是否按下按键
- 当用户按下按键后,键盘向`CPU`发送消息
- 当`CPU`接收到键盘发来的消息后,停止当前程序的运行,去接受用户按下的按键的值
- 当`CPU`读取完用户按下的按键的值后,再继续运行当前程序
通过上面这种`软硬件结合`的方式,`CPU`可以高效的处理外部随机事件,从而使得计算机能够和充满随机事件的外部环境进行交互。
#### 1.4 中断相关的概念
由于`软硬件结合`的处理随机事件的方式会打断当前程序的运行,因此这种方式称为`中断式`随机事件处理。对应的:
- 导致中断发生的事件/原因称为`中断源`
- `CPU`运行的处理中断的程序称为`中断处理程序`
- 从硬件检测到中断发生,到硬件发消息给`CPU`,在到`CPU`运行中断处理程序,最后重新运行源程序的整个流程,称为`中断处理流程`
- 实现中断处理的软件和硬件的全体称为`中断处理机制`
- 不同架构的`CPU`有不同的硬件电路实现中断处理,但大体的流程是相通的,只是具体到寄存器、指令层面会存在不同
#### 1.5 RISC-V中的中断和异常
像上面举的例子是因为用户按键导致的`CPU`停止运行当前程序,因此是外部事件导致的`中断`。
但不要就此认为中断都是由于外部事件导致的,事实上`CPU`内部也可能会`CPU`停止运行当前程序,例如考察下面的程序:
```c
int num1 = 100, num2;
scanf("%d", &num2); // 用户输入0
int num3 = num1 / num2;
```
`CPU`最终在运行第三行的除法指令的时候就会发现由于除数为0,因此就会停止运行,转而去运行`除数为0`的中断处理函数,例如:
- 要求用户重新输出一个非0值
不过在`Linux`的绝大部分实现下,这种情况都是直接kill掉用户的程序。
除此以外,还有可能因为用户修改了编译得到的程序,导致其中有一些指令不是合法指令,`CPU`在运行的时候就会发现这个问题,从而产生`非法指令中断`,还有涉及到虚拟内存和内存换页的`缺页中断`……
因此,中断的来源可能是`CPU`外部,还有可能是`CPU`内部。
> 虽然中断的来源可能是`CPU`的外部或者`CPU`的内部,但是根本原因都是由于引入了外部随机因素
来自于`CPU`内部的中断需要在`CPU`设计的时候就将对应的硬件电路设计好,而来自于`CPU`外部的中断则需要设计专门的硬件电路模块和多种不同的外部设备对接以通知`CPU`发生了外部中断。
因此,`RISC-V`中将中断进一步进行了分类。具体来说,根据中断的来源,将中断分为:
- **来自于`CPU`外部的中断:`RISC-V`将其称为`中断`**
- **来自于`CPU`内部的中断:`RISC-V`将其称为`异常`**
> 后文中我们会使用中断和异常来分别指代对应的中断,而非将两者统称为中断
在有些体系结构中,不会严格区分异常和中断,但是在`RISC-V`中,异常和中断是被严格区分的。
#### 1.5 深入理解异常和中断
一般来说,软件控制`CPU`硬件的运行,或者说软件请求处理器硬件运行。
**而我们其实可以把异常和中断理解为处理器硬件主动请求与软件交互的一种接口**。通过这种接口,软件可以处理各种随机事件。
现代计算机,从某种程度上来说就是中断/异常驱动的。从火星上的好奇号,到构建全局地图的`SLAM`算法,语音转文本的听写输入法……的背后,都要中断/异常的身影。
中断和异常是如此的底层和常见,以至于如同空气一般,我们在使用计算机的时候已经感知不到它了。但他又如此重要,以至于没有它,计算机基本没有什么用。
我们需要开发一个操作系统,那么就必须要处理异常和中断。为此,我们需要知道中断和异常的处理流程,而后才能写代码出处理异常和中断。
由于`RISC-V`对异常和中断进行了严格的区分,因此下面分别介绍`RISC-V`的异常处理流程和`RISC-V`的中断处理流程
### 2 RISC-V的异常处理流程
不同架构的`CPU`的异常处理机制(硬件电路设计和软件)不同,因此不同架构的`CPU`的异常处理流程不同。我们因为要编写基于`RISC-V`的操作系统,所以我们需要了解`RISC-V`的异常处理流程。
`RISC-V`的异常处理流程可以分为两段:
- **陷入异常(Exception Trap)**
- **退出异常(Exception Exit)**
异常的整个处理流程是软硬件结合的,因此我们需要知道在陷入异常和退出异常的时候,硬件会做哪些事情、那些事情是需要软件执行的,而后才能够在操作系统中编写代码实现的陷入异常和退出异常。具体来说:
- `CPU`硬件执行的动作是由其硬件电路所决定的,在设计电路的时候就已经决定了硬件`CPU`会执行那些动作。所以对于`CPU`执行的动作我们只需要学习、了解即可
- 硬件自动执行动作已经规定好了,因此我们需要学习软件应该执行那些动作才能够和`CPU`硬件配合起来,而后实现
因此,下面我们会详细介绍`陷入中断`和`退出中断`时硬件自动执行的动作和软件需要执行的动作。
#### 2.1 陷入异常
**`陷入异常`指的是从`CPU`硬件检测到异常发生,经历一系列软硬件的动作后开始运行异常处理程序前的整个流程**
由于陷入异常是软件和硬件配合完成的流程,因此可以可以将`陷入异常`的流程分为两大段:
- `Top Half`:由`CPU`完成的动作
- `Bottom Half`:由软件完成动作
> **为什么称为`陷入异常`**?
>
> 之所以将进入异常处理程序称为`陷入`(`Trap`)的原因是因为最终会返回到原程序中运行,同时用于原程序和中断处理程序可能运行在不同的特权下,因此将进入中断处理程序称为陷入中断/异常
>
> 下图描述的是系统调用的程序执行流,系统调用的实现就是通过`CPU`的异常处理机制实现的。系统调用会主动通过指令触发一个异常,而后跳转到异常处理程序去运行。
>
> 只不过对于系统调用来说,异常处理程序就是系统调用的实现函数。
>
> 
##### A. Top Half
不同架构的`CPU`的`Top Half`部分干的事情不同,对于`RISC-V`来说,`CPU`会自动(按顺序)做如下的事情:
1. 将当前`pc`寄存器的值保存到`mepc`寄存器中
- 借助`mepc`寄存器,我们能够知道中断发生前当前程序执行到哪里了,因此在退出异常的时候通过把`mepc`寄存器中的值赋值给`pc`寄存器就能继续执行当前程序了
2. 将异常的类型的编号保存到`mcause`寄存器中
- 借助`mcause`寄存器,在`Bottom Half`中软件就可以知道到底发生了哪种异常,而后在`Bottom Half`的后续程序中分别进行处理
3. 将当前的虚拟地址写入到`mtval`寄存器中
- 借助于`mtval`寄存器,软件可以得知到底是访问那个虚拟地址导致了缺页中断
4. 将`mstatus`寄存器中的`MIE`字段的值复制到`mstatus`寄存器的`MPIE`字段
- `MIE`字段保存了`CPU`是否响应某些中断源
- 借助`mstatus`寄存器的`PMIE`字段,在退出异常的时候可以`CPU`可以恢复先前程序的中断使能状态
5. 设置`CPU`当前的模式(权限),即设置`mstatus`寄存器的`MPP`字段
6. 将`mstatus`寄存器中的`MIE`字段的值设置为0
- 禁止正在运行中断A的中断处理程序时候又被另一个中断B打断运行
- 即强制要求`CPU`必须处理完当前中断才能响应其他中断
- 因为稍后在`Bottom Half`软件可以修改`MIE`,所以其实到底是不是处理完当前中断才能响应其他终端是取决于用户的软件设置的
7. 设置`CPU`的模式为M模式
- `RISC-V`默认异常处理程序运行在最高的特权级
8. 读取`mvtec`寄存器中的值,跳转到异常向量表里执行异常处理程序,即进入到`Bottom Half`的程序
- 异常向量表的地址保存在`mvtec`寄存器中
- `mvtec`寄存器有两种模式,对应的`CPU`有两种不同的运行方式,后续会说明
##### B. Bottom Half
在完成`Top Half`的第八步之后,就已经开始运行软件程序了,即进入到了`陷入异常`的`Bottom Half`中。
在`Bottom Half`,软件/编写软件的开发者/操作系统需要干的事情有:
1. 保存先前程序的上下文
- **程序的上下文指的是所有的通用寄存器以及部分M模式的系统寄存器**
- `Top Half`中`CPU`不会保存通用寄存器,因此先前程序计算到一半的结果需要在`Bottom Half`中由程序来保存。保存通用寄存器实际上就是保存了先前程序计算到一半的结果,以便于后续退出中断时能够继续运行先前的程序,否则退出中断时候仅恢复程序运行,但是运算结果都不对也没有用
- 与此类似,还有一些系统寄存器保存了先前程序的状态,`Top Half`也不会保存这些状态,因此也需要保存
- **程序的上下文是能确保在退出异常时候能够继续正常的运行先前程序的所必需的**
2. 查询`macuse`寄存器中的异常编号,而后跳转到合适的异常处理程序中
- 这和`mvtec`寄存器的模式有关,后续会介绍
#### 2.2 退出异常
**`退出异常`指的是从异常处理程序运行完成到`CPU`自动执行一系列动作以恢复先前程序的流程**。
和`陷入异常`类似,`退出异常`也是需要软硬件结合的,各自会执行响应的动作。
`退出异常`的时候,软件/编写软件的开发者/操作系统需要干的事情有:
1. 恢复先前程序的上下文
- 恢复先前程序计算到一般的结果:恢复通用寄存器
- 恢复先前程序的状态:恢复一些系统寄存器
2. 执行`mret`指令
- 通知`CPU`执行一些动作
执行`mret`指令后,`CPU`硬件自动执行动作为:
1. 把`mstatus`寄存器中`MPIE`域的值复制到`MIE`域中
- 恢复先前程序的中断使能状态
2. 根据`mstatus`寄存器中的`MPP`域的值恢复`CPU`的运行模式
3. 将`mpec`寄存器中的值写入到`pc`寄存器中
- 跳转到先前程序被打断的地方,继续运行先前程序
### 3 RISC-V的中断处理流程
略
### 4. RISC-V异常/中断处理相关寄存器
> 注意,下面介绍的寄存器在异常处理和中断处理中都会用到,因此在描述的时候统一使用中断
#### A. `mtvec`寄存器
**`mtvec`寄存器(`Machine Trap Vector`)中保存了中断向量的地址**

`RISC-V`的中断有两种运行模式:
- `直接模式`:`BASE[XLEN-1:2]`保存的是**中断向量地址**,即**中断处理程序地址**
- `间接模式`:`BASE[XLEN-1:2]`保存的是**中断向量表地址**,即**中断处理程序数组**
通常而言:
- 间接模式一般用在对实时性要求高的系统上,例如:嵌入式系统、`RTOS`……
- 直接模式一般用在通用的操作系统上,例如:`Linux`……
`RISC-V CPU`具体运行在哪种模式,由`mtvec`寄存器的`mode`字段决定:
- `mode = 0b00`:直接模式
- `mode = 0b11`:间接模式
此外:
- 直接模式要求中断向量是4字节对齐的,这是因为`RISC-V 64`的指令是32位的,因此要求四字节对齐
- 间接模式要求中断向量表是256字节对齐的
#### B. `mcause`寄存器
`mcause`寄存器(`Machine Cause`)保存了中断/异常的编号。通过这编号,我们可以知道具体发生了哪个中断/异常

`mcause`寄存器的`Exception Code`字段则存储了异常/中断的编号;而`Interrupt`字段表明了打断CPU运行当前程序的是中断还是异常:
- `Interrupt = 0`:异常
- `Interrupt = 1`:中断
> `Interrupt`字实际上将`Exception Code`字段的值域一分为二,`Exception Code = 1`在`Interrupt = 0`和`Interrupt = 1`时表达的含义是截然不同的。
>
> 因此我们在写操作系统的时候需要先判断是异常还是中断,而后再调用对应的处理函数
所有的`Exception Code`如下:
#### C. `mtval`寄存器
`mtval`寄存器(`Machine Trap Value`)保存了发生中断/异常时候的虚拟地址

`mtval`寄存器和`mcause`寄存器结合,就可以完成缺页异常处理、设备缺失异常处理,即在异常/中断发生后:
1. 首先读取`mcause`寄存器,判断是否为`Page Fault`
2. 如果是`Page Fault`,则读取`mtval`寄存器,而后位对应的地址建立映射
## 二、`X2W-OS`的异常/中断处理模块
### 1 异常处理模块
略
### 2 中断处理模块
略