深刻浅出计算机组成原理学习笔记:第二十八讲

1、引子

过去这么多讲,咱们的程序都是自动运行且正常运行的。自动运行的意思是说,咱们的程序和指令都是一条条顺序执行,你不须要经过键盘或者网络给这个程序任何输入。
正常运行是说,咱们的程序都是可以正常执行下去的,没有遇到计算溢出之类的程序错误。前端

不过,现实的软件世界可没有这么简单。一方面,程序不只是简单的执行指令,更多的还须要和外部的输入输出打交道。另外一方面,程序在执行过程当中,还会遇到各类异常状况,
好比除以0、溢出,甚至咱们本身也可让程序抛出异常。java

那这一讲,我就带你来看看,若是遇到这些状况,计算机是怎么运转的,也就是说,计算机到底是如何处理异常的。程序员

2、异常:硬件、系统和应用的组合拳

一、软件异常”和“硬件异常”

一提到计算机当中的 异常(Exception),可能你的第一反应就是C++或者Java中的Exception。不过咱们今天讲的,并非这些软件开发过程当中遇到的“软件异常”,而是和硬件、系统相关的“硬件异常”。固然,“软件异常”和“硬件异常”并非实际业界使用的专有名词,只是我为了方便给你说明,和C++、Java中软件抛出的Exception进行的人为区分,你明白这个意思就好。后端

尽管,这里我把这些硬件和系统相关的异常,叫做“硬件异常”。可是,实际上,这些异常,既有来自硬件的,也有来自软件层面的。网络

好比,咱们在硬件层面,当加法器进行两个数相加的时候,会遇到算术溢出;或者,你在玩游戏的时候,按下键盘发送了一个信号给到CPU,CPU要去执行一个现有流程以外的指令,
这也是一个“异常”。一样,来自软件层面的,好比咱们的程序进行系统调用,发起一个读文件的请求。这样应用程序向系统调用发起请求的状况,同样是经过“异常”来实现的。前后端分离

二、异常的前半生和后半生

关于异常,最有意思的一点就是,它实际上是一个硬件和软件组合到一块儿的处理过程。异常的前半生,也就是异常的发生和捕捉,是在硬件层面完成的。可是异常的后半生,
也就是说,异常的处理,实际上是由软件来完成的。异步

三、异常代码

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

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

拿到异常代码以后,CPU就会触发异常处理的流程。计算机在内存里,会保留一个异常表(ExceptionTable)。也有地方,把这个表叫做中断向量表(Interrupt Vector Table),性能

好和上面的中断向量对应起来。这个异常表有点儿像咱们在第10讲里讲的GOT表,存放的是不一样的异常代码对应的异常处理程序(Exception Handler)所在的地址。

四、异常处理程序

咱们的CPU在拿到了异常码以后,会先把当前的程序执行的现场,保存到程序栈里面,而后根据异常码查询,找到对应的异常处理程序,最后把后续指令执行的指挥权,交给这个异常处理程序。

 

这样“检测异常,拿到异常码,再根据异常码进行查表处理”的模式,在平常开发的过程当中是很常见的。

五、系统和应用的组合拳

好比说,如今咱们平常进行的Web或者App开发,一般都是先后端分离的。前端的应用,会向后端发起HTTP的请求。当后端遇到了异常,一般会给到前端一个对应的错误代码。
前端的应用根据这个错误代码,在应用层面去进行错误处理。在不能处理的时候,它会根据错误代码向用户显示错误信息。

public class LastChanceHandler implements Thread.UncaughtExceptionHandler {
    @Override
    public void uncaughtException(Thread t, Throwable e) {
        // do something here - log to file and upload to    server/close resources/delete files...
    }
}

Thread.setDefaultUncaughtExceptionHandler(new LastChanceHandler());

再好比说,Java里面,咱们使用一个线程池去运行调度任务的时候,能够指定一个异常处理程序。对于各个线程在执行任务出现的异常状况,咱们是经过异常处理程序进行处理,
而不是在实际的任务代码里处理。这样,咱们就把业务处理代码就和异常处理代码的流程分开了。

3、异常的分类:中断、陷阱、故障和停止

我在前面说了,异常能够由硬件触发,也能够由软件触发。那咱们平时会碰到哪些异常呢?下面咱们就一块儿来看看。

一、第一种异常叫 中断(Interrupt)

顾名思义,天然就是程序在执行到一半的时候,被打断了。这个打断执行的信号,来自于CPU外部的I/O设备。你在键盘上按下一个按键,就会对应触发一个相应的信号到达CPU
里面。CPU里面某个开关的值发生了变化,也就触发了一个中断类型的异常。

二、第二种异常叫 陷阱(Trap)。陷阱

实际上是咱们程序员“故意“主动触发的异常。就好像你在程序里面打了一个断点,这个断点就是设下的一个"陷阱"。当程序的指令执行到这个位置的时候,就掉到了这个陷阱当
中。而后,对应的异常处理程序就会来处理这个"陷阱"当中的猎物。

最多见的一类陷阱发生在咱们的应用程序调用系统调用的时候,也就是从程序的用户态切换到内核态的时候。咱们在第3讲讲CPU性能的时候说过,能够用Linux下的time指令,去查看一个程序运行实际花费的时间,里面有在用户态花费的时间(user time),也有在内核态发生的时间(system time)。

咱们的应用程序经过系统调用去读取文件、建立进程,其实也是经过触发一次陷阱来进行的。这是由于,咱们用户态的应用程序没有权限来作这些事情,
须要把对应的流程转交给有权限的异常处理程序来进行。

三、第三种异常叫 故障(Fault)

它和陷阱的区别在于,陷阱是咱们开发程序的时候刻意触发的异常,而故障一般不是。好比,咱们在程序执行的过程当中,进行加法计算发生了溢出,其实就是故障类型的异常。这个
异常不是咱们在开发的时候计划内的,也同样须要有对应的异常处理程序去处理。

四、故障和陷阱的区别

故障和陷阱、中断的一个重要区别是,故障在异常程序处理完成以后,仍然回来处理当前的指令,而不是去执行程序中的下一条指令。由于当前的指令由于故障的缘由并无成功执行完成。

最后一种异常叫 停止(Abort)。与其说这是一种异常类型,不如说这是故障的一种特殊状况。当CPU遇到了故障,可是恢复不过来的时候,程序就不得不停止了。

五、四种异常的比较

 

在这四种异常里,中断异常的信号来自系统外部,而不是在程序本身执行的过程当中,因此咱们称之为“异步”类型的异常。而陷阱、故障以及停止类型的异常,
是在程序执行的过程当中发生的,因此咱们称之为“同步“类型的异常。

在处理异常的过程中,不管是异步的中断,仍是同步的陷阱和故障,咱们都是采用同一套处理流程,也就是上面所说的,“保存现场、异常代码查询、异常处理程序调用“。而停止类型的异常,
实际上是在故障类型异常的一种特殊状况。当故障发生,可是咱们发现没有异常处理程序可以处理这种异常的状况下,程序就不得不进入停止状态,也就是最终会退出当前的程序执行。

4、异常的处理:上下文切换

在实际的异常处理程序执行以前,CPU须要去作一次“保存现场”的操做。这个保存现场的操做,和我在第7讲里讲解函数调用的过程很是类似。

由于切换到异常处理程序的时候,其实就好像是去调用一个异常处理函数。指令的控制权被切换到了另一个"函数"里面,因此咱们天然要把当前正在执行的指令去压栈。
这样,咱们才能在异常处理程序执行完成以后,从新回到当前的指令继续往下执行。

不过,切换到异常处理程序,比起函数调用,仍是要更复杂一些。缘由有下面几点。

一、第一点

由于异常状况每每发生在程序正常执行的预期以外,好比中断、故障发生的时候。因此,除了原本程序压栈要作的事情以外,咱们还须要把CPU内当前运行程序用到的全部寄存器,
都放到栈里面。最典型的就是条件码寄存器里面的内容。

二、第二点

像陷阱这样的异常,涉及程序指令在用户态和内核态之间的切换。对应压栈的时候,对应的数据是压到内核栈里,而不是程序栈里。

三、第三点

像故障这样的异常,在异常处理程序执行完成以后。从栈里返回出来,继续执行的不是顺序的下一条指令,而是故障发生的当前指令。由于当前指令由于故障没有正常执行成功,
必须从新去执行一次。因此,对于异常这样的处理流程,不像是顺序执行的指令间的函数调用关系。而是更像两个不一样的独立进程之间在CPU层面的切换,

因此这个过程咱们称之为 上下文切换(Context Switch)。

5、总结延伸

这一讲,我给你讲了计算机里的“异常”处理流程。这里的异常能够分红中断、陷阱、故障、停止这样四种状况。这四种异常,分别对应着I/O设备的输入、
程序主动触发的状态切换、异常状况下的程序出错以及出错以后无可挽回的退出程序。

当CPU遭遇了异常的时候,计算机就须要有相应的应对措施。CPU会经过“查表法”来解决这个问题。在硬件层面和操做系统层面,各自定义了全部CPU可能会遇到的异常代码,
而且经过这个异常代码,在异常表里面查询相应的异常处理程序。在捕捉异常的时候,咱们的硬件CPU在进行相应的操做,而在处理异常层面,则是由做为软件的异常处理程序进行相应的操做。

而在实际处理异常以前,计算机须要先去作一个“保留现场”的操做。有了这个操做,咱们才能在异常处理完成以后,从新回到以前执行的指令序列里面来。
这个保留现场的操做,和咱们以前讲解指令的函数调用很像。可是,由于“异常”和函数调用有一个很大的不一样,那就是它的发生时间。函数调用的压栈操做咱们在写程序的时候彻底可以知道,而“异常”发生的时间却很不肯定。

因此,“异常”发生的时候,咱们称之为发生了一次“上下文切换”(Context Switch)。这个时候,除了普通须要压栈的数据外,计算机还须要把全部寄存器信息都存储到栈里面去。

相关文章
相关标签/搜索