异常的类别
异常分为四类:中断、陷阱、故障和终止。
1、中断:硬件中断的异常处理程序常常称为中断处理程序。
2、陷阱和系统调用
陷阱是有意的异常,是执行一条指令的结果。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
3、故障
故障由错误情况引起,它可能能够被故障处理程序修正。例如缺页异常。如果处理程序能够修正这个错误情况,它将控制返回到引起故障的指令,从而重新执行它。
4、终止
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者SRAM位被损坏时发生的奇偶错误。终止处理程序从不将控制返回给应用程序。处理程序将控制返回给一个abort例程,该例程会终止这个应用程序。
Linux/x86-64系统中的异常
x86-64系统定义的一些异常。有高达256种不同的异常类型。0-31的号码对应的是由Intel架构师定义的异常,因此对任何x86-64系统都是一样的。32~255的号码对应的是操作系统定义的中断和陷阱。
"异常"仅仅表示同步事件引起的控制流的改变。
进程
异常是允许操作系统内核提供进程概念的基本构造块,进程是计算机科学中最深刻、最成功的概念之一。
进程的经典定义就是一个执行中的程序的实例。
进程提供给应用程序的关键抽象:
- 一个独立的逻辑控制流,它提供了一个假象,好像我们的程序独占地使用处理器。
- 一个私有的地址空间,它提供了一个假象,好像我们的程序独占地使用内存系统。
逻辑控制流
即使在系统中通常有许多其他程序在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。
单核的处理器中,进程是轮流使用处理器的。每个进程执行它的流的一部分,然后被抢占,然后轮到其他进程。
并发流
一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发地运行。
多个流并发地执行的一般现象被称为并发。一个进程和其他进程轮流运行的概念称为多任务。一个进程执行它的控制流的一部分的每一时间段叫做时间片。因此,多任务也叫做时间分片。
并发流的思想与流运行 的处理器核数或者计算机数无关。如果两个流在时间上重叠,那么它们就是并发的,即使它们是运行在同一个处理器上。如果两个流并发地运行在不同的处理器核或者计算机上,那么我们称它们为并行流,它们并行地运行,且并行地执行。
私有地址空间
进程为每个程序提供了一种假象,好像它独占地使用系统地址空间。进程为每个程序提供它自己私有地址空间。
用户模式和内核模式
为了使操作系统内核提供一个无懈可击的进程抽象,处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。
处理器通常是用某个控制寄存器中的一个模式位,来提供这种功能的,改寄存器描述了进程当前享有的特权。当设置了模式位时,进程就运行在内核模式中(有时叫做超级用户模式)。一个运行在内核模式的进程可以执行指令集中任何指令,并且可以访问系统中的任何内存位置。
没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令。也不允许用户模式中的进程直接引用地址空间中内核区内的代码和数据。任何这样的尝试都会导致致命的保护故障。反之,用户程序必须通过系统调用接口间接地访问内核代码和数据。
运行应用程序代码 的进程初始时是在用户模式的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改为用户模式。
上下文切换
内核为每个进程维持一个上下文,上下文就是内核重新启动一个被抢占的进程所需的状态。
当Unix系统级函数遇到错误时,它们通常会返回-1.
进程控制
获取进程ID
每个进程都有一个唯一的正数(非零)进程ID(PID).getpid函数返回调用进程的PID。getpid函数返回它的父进程的PID(创建调用进程的进程).
创建和终止进程
从程序员的角度,我们可以认为进程总是处于下面三种状态之一:
运行。进程要么在CPU上执行,要么在等待被执行且最终会被内核调度。
停止。进程的执行被挂起,且不会被调度。当收到SIGSTOP、SIGTSTP、GIGTTIN或者SIGTTOU信号时,进程就停止,并且保持停止直到它收到一个SIGCONT信号,在这个时刻,进程再次开始运行。
终止。进程永远地停止了。进程会因为三种原因终止:1)收到一个信号,该信号的默认行为是终止进程 2)从主程序返回,3)调用exit函数。
void exit (int status);
exit函数以status退出状态来终止进程(另一种设置退出状态的方法是从主程序中返回一个整数值)。
父进程通过调用fork函数创建一个新的运行的子进程。
子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。
fork函数只被调用一次,却会被返回两次:一次是在调用进程(父进程)中,一次是在新创建的子进程中。在父进程中,fork返回子进程的PID。在子进程中,fork返回0.因为子进程的PID总是为非零,返回值就提供一个明确的方法来分辨程序是在父进程还是在子进程中执行。
父进程和子进程是并发运行的独立进程。
相同但是独立的地址空间
回收子进程
当一个进程由于某种原因终止时,内核并不是立即将它从系统中清除。相反,进程被保持在一种已终止的状态中,知道它被它的父进程回收。当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程,从此时开始,改进程就不存在了。一个终止了但还未被回收的进程称为僵死进程。
僵死进程已经终止了,而内核扔保留着它的某些状态直到父类进程回收它为止。
如果一个父进程终止了,内核会安排init进程成为它的孤儿进程的养父。init进程的PID为1,是咋系统启动时由内核创建的,它不会终止,是所有进程的祖先。如果父进程没有回收它的僵死子进程就终止了,那么内核会安排init进程去回收它们。不过,长时间运行的程序,比如shell或者服务器,总是应该回收它们的僵死子进程。即使僵死子进程没有运行,它们仍然消耗系统的内存资源。
一个进程可以通过调用waitpid函数来等待它的子进程终止或者停止。
信号
信号允许进程和内核中断其他进程。
每种信号类型都对应于某种系统事件。低层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程而言是不可见的。信号提供了一种机制,通知用户进程发生了这些异常。
信号术语
传送一个信号到目的进程是由两个不同步骤组成的:
1)发送信号。内核通过更新目的进程上下文的某个状态,发送(传递)一个信号给目的进程。发送信号可以有如下两种原因:1)内核检测到一个系统事件,比如除零错误或者子进程终止.2)一个进程调用了kill函数,显式地要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。
2)接收信号。当目的进程被内核强迫以某种方式对信号的发送做出反应时,它就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理程序的用户层函数捕获这个信号。
一个发出而没有被接收的信号叫做待处理信号(pending signal)。在任何时刻,一种类型至多只会有一个待处理信号。如果一个进程有一个类型为k的待处理信号,那么任何接下来发送到这个进程的类型为k的信号都不会排队等待;它们只会被简单的丢弃。一个进程可以选择性地阻塞接收某种信号。当一个信号被阻塞时,它仍可以被发送,但是产生的待处理信号不会被接收,直到进程取消对这种信号的阻塞。
一个待处理信号最多只能被接收一次。内核为每个进程在pending位向量中维护着待处理信号的集合,而在blocked位向量中维护着被阻塞的信号集合。只要传送了一个类型为k的信号,内核就会设置pending中的第k位,而只要接收了一个类型为k的信号,内核就会清除pending中的第k位。