大家好,我叫徐锦桐,个人博客地址为www.xujintong.com,github地址为https://github.com/xjintong。平时记录一下学习计算机过程中获取的知识,还有日常折腾的经验,欢迎大家访问。
一、异常控制流(ECF)
现代系统通过使控制流发生突变来对这些情况做出反应。一般而言,我们把这些突变称为异常控制流(Exceptional Control Flow,ECF)。
ECF是操作系统用来实现I/O、进程和虚拟内存的基本机制。应用程序通过使用陷阱(trap)或者系统调用(system call)的ECF形式,向操作系统请求服务(比如向磁盘写入数据、创建一个新进程、终止当前进程,都是通过系统调用来实现的)。
二、异常
异常是异常控制流的一种形式,它一部分由硬件实现,一部分由操作系统实现。
异常(exception)就是控制流中的突变,用来响应处理器状态中的某些变化。
状态变化称为事件(event)。事件的发生可能和当前指令的执行直接相关。比如,发生虚拟内存缺页、算数溢出,或者一条指令试图除以0。事件也可能和当前指令的执行没有关系。比如,一个系统定时器产生信号或者一个I/O请求完成。
系统中可能的每种异常都分配一个唯一的非负整数的异常号,并且每个异常号都对应了对应异常的处理程序。异常处理程序都存储在异常表中,通过唯一的异常号可以在异常表中查找对应的异常处理程序。
异常表的起始位置放在一个叫做异常表基址寄存器的特殊CPU寄存器中,异常号是到异常表中的索引。
在IA-32(也就是32位Linux系统中) ,这个异常表是 IDT (Interrupt Descriptor Table),异常表的起始位置放在IDTR (IDT register)。在IDT中有256种不同的异常类型,具体如下:
三、异常的类别
异常可以分为四类:中断(interrupt)、陷阱(trap)、故障(fault)、终止(abort)。
3.1 中断
中断的概念
CPU在执行指令时,收到某个中断信号转而去执行预先设定好的代码,然后再返回到原指令流中继续执行,这就是中断机制。
其实举个例子来说:就是你写着作业呢,你舍友在蹲坑给你打个电话让你拿个卫生纸(发生事件),你保存下当前写的作业的状态(保存现场),然后给舍友去送纸(处理中断),送完回来继续接着写作业(恢复现场)。
中断包括可屏蔽中断和不可屏蔽中断。可屏蔽中断(EFLAG寄存器中的IF位为0)就是该中断可以被CPU屏蔽不管,也可以等待当前指令执行完然后再处理该中断。可屏蔽中断一般用于优先级较低的中断,一般来自IO设备。不可屏蔽中断(EFLAG寄存器中的IF位为1)是需要CPU立即处理该事务,不可屏蔽中断发生时,将当前的指令和状态(各个寄存器的值)存储在栈中,方便CPU处理完中断后继续恢复中断前的工作。
中断信号由外部硬件产生,并且中断是异步的,也就是说CPU不知道什么时候会产生中断,所以CPU每次取指前都要查看一下中断引脚查看是否有中断产生。
在IA-32架构中,中断产生后会通过中断号选择对应的中断处理程序,执行预先设计好处理该中断的代码,处理完成后再返回。
中断基本流程
异常的流程和中断的流程大差不差,只有一些细节不一样。
- 响应中断
系统要求中断请求信号一直保持到CPU对其进行中断响应为止。CPU每次取指前都会查看一下中断引脚如果有有效值,并且IF=1(不可屏蔽中断),CPU就会向发送中断信号的外设发送一个低电平有效的电信号,表示已经收到了你的中断信号。
- 关中断
如果中断处理程序在保存现场的过程中产生了新的中断,一些还没来得及被保存的信息可能会被破坏。因此,必须在此过程中屏蔽外部中断。CPU自动将状态标志寄存器FR或EFR的内容压入堆栈保护起来,然后将FR或EFR中的中断标志位IF与陷阱标志位TF清零,从而自动关闭外部硬件中断。
- 保护断点
保护断点就是将CS和IP/EIP的当前值压入堆栈保存,为了中断处理完毕后能够继续执行中断前的下一个指令。
- 识别并跳转到对应的中断处理程序
在IA-32中,CPU会通过中断号在IDT(中断向量表)中找到对应的中断处理程序。
- 中断处理上下部
进入中断后,当前系统处于关中断,也就是其他所有的代码都在等待当前中断处理完成,所以需要当前中断处理的要快。但是有的时候中断处理的工作量并不小,那要怎么办呢。就是将中断分为上下部,上部紧急需要立即处理完的,但是该中断也有不紧急的部分就分为下部,下部进入调度,可以以后再做,因为下部不是很着急做。
- 恢复现场
恢复中断前各个寄存器的值。
- 中断返回
栈中弹出CS和IP/EIP中断前的值,执行中断前的下一条指令。
3.2 陷阱
陷阱是有意的异常,是执行一条指令的结果。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
用户程序经常需要向内核请求服务,比如读一个文件(read)、创建一个新的进程(fork)、加载一个新的程序(execve),或者终止当前进程(exit)。为了允许对这些内核服务的受控的访问,处理器提供了一条特殊的"syscall n"指令,当用户程序想要请求服务n时,可以执行这条指令。执行 syscall 指令会导致一个到异常处理程序的陷阱,这个处理程序解析参数,并调用适当的内核程序。
3.3 故障
故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的 abort 例程,abort 例程会终止故障引起的应用程序。
故障的经典例子是缺页异常。
3.4 终止
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者SRAM位被损坏时发生的奇偶错误。终止程序从不将控制返回给应用程序。处理程序将控制返回给一个 abort 例程,该例程会终止这个应用程序。