异常定义

异常(Exception)其实是一个硬件和软件组合到一起的处理过程。异常的前半生,也就是异常的发生和捕捉,是在硬件层面完成的。但是异常的后半生,也就是说,异常的处理,其实是由软件来完成的。

计算机会为每一种可能会发生的异常,分配一个异常代码(Exception Number),或叫作中断向量(Interrupt Vector)。异常发生的时候,通常是 CPU 检测到了一个特殊的信号。比如,你按下键盘上的按键,输入设备就会给 CPU 发一个信号。或者,正在执行的指令发生了加法溢出,同样,我们可以有一个进位溢出的信号。这些信号呢,在组成原理里面,我们一般叫作发生了一个事件(Event)。CPU 在检测到事件的时候,其实也就拿到了对应的异常代码。

这些异常代码里,I/O 发出的信号的异常代码,是由操作系统来分配的,也就是由软件来设定的。而像加法溢出这样的异常代码,则是由 CPU 预先分配好的,也就是由硬件来分配的。这又是另一个软件和硬件共同组合来处理异常的过程。

拿到异常代码之后,CPU 就会触发异常处理的流程。计算机在内存里,会保留一个异常表(Exception Table)。也有把这个表叫作中断向量表(Interrupt Vector Table),好和上面的中断向量对应起来。异常表中存放的是不同的异常代码对应的异常处理程序(Exception Handler)所在的地址。

CPU 在拿到了异常码之后,会先把当前的程序执行的现场,保存到程序栈里面,然后根据异常码查询,找到对应的异常处理程序,最后把后续指令执行的指挥权,交给这个异常处理程序。异常处理程序执行完毕后,再返回正常指令位置,同时恢复现场,继续执行后续指令。一个示意图如下图所示:

1

异常分类

  • 中断(Interrupt):顾名思义,程序在执行过程中,被打断了。这个打断执行的事件,来自于CPU外部的I/O设备。比如在键盘上按下一个键,就会触发一个中断类型的中断。
  • 陷阱(Trap):是程序员“故意”主动触发的异常。比如在程序上打断点,再比如应用程序调用系统调用时,从用户态切换到内核态,就是通过触发陷阱异常,去执行用户态没有权限,但异常处理程序有相应的系统权限的事情。
  • 故障(Fault):并不是开发程序时可以触发的,而是一种非计划内的错误情况。比如加法计算时的溢出。故障的一个重要特征就是,故障在异常程序处理完成之后,仍然回来处理当前的指令,而不是去执行程序中的下一条指令。因为当前的指令因为故障的原因并没有成功执行完成。
  • 中止(Abort):可以认为是故障的一种特殊情况。即没有对应的异常处理程序可以处理这种异常,程序不得不进入中止状态,是一种无法恢复的故障。

在这四种异常里,中断异常的信号来自系统外部,而不是在程序自己执行的过程中,所以称之为异步类型的异常。而陷阱、故障以及中止类型的异常,是在程序执行的过程中发生的,所以称之为同步类型的异常。

2

异常处理:上下文切换

与函数调用类似,我们在异常发生后,切换到执行异常处理程序之前,需要做保存现场的操作。以便异常返回时恢复现场。但比起函数调用,这个操作要更复杂一些:

  • 首先,因为异常情况往往发生在程序正常执行的预期之外,比如中断、故障发生的时候。所以,除了本来程序压栈要做的事情之外,我们还需要把 CPU 内当前运行程序用到的所有寄存器,都放到栈里面。最典型的就是条件码寄存器里面的内容。
  • 其次,像陷阱这样的异常,涉及程序指令在用户态和内核态之间的切换。对应压栈的时候,对应的数据是压到内核栈里,而不是程序栈里。
  • 最后,像故障这样的异常,在异常处理程序执行完成之后。从栈里返回出来,继续执行的不是顺序的下一条指令,而是故障发生的当前指令。因为当前指令因为故障没有正常执行成功,必须重新去执行一次。

所以,对于异常的处理流程,不像是顺序执行的指令间的函数调用关系。而是更像两个不同的独立进程之间在CPU层面的切换,所以这个过程我们称之为上下文切换(Context Switch)。

参考

[1] 深入浅出计算机组成原理