中断/异常、系统调用与信号

本文涉及到中断/异常、系统调用、信号、进程切换,因为有这之间涉及到很多依赖,而很多概念容易混淆,所以专门梳理一下概念

中断/异常:

中断是由间隔定时器和I/O设备产生的,即CPU的外围设备产生,以通知CPU一个事件的发生,硬件上存在可编程中断控制器(PIC, APIC),负责向 CPU 转发外设的中断信号。而异常基本上是另一个东西:由程序的错误产生,即为 CPU 执行指令时同步产生。所以中断(interrupt)又叫异步中断,因为对于 CPU 来说任何时候都有可能到来一个中断事件;而异常(exception)又叫同步中断,因为什么时候产生异常是由当前CPU所执行的指令决定的。

中断分为可屏蔽中断和不可屏蔽中断,I/O设备的中断都是可屏蔽中断;异常分为处理器探测异常和编程异常(又叫软中断),后者可以理解为有意而为之(通过执行 INT 指令)的异常,系统设计者通过它来实现系统调用。虽然中断和异常产生条件不同,但对操作系统来说,往往以相同机制处理。

中断/异常处理属于指令级别的流程控制,如同CALL / RET指令,会自动修改CS:IP寄存器(或ARM下的PC),但不同的是其硬件原理:控制单元运行每一条指令之前都会check是否有新的中断或异常产生,如果有就会执行一系列操作(目的是找到对应的中断处理程序入口),最后才是保存并修改CS:IP寄存器。这一系列操作的目的是让程序跳转到正确的中断/异常处理程序,对事件完成正确的响应,当处理程序调用完成后,会自动恢复之前保存的CS:IP,以返回到原来的执行流,这部分和RET指令类似,不过中断/异常处理程序使用IRET。

中断处理程序由内核代表当前进程执行,并且中断处理是可以嵌套的,也就是一个中断处理程序执行的过程中可以被另一个中断处理程序“中断”,整个嵌套的执行路径在IRET时所需要的恢复数据都存放在当前进程的内核栈中。而中断打断了正常执hu行流甚至另一个先前的中断处理程序,所以对其的处理必须快速执行、快速返回,也就是实时性要求非常高,这就要求中断处理程序不能引起进程切换,否则会导致整个执行路径都跟着进程切换被挂起了,这也意味着中断处理程序中的同步只能使用自旋锁而不能使用信号量;但由于中断是外围设备触发,中断到来时正在运行的可能是任一进程,而中断并不引用当前进程的专有数据结构,也就是说中断上下文与进程无关。

与中断不同,异常处理是进程相关的,因为异常通常只处于用户态时发生,因为异常往往由当前进程自身触发,所以异常处理程序可能会访问进程的数据结构(读、写),并且异常处理不允许嵌套。一个允许嵌套的特例是缺页异常发生时,但这个特例也只有两个内核控制路径的堆叠(第一个是系统调用引起,第二个是缺页引起),对于这个特例,其处理程序会挂起当前进程,调度另一个进程运行,这就会导致一次进程切换,当被挂起的进程再次获得处理器时,缺页异常处理程序恢复执行。

系统调用:

系通调用的意义是用户进程不需要直接和硬件打交道,而是将这些操作托管给操作系统内核。比如读一个文件,用户进程只需要发起一个 read 调用,而真正的 read 是无法在用户空间完成的,必须依赖系统内核,也就是切换到内核态由系统去完成并将数据传送到用户态堆栈(系统调用可以直接访问进程的地址空间)。本质上,系统调用是一个特殊的编程异常,系统调用处理程序也就是一个特殊的异常处理程序,通过软中断INT $0x80触发

信号:

信号的存在的两个主要目的:让进程知道已经发生了一个特定事件;强迫进程执行它自己代码中的信号处理函数。

信号传递的两个阶段:

信号产生 – 内核更新目标进程的进程描述符,以表示一个新信号已经发送

信号传递 – 内核强迫目标进程改变执行状态或者运行进程对应的信号处理函数

内核或者另一个进程给目标进程发送信号后,内核执行第一步,即更新一个或多个进程描述符,以完成信号的产生;这时内核并不直接执行第二步的信号传递操作,而是唤醒目标进程,促使他们接受信号。

写过 C 程序的朋友应该都知道,信号处理函数对于程序的正常执行流来说是异步执行的,换句话说,对与一个进程,随时都可能从正常的执行流跳转到信号处理函数。

那么对于系统内核,如何支持进程的这种异步的呢?对于CPU来说,只有中断才是真正的异步(指令级)。要做到一个进程执行正常的代码时,随时可以跳转到信号处理函数,这实际上是依靠中断/异常处理程序:当中断/异常处理程序从内核态返回到用户态之前,最后一个操作就是检查是否存在挂起信号,然后执行do_signal函数