深入浅出计算机组成原理:异常和中断-程序出错了怎么办?(第28讲)

一、引子

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

不过,现实的软件世界可没有这么简单。一方面,程序不仅是简单的执行指令,更多的还需要和外部的输入输出打交道。另一方面,程序在执行过程中,还会遇到各种异常情况,
比如除以0、溢出,甚至我们自己也可以让程序抛出异常。

那这一讲,我就带你来看看,如果遇到这些情况,计算机是怎么运转的,也就是说,计算机究竟是如何处理异常的。

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

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

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

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

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

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

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

3、异常代码

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

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

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

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

4、异常处理程序

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

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

5、系统和应用的组合拳

比如说,现在我们日常进行的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里面,我们使用一个线程池去运行调度任务的时候,可以指定一个异常处理程序。对于各个线程在执行任务出现的异常情况,我们是通过异常处理程序进行处理,
而不是在实际的任务代码里处理。这样,我们就把业务处理代码就和异常处理代码的流程分开了。

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

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

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

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

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

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

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

我们的应用程序通过系统调用去读取文件、创建进程,其实也是通过触发一次陷阱来进行的。这是因为,我们用户态的应用程序没有权限来做这些事情,
需要把对应的流程转交给有权限的异常处理程序来进行。

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

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

4、故障和陷阱的区别

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

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

5、四种异常的比较

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

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

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

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

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

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

1、第一点

因为异常情况往往发生在程序正常执行的预期之外,比如中断、故障发生的时候。所以,除了本来程序压栈要做的事情之外,我们还需要把CPU内当前运行程序用到的所有寄存器,
都放到栈里面。最典型的就是条件码寄存器里面的内容。

2、第二点

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

3、第三点

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

所以这个过程我们称之为 上下文切换(Context Switch)。

五、总结延伸

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

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

而在实际处理异常之前,计算机需要先去做一个“保留现场”的操作。有了这个操作,我们才能在异常处理完成之后,重新回到之前执行的指令序列里面来。
这个保留现场的操作,和我们之前讲解指令的函数调用很像。但是,因为“异常”和函数调用有一个很大的不同,那就是它的发生时间。函数调用的压栈操作我们在写程序的时候完全能够知道,而“异常”发生的时间却很不确定。

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

原文地址:https://www.cnblogs.com/luoahong/p/11425628.html

时间: 2024-08-29 21:22:01

深入浅出计算机组成原理:异常和中断-程序出错了怎么办?(第28讲)的相关文章

深入浅出计算机组成原理

课程目录:第00课.开篇词丨为什么你需要学习计算机组成原理?.rar第01课.冯-诺依曼体系结构:计算机组成的金字塔.rar第02课.给你一张知识地图,计算机组成原理应该这么学.rar第03课.通过你的CPU主频,我们来谈谈“性能”究竟是什么?.rar第04课.穿越功耗墙,我们该从哪些方面提升“性能”?.rar第05课.计算机指令:让我们试试用纸带编程.rar第06课.指令跳转:原来if.rar第07课.函数调用:为什么会发生stackoverflow?.rar第08课.ELF和静态链接:为什么

深入浅出计算机组成原理:通过你的CPU主频,我们来谈谈“性能”究竟是什么?(第3讲)

一.性能到底指的是什么? 买新电脑的时候,我们会说:"原来的电脑性能跟不上了" 写程序的时候,我们会说:"这个程序西能需要优化一下" 1."性能"到底指的是什么? 在计算机组成原理乃至体系结构中"性能"都是最重要的一个主题.我在前面说过学习和研究计算机组成原理,就是在理解计算机是怎么运作的, 以及为什么要这么运作."为什么"?所要解决的事情,很多时候就是提升"性能" 2.性能有几个指标?

深入浅出计算机组成原理:数据完整性(下)-如何还原犯罪现场(第50讲)

一.引子 讲完校验码之后,你现在应该知道,无论是奇偶校验码,还是CRC这样的循环校验码,都只能告诉我们一个事情,就是你的数据出错了.所以,校验码也被称为检错码(Error Detecting Code). 不管是校验码,还是检错码,在硬件出错的时候,只能告诉你“我错了”.但是,下一个问题,“错哪儿了”,它是回答不了的.这就导致,我们的处理?式只有一种,那就是当成“哪儿都错了”.如果是下载一个文件,发现校验码不匹配,我们只能重新去下载:如果是程序计算后放到内存里面的数据,我们只能再重新算一遍. 这

深入浅出计算机组成原理:GPU(下)-为什么深度学习需要使用GPU?(第31讲)

一.引子 上一讲,我带你一起看了三维图形在计算机里的渲染过程.这个渲染过程,分成了顶点处理.图元处理.栅格化.片段处理,以及最后的像素操作.这一连串的过程, 也被称之为图形流水线或者渲染管线. 因为要实时计算渲染的像素特别地多,图形加速卡登上了历史的舞台.通过3dFx的Voodoo或者NVidia的TNT这样的图形加速卡,CPU就不需要再去处理一个个像素点的图元处理.栅格化和片段处理这些操作.而3D游戏也是从这个时代发展起来的. 你可以看这张图,这是“古墓丽影”游戏的多边形建模的变化.这个变化,

深入浅出计算机组成原理:32-FPGA、ASIC和TPU(上)-计算机体系结构的黄金时代(第32讲)

一.引子 过去很长一段时间里,大家在讲到高科技.互联网.信息技术的时候,谈的其实都是“软件”.从1995年微软发布Windows 95开始,高科技似乎就等同于软件业和互联网.著名的风险投资基金Andreessen Horowitz的合伙人Marc Andreessen,在2011年发表了一篇博客,声称“Software is Eating the World”.Marc Andreessen,不仅是投资人,更是Netscape的创始人之一.他当时的搭档就是我们在前两讲提过的SGI创始人Jim C

深入浅出计算机组成原理:Superscalar和VLIW-如何让CPU的吞吐率超过1?(第26讲)

一.引子 到今天为止,专栏已经过半了.过去的20多讲里,我给你讲的内容,很多都是围绕着怎么提升CPU的性能这个问题展开的.我们先回顾一下第4讲,不知道你是否还记得这个公式: 程序的CPU执行时间 = 指令数 × CPI × Clock Cycle Time 这个公式里,有一个叫CPI的指标.我们知道,CPI的倒数,又叫作IPC(Instruction Per Clock),也就是一个时钟周期里面能够执行的指令数,代表了CPU的吞吐率.那么,这个指标,放在我们前面几节反复优化流水线架构的CPU里,

深入浅出计算机组成原理:SIMD:如何加速矩阵乘法?(第27讲)

一.引子 上一讲里呢,我进一步为你讲解了CPU里的“黑科技”,分别是超标量(Superscalar)技术和超长指令字(VLIW)技术. 超标量(Superscalar)技术能够让取指令以及指令译码也并行进行:在编译的过程,超长指令字(VLIW)技术可以搞定指令先后的依赖关系,使得一次可以取一个指令包. 不过,CPU里的各种神奇的优化我们还远远没有说完.这一讲里,我就带你一起来看看,专栏里最后两个提升CPU性能的架构设计.它们分别是,你应该常常听说过的 超线程(Hyper-Threading)技术

深入浅出计算机组成原理:解读TP-设计和拆解一块ASIC芯片(第33讲)

一.引子 过去几年,最知名.最具有实用价值的ASIC就是TPU了.各种解读TPU论文内容的文章网上也很多.不过,这些文章更多地是从机器学习或者AI的角度,来讲解TPU. 上一讲,我为你讲解了FPGA和ASIC,讲解了FPGA如何实现通过“软件”来控制“硬件”,以及我们可以进一步把FPGA设计出来的电路变成一块ASIC芯片. 不过呢,这些似乎距离我们真实的应用场景有点儿远.我们怎么能够设计出来一块有真实应用场景的ASIC呢?如果要去设计一块ASIC,我们应该如何思考和拆解问题呢?今天,我就带着你一

深入浅出计算机组成原理:设计?型DMP系统(下)-SSD拯救了所有的DBA(第53讲)

一.上节总结回顾 上一讲里,根据DMP系统的各个应用场景,我们从抽象的原理层面,选择了AeroSpike作为KV数据库,Kafka作为数据管道,Hadoop/Hive来作为数据仓库. 不过呢,肯定有不信邪的工程师会问,为什么MongoDB,甚至是MySQL这样的文档数据库或者传统的关系型数据库不适应呢?为什么不能通过优化SQL.添加缓存这样的调优手段,解决这个问题呢? 今天DMP的下半场,我们就从数据库实现的原理,一起来看一看,这背后的原因.如果你能弄明表今天的这些更深入.更细节的原理,对于什么