本文已参与「新人创作礼」活动,一起开启掘金创作之路。
异常
理想上,我们的程序都是自动且正常运行的,程序和指令都是一条条的顺序执行。我们不需要通过外设给程序任何输入。但是现实中,程序在执行指令时,还要和外部的输入输出通信,有时也会遇到各种异常,比如栈溢出。
异常可以分为来自硬件层面的和来自软件层面的。
硬件层面的异常,比如两个数相加会遇到算术溢出,比如按下鼠标或者键盘发送信号到CPU,CPU需要去执行现有流程之外的指令。
软件层面的异常,比如应用程序进行系统调用(用户态到内核态)。
有关异常,他的发生和捕捉,是由硬件完成。他的处理,是由软件来完成。
计算机会为每一种异常分配一个异常代码(也叫中断向量)——查询相应异常处理程序的入口。像I/O发出的信号的异常代码,由操作系统(软件)分配。像加法溢出的异常代码,由硬件来分配。
异常发生时,通常是CPU检测到了一个特殊信号,这个信号我们一般叫做发生了一个事件。
CPU在检测到事件的时候,也拿到了异常代码(中断向量)。
发生异常,CPU拿到异常代码后,CPU就会触发异常处理流程。内存中,会有一个异常表(中断向量表),这个表存放的是不同异常代码对应的异常处理程序的地址。
拿到异常代码之后,CPU把当前的程序的执行现场,保存到程序栈中,然后根据异常代码查询并且找到对应的异常处理程序,然后把指令执行的指挥权交给异常处理程序。
异常的分类
-
中断,程序执行到一半的时候,被打断了。而这个打断的信号,来自于CPU外部的I/O设备。
-
陷阱,我们在程序中设置一个“陷阱”,当程序走到这里的时候,就掉入了这个陷阱中。然后对应的异常处理程序来处理陷阱中的异常。
-
故障,我们程序在执行中,加法发生了溢出,或者栈溢出,这类异常就是故障。
区别于陷阱和中断的是,故障的异常处理程序处理完之后,仍然要执行当前的指令,而不是当前指令的下一条指令。因为发生故障时当前指令并没有成功执行,所以要重新执行。
-
中止,CPU遇到故障,恢复不过来,程序不得不中止。
异常的处理:上下文切换
之前我们提到在执行异常处理程序之前,CPU要去“保存现场”,然后再去执行异常处理程序。这个过程有点像函数调用,不过相比于函数调用,切换到异常处理程序还有更复杂的几点。
- 异常往往发生在程序正常执行之外。比如中断、故障发生时。所以我们不仅要把程序压栈,还要把所有用到的寄存器都放到栈中。
- 像陷阱这样的异常,涉及到用户态到内核态的切换。压栈时,对应的数据要压到内核栈中。
- 像故障这样的异常,异常处理程序执行完之后,不是顺序执行下一条,而是执行当前故障发生的指令。
所以,相比于函数调用的过程,异常处理流程更像是两个独立进程之间在CPU层面的切换,我们叫这个过程为上下文切换。