中断/异常处理机制#

作者:王仕鸿
日期: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停止运行当前程序,例如考察下面的程序:

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#

不同架构的CPUTop 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 HalfCPU不会保存通用寄存器,因此先前程序计算到一半的结果需要在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)中保存了中断向量的地址

mtvec寄存器

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寄存器

mcause寄存器的Exception Code字段则存储了异常/中断的编号;而Interrupt字段表明了打断CPU运行当前程序的是中断还是异常:

  • Interrupt = 0:异常

  • Interrupt = 1:中断

Interrupt字实际上将Exception Code字段的值域一分为二,Exception Code = 1Interrupt = 0Interrupt = 1时表达的含义是截然不同的。

因此我们在写操作系统的时候需要先判断是异常还是中断,而后再调用对应的处理函数

所有的Exception Code如下:

Exception Code

C. mtval寄存器#

mtval寄存器(Machine Trap Value)保存了发生中断/异常时候的虚拟地址

mtval寄存器

mtval寄存器和mcause寄存器结合,就可以完成缺页异常处理、设备缺失异常处理,即在异常/中断发生后:

  1. 首先读取mcause寄存器,判断是否为Page Fault

  2. 如果是Page Fault,则读取mtval寄存器,而后位对应的地址建立映射

二、X2W-OS的异常/中断处理模块#

1 异常处理模块#

2 中断处理模块#