异常控制流(Exceptional Control Flow,ECF)。本章主要讨论ECF对于应用和操作系统交互的影响。
- 异常就是控制流中的突变,用来响应处理器状态中的某些变化。在处理器中,状态被编码为不同的位和信号。状态变化称为事件。
在任何情况下,当处理器检测到有事件发生时,它就会通过一张叫做异常表的跳转表,进行一个间接过程调用,到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序)。当异常处理程序完成处理后,根据引起异常的事件的类型,会发生以下3种情况中的一种:
- 处理程序将控制返回给当前指令I-curr,即当事件发生时正在执行的指令。
- 处理程序将控制返回给I-next,如果没有发生异常将会执行的下一条指令。
- 处理程序终止被中断的程序。
- 系统中可能的每种类型异常都分配了一个唯一的非负整数的异常号。当系统启动时,操作系统分配和初始化一张称为异常表的跳转表,使得表目 k 包含异常 k 的处理程序的地址。异常号是到异常表中的索引,异常表的起始地址放在一个叫做异常表基址寄存器的特殊CPU寄存器里。
- 异常可以分为四类:中断(interrupt)、陷阱(trap)、故障(fault)和终止(abort)。
中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。硬件中断的异常处理程序常常称为中断处理程序。
陷阱是有意的异常,是执行一条指令的结果。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的abort例程,abort 例程会终止引起故障的应用程序。
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误。终止处理程序从不将控制返回给应用程序。
- 异常是允许操作系统内核提供进程概念的基本构造块。进程的经典定义就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
进程提供给应用程序的关键抽象:
- 一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
- 一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
-
逻辑控制流,简称逻辑流。
进程轮流使用处理器。每个进程执行它地流地一部分,然后被抢占(暂时挂起),然后轮到其他进程。 -
一个逻辑流地执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发地运行。多个流并发地执行地一般现象被称为并发。一个进程和其他进程轮流运行地概念称为多任务。一个进程执行它地控制流地一部分地每一时间段叫做时间片。因此,多任务也叫做时间分片。如果两个流并发地运行在不同地处理器核或者计算机上,那么我们称它们为并行流,它们并行地运行,且并行地执行。
-
进程地址空间
地址空间底部是保留给用户程序地,包括通常地代码、数据、堆和栈段。代码段总是从地址0x400000开始。地址空间顶部保留给内核(操作系统常驻内存的部分)。地址空间的这个部分包含内核在代表进程执行指令时使用的代码、数据和栈。
-
为了使操作系统内核提供一个无懈可击的进程抽象,处理器必须提供一种机制,限制一个应用可以执行的指令以及它可以访问的地址空间范围。
处理器通常是用某个控制寄存器中的模式位来提供这种功能的,该寄存器描述了进程当前享有的特权。当设置了模式位时,进程就运行在内核模式中(有时叫做超级用户模式)。一个运行在内核模式的进程可以执行指令集中的任何指令,并且可以访问系统中的任何内存位置。
没有设置模式位时,进程就运行在用户模式中。用户模式中的进程不允许执行特权指令,也不允许用户模式中的进程直接引用地址空间中内核区的代码和数据。任何这样的尝试都会导致指明的保护故障。反之,用户程序必须通过系统调用接口间接地访问内核代码和数据。
运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法就是通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。 -
操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务。内核为每个进程维持一个上下文。上下文就时内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页表、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度(scheduling),是由内核中称为调度器(scheduler)的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换 1)保存当前进程的上下文,2)恢复某个先前被抢占的进程被保存的上下文,3)将控制传递给这个新恢复的进程。
-
从程序员的角度,我们可以认为进程总是处于下面三种状态之一:
- 运行。进程要么在CPU上执行,要么在等待被执行且最终会被内核调度。
- 停止。进程的执行被挂起(suspended),且不会被调度。当收到SIGSTOP、SIGTSTP、SIGTTIN或者SIGTTOU信号时,进程就停止,并且保持停止直到它收到一个SIGGONT信号,在这个时刻,进程再次开始运行。
- 终止。进程永远的停止了。进程会因为三种原因终止:1)收到一个信号,该信号的默认行为是终止进程,2)从主程序返回,3)调用exit 函数
- 父进程通过调用fork函数创建一个新的运行的子进程。
新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。fork函数只被调用一次,却会返回两次:一次是在调用进程(父进程)中,一次是在新创建的子进程中。在父进程中,fork返回子进程的PID。在子进程中,fork返回0.
看下例:
- 当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收。当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已经终止的进程,从此时开始,该进程就不存在了。一个终止了但还未被回收的进程称为僵死进程。
如果一个父进程终止了,内核会安排init进程成为它的孤儿进程的养父。init进程PID为1,是在系统启动时由内核创建的,它不会终止,是所有进程的祖先。