CSAPP 异常处理

本文最后更新于:2022年12月8日 下午

CSAPP 第八章 异常处理

操作系统通过对控制流的状态变化作出响应,我们把这些突变称为 异常控制流(Exceptional Control Flow, ECF)。例如对于程序对磁盘请求数据,然后休眠,直到被唤醒

ECF 是操作系统用来处理I/O,进程和虚拟内存的基本机制

异常

操作系统为每种可能出现的异常都分配一个非负整数的异常号(exception number),有一次异常号是CPU设计者分配的,还有一些是操作系统分配的。

异常可能在指令执行的过程中由自身引起,如虚拟内存缺页错误,算术溢出,或者一条指令试图除以零,也有可能不会由自身引起的,如系统定时器产生的型号,或者I/O请求完成

当处理器检测到事件(异常)发生时,它会去查一张异常表(exception table)的跳转表,进行一个间接的过程调用(异常),当异常处理程序完成后,会根据引起异常事件的类型,发生以下情况的3种情况中的1中

  • 处理程序将控制返回当前指令$Icurr$,即事件发生时正在执行的指令(如 页中断)
  • 处理程序将控制返回$Icurr$,如果没有发生异常则执行下一条指令
  • 处理程序终止被中断的程序

CPU可能会发出被零除,缺页,内存访问违例,断点和算术溢出

操作系统可能会发出系统调用和外部I/O设备信号的中断

在计算机重启或者加电时,操作系统会分配和初始化一张异常表

异常的分类

类别
中断 I/O事件 I/O的异常是异步的
陷阱 debugger 同步
故障 缺页错误 同步
中止 中断处理程序无法处理 同步

中断

中断是异步产生的,一般是处理器外部的硬件I/O发出的,它不是有一条专门的指令发出的,所以从这种意义上来说它是异步的。

中断产生的时候,会向CPU特定引脚触发一个信号,然后把异常号放在系统总线上,这个异常号标识了具体产生异常的设备,如可能是网络适配器,硬盘适配器和定时器芯片

中断一旦产生,CPU会在当前指令执行完成后, 会发现中断引脚的电压变高了,它就会去从系统总线中读取异常号。

然后调用其对应的中断处理程序, 将当前程序栈保留,再将控制权交给中断异常处理程序,等它处理完毕后继续下一条指令

陷阱和系统调用

这种异常类型是同步发生的,是执行当前指令的结果,这种指令我们称为故障指令(faulting instruction)

陷阱是有意的异常,是执行一条指令的结果,陷阱处理程序将控制返回到下一条指令,陷阱最重要的用途是用户态到内核态转换,叫系统调用

例如,读取文件,开启新的程序,结束当前进程,操作系统提供了一个特殊的 ”syscall n“ 指令,当用户想要请求服务 n 时,可以执行这条指令。当执行syscall的时候会导致一个异常处理程序的陷阱,然后异常处理程序会解析参数,再调用对应的内核程序

从程序员的角度来看,系统调用和运行普通函数差不多。但是它们的实现确十分不同,普通的函数会运行在用户模式下,用户模式下限制了函数可以执行的指令类型。而且他们只能访问和调用函数相同的栈。系统调用运行在内核模式中,内核模式可以调用一些特权指令,并访问定义在内核中的栈。

故障

故障是由错误引起,它有可能能被故障处理程序处理,也可能故障处理程序处理不了,如果故障处理程序能够这个错误,那么当它处理完之后会将控制权转移给故障处理程序。否则处理程序将返回到内核中的abort例程abort例程会中止掉引起故障的应用程序。

最典型的故障就是缺页错误,当指令引用一个虚拟地址,而该地址的真实地址并没有被加载到内存中的时候,就会触发故障,当缺页处理程序从磁盘中将数据加载完毕后,会将控制权重新返回引起故障的指令,这个时候数据已经加载好了,重新执行这条指令就能取到数据了

终止

终止是不可恢复的致命错误导致的结果,通常是一些硬件错误,比如DRAM或者SRAM位损坏时发生的奇偶错误,终止处理程序从来不将控制返回给任务程序,它会将控制返回abort例程,abort例程会终止掉这个应用程序

除法错误

一般为除0错误,当一个除法指令的结果对于目标数来说太大的时候,就会发生除法错误(异常0), Unix不会试图从除法错误中恢复,而是选择终止程序。

一般保护异常

许多原因都会导致一般保护异常,最常见的就是一个程序引用了虚拟内存区域(野指针),或者程序尝试去对一个只读的文本段进行写的操作,Linux不会尝试回复这类故障,Linux shell通常会把这种保护故障报告为”段故障“(Segmentation fault)

缺页

缺页错误时重新执行产生故障的指令的一个异常示例。处理程序将适当的磁盘上虚拟内存的一个页面映射到物理内存的一个页面,然后重新执行这条产生故障的指令。

机器检查

导致故障的指令执行中检测到致命的硬件错误时发生的。机器检查处理程序从不返回控制给引用程序

inter手册

可以看到x86-64系统定义的一些错误,0~31都是intel内部定义的一些错误,但是intel还保留了一些没有被使用32-255是提供给操作系统可以自定义的错误

并发流

一个逻辑流和另一个逻辑流在执行时间上出现重叠,称为并发流

多个流并发执行的一般现象称为并发,一个进程和其它进程轮流运行的概念称为多任务(multitasking),一个进程执行它的控制流的一部分的每一时间段叫时间片(time slice)。因此,多任务也叫时间分片(time slicing)

用户模式和内核模式

处理器通过用户模式和内核模式来限制应用的可执行指令,处理器一般是通过某个控制寄存器中的一个模式位(mode bit)来提供这种功能,这个寄存器会描述当前进程享有的特权。

当运行在内核模式下,可以执行指令中的任何指令,并且可以访问系统中内存的任何位置。

在没有设置模式位的情况下,进程就会运行在用户模式下,那么这个进程就不允许执行特权指令(privileged instruction),如停止处理器,开启一个新的进程,发起I/O事件,改变模式位,也不允许用户模式中进程直接引用地址空间中内核区内的代码和数据。任何这样的尝试都会导致致命的保护故障,用户模式下的进程需要通过系统调用(system call)的方式来间接的调用。

应用程序初始化是在用户模式中的,进程从用户模式变为内核模式的唯一方式是诸如中断,故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常程序,处理器将模式从用户模式变为内核模式。在内核模式下执行完异常程序后,处理器会将模式修改为用户模式

上下文切换(进程切换)

操作系统内核使用一种称为上下文切换(context switch)的较高层形式的异常控制流来实现多任务。

内核会为每个进程维护一个上下文(context),上下文中会有每个进程中的一些状态,这些状态在进行进程切换的时候会需要用到,例如重启一个之前被抢占的进程。这种决策就叫做调度(scheduling),是内核中的调度器(scheduler)来负责处理的。

当内核选择一个新的进程执行的时候,我们称为是内核调度了这个进程,在内核调了这个进程后,就会抢占当前进程。使用上下文切换的机制来将控制转移到新的进程。

  1. 保存当前进程的上下文
  2. 恢复某个之前被抢占进程的上下文
  3. 将控制权恢复到这个新的进程

如果某个系统调用因为等待而发生阻塞,那么内核可以让当前进程休眠,切换到其它进程。如一个read系统调用需要访问磁盘,内核可以选择上下文切换,运行新的进程,而不是等待数据从磁盘到达。另一个示例是sleep系统调用,它显示的请求让当前进程休眠

但有些请求,即使系统调用没有发生阻塞,内核也可以决定执行上下文切换。

中断也可能会引发上下文切换。比如所有系统都有某种产生周期性定时器中断的机制,通常为1ms或者10ms,每次发成定时器中断,系统就会认为当前进程执行的时间已经足够长了,切换到下一个进程。

回收子进程

僵尸进程

如果一个进程因为某种原因终止,内核不会立即将它从系统中清除。进程会被保持在一种已终止的状态中,直到被父进程回收(reaped)。在没有被回收的时候称为僵尸进程。当父进程回收已经终止的子进程时,子进程会将退出状态传递给父进程,此时内核会将子进程在系统中真正的清除。

孤儿进程

当父进程终止,而子进程确还在时,那么子进程就是孤儿进程,孤儿进程会被init进程收养,init进程1号进程,是系统启动时内核创建的,它不会终止,是所有进程的祖先

参考链接

https://www.cnblogs.com/leijiangtao/p/4198313.html

信号


本博客所有文章除特别声明外,均采用 CC BY-SA 4.0 协议,转载请注明出处。