csapp第八章 异常控制流

从给处理器加电开始,直到断电为止,程序计数器里总是一个一个的地址,指令的地址,假设这些地址依次是a0,a1,...,ak,其中ak是指令Ik的地址。

从ak到ak+1的过渡称为控制转移。

控制转移序列叫做处理器的控制流。

Ik+1和Ik不总是相邻的,不相邻一般是由诸如跳转、调用和返回这样的指令造成的。

跳转、调用和返回是必要的,使得程序能够对内部程序状态中的变化做出反应。这些内部程序状态是由程序变量表示的。

程序有程序的状态,系统也有系统的状态。程序的状态通过程序变量来表示,比如程序变量变成0可能就是一种程序状态,系统的状态是一种统称。

定时器产生信号、网络包到达网络适配器、子进程终止这些都是系统状态。

一般而言,我们把这些突变称为异常控制流。

这里的核心是系统状态,程序也有程序状态,程序状态的变化,由程序自身去检测并做出反应,比如if语句。系统的状态如何被检测呢?

系统状态包含了硬件的状态,也包含了软件的状态。软件的状态由包含了内核状态和应用层状态。

  • 硬件的一个系统状态的改变,会触发控制转移到异常处理程序。(程序状态变化时,会触发控制转移到该去的地方)
  • 内核状态的变化,会触发控制从一个用户进程转移到另一个用户进程。
  • 应用层状态的变化,会让一个用户进程将控制转移到其信号处理程序中。

所以,异常控制流,讲的是控制,各种状态;状态变化了,肯定有一定的方法可以检测,检测之后,就对应的转移控制。这是很容易理解的概念。其实就是我设置了一些条件,如果条件触发了,我就做一些事情。核心就是这样的。

现在有两个问题:系统状态是我设置的条件,我如何检测条件是否触发了;对各种系统状态的触发,结果是什么?

  • ECF是操作系统用来实现I/O、进程和虚拟存储器的基本机制。(鼠标的一个移动就是一个触发?然后才有了对应的控制鼠标移动?)
  • 系统调用是一种ECF,我在程序中调用系统函数,就是一个触发,然后系统给出反应?
  • 操作系统为应用程序提供了强大的ECF机制,用来创建新进程等。
  • 理解ECF将帮助你理解并发。
  • 理解ECF将帮助你理解软件异常如何工作。

我还是小看了ECF,ECF是系统的基础,系统中能称为触发的很多,或者说绝大多数的操作都可以称为触发,也就是变化系统状态,也就是ECF的一种表现,ECF无处不在。操作系统也就是一个各种触发和反应的集合体。

下面的学习记住一点,现在的触发是什么,反应是什么?

8.1 异常

异常是ECF的一种,一部分由硬件实现,一部分由操作系统实现。他就是位于硬件和操作系统之间的ECF。

硬件上,系统状态实际是处理器的状态,处理器的状态通常就是不同的位和信号(寄存器的位),处理器状态的变化(比如说某个bit置一)称为事件。

而事件,可能和当前指令的执行相关。比如当前指令执行中产生了溢出。

当然,事件也可能和当前指令没有关系,比如系统定时器产生信号,或者,一个I/O请求完成。

在任何情况下,当处理器检测到有事件发生时,它就会通过一张叫做异常表的跳转表,进行一个间接过程调用,到一个专门设计用来处理这类事件的操作系统子程序。

这里有一点:处理器如何检测到事件发生呢?如果溢出了,那么某个寄存器的某个bit会置一,置一这个动作本身,比如说低电平到高电平的上升沿,或者高电平本身就会使得后续电路有一定的响应,这个响应就是跳转到处理事件的子程序吗?这么解释应该行得通。

上面的表叫做异常表,上面的子程序叫做异常处理程序。

当异常处理程序完成处理后,根据引起异常的事件的类型:

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

系统中每种类型的异常都分配了一个唯一的非负整数的异常号,一些是处理器设计者分配的,一些是操作系统内核设计者分配的。

前者包括:被零除、缺页、存储器访问违例、断点、算术溢出。

后者包括:系统调用、来自外部I/O设备的信号。

异常表在系统启动时由操作系统分配和初始化,条目k对应异常k的处理程序的地址。

 异常可以分为四类:中断(interrupt),陷阱(trap),故障(fault),终止(abort)。

  • 中断——来自处理器外部的I/O设备的信号的结果。——中断处理程序——异步异常——由处理器外部I/O设备中的事件产生的。同步异常是执行一条指令的直接产物。
    • 中断通过向处理器芯片上的一个引脚发信号(高低电平),并将异常号放在系统总线上,以触发中断,很清楚。其中断处理程序完成后直接返回给下一条指令。
  • 陷阱——同步异常——完成处理后也返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
    • 从程序员的角度来看,系统调用和普通的函数调用是一样的。但是它们的实现是非常不同的。普通函数在用户模式,系统调用在内核模式。
    • syscall n指令,执行这条指令,会导致一个到异常处理程序的陷阱,就是跳到一个异常处理程序,这个程序会对参数解码,并调用适当的内核程序,比如read,fork,execve,exit什么的。
  • 故障——同步异常——对应的处理程序叫故障处理程序。如果处理程序可以修正这个错误(缺页异常),那么就返回到引起故障的指令,如果不能修正,就返回到内核的abort例程。
  • 终止——同步异常——对应的处理程序叫做终止处理程序,终止处理程序从不将控制返回,它会将控制返回给内核的abort例程。

IA32系统有高达256种不同的异常,0-31是intel架构师定义的异常,32-255是操作系统定义的中断和陷阱。

linux提供上百种的系统调用。——这有点出乎我的意料,上百种,感觉不多啊,感觉中那么多的系统调用函数怎么也不止上百种阿。这里却好像上百种已经很多了似的。

在IA32上,系统指令通过一条int n的陷阱指令来提供的(这是汇编的),c程序用syscall函数可以调用任何系统调用。然而,实际中几乎没必要这么做,对于大多数系统调用,标准c函数库提供了一组方便的包装函数。

这些包装函数将参数打包到一起,以适当的系统调用号陷入内核,然后将系统调用的返回状态传递回调用程序。

在本文全文中,我们将系统调用和与它们相关联的包装函数称为系统级函数。

8.2 进程

异常是允许操作系统提供进程的概念所需要的基本构造块,进程是计算机科学中最深刻最成功的概念之一。

进程的经典定义就是一个执行中的程序的实例。

系统中的每个程序都是运行在某个进程的上下文中的。

上下文是由程序正确运行所需的状态组成的。这个状态包括存放在存储器中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。

上下文是一份集合,它的每个组成部分都真实存在,但这些组成部分不是放在一起的,这个集合的概念是概念上,将这些组成部分合在一起,我们称这个集合是上下文。

当我们说在上下文中执行一个程序,其实是说,这些组成部分都分配安排好了,放在那里了,然后开始执行这些组成部分中的存储器中的代码。

进程提供给应用程序的关键抽象:

  • 一个独立的逻辑控制流,它提供了一个假象,好像我们的程序独占的使用处理器。
  • 一个私有的地址空间,它提供了一个假象,好像我们的程序独占的使用存储器系统。

上下文是集合,进程呢,其实是这个集合下实际一条一条执行代码的过程吧。进程包含上下文以及执行的过程。

PC值的序列称为逻辑控制流。

处理的实际的PC序列称为物理控制流,物理控制流总包含多个逻辑流。

进程是程序和内核之间的屏障,不,进程是应用程序的抽象,它将内核和硬件踢掉,给应用程序假象。不,内核和硬件提供了进程这种功能,应用程序都是一个一个的进程,应用程序不知道内核和硬件,只知道进程。

应用程序没有时间和空间的概念,它只知道进程,但进程自己知道,但进程不让应用程序知道。

异常处理程序、进程、信号处理程序、线程、java进程都是逻辑流的例子。

多个流并发的执行的一般现象称为并发。

一个进程和其他进程轮流运行的概念称为多任务。

一个进程执行它的控制流的一部分的每一个时间段叫做时间片。因此多任务也叫时间分片。

两个流并发的运行在不同的处理器核或者计算机上,我们称为并行流。并行流一定是并发流,并发流是老大。

一个进程为每个程序提供它自己的私有地址空间,这个空间中的某个地址相关联的那个存储器字节,一般而言,是不能被其他进程读或者写的,从这个意义上说,这个地址空间是私有的。

x86 linux地址空间顶部是保留给内核的。这部分也包含了代码、数据、栈,但是,这部分是内核在代表进程执行指令时才会使用的,比如系统调用时,使用的就是这部分空间。

每个私有地址空间都关联着一段真实的存储器空间,一般来说,一般的理解,这些对应着的真实的存储器空间应该是相互都错开的。但每个这样的空间都有相同的通用结构。

运行应用程序的进程初始时是在用户模式中的。

进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障、陷阱这样的异常。

当异常发生时,控制转移到异常处理程序,处理器将模式从用户模式变为内核模式。当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。

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

上下文切换是建立在前面的4种异常的基础上的。

内核为每个进程维持一个上下文,上下文就是内核重新启动一个被抢占的进程所需的状态。

在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。这种决定就叫做调度,是由内核中称为调度器的代码处理的。

上下文切换:

  • 保存当前进程的上下文。
  • 恢复某个先前被抢占的进程被保存的上下文。
  • 将控制传递给这个新恢复的进程。

8.3 系统调用错误

当UNIX系统级函数遇到错误时,它们典型地会返回-1,并设置全局整数变量errno来表示什么出错了。

8.4 进程控制

Unix提供了大量从c程序中操作进程的系统调用。

从程序员的角度,我们可以认为进程总处于下面三种状态之一:

  • 运行——在cpu上运行,或者,等待运行且最终会运行(会被内核调度)
  • 停止——进程被挂起(也就是被其他的进程抢占了),且不会被调度,但可以被信号唤醒
  • 终止——进程被永远的停止了,受到终止信号,或者从主程序返回,或者调用exit函数。

fork函数:

  • 调用一次,返回两次。
  • 父进程和子进程是独立的进程,并发执行的。一般而言,作为程序员,我们决不能对不同的进程中指令的交替执行做任何假设。
  • 相同的但是独立的地址空间。
  • 共享文件。

当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终止的状态中。直到被它的父进程回收。

当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程。然后抛弃已终止的进程。

一个终止了但是还未被回收的进程称为僵死进程。

如果父进程没有回收它的僵死子进程就终止了,那么内核就会安排另外一个进程来回收它们,这个进程是init。其pid为1。其在系统初始化时由内核创建。

一个进程可以通过调用waitpid函数来等待它的子进程终止,或者,停止。

这个函数有三个参数:pid, status,options。

options=0时,调用这个函数的进程挂起,也就是这个进程处于函数中什么也不做,等待着,等待什么呢,等待其子进程终止,如果终止了一个,那么函数就返回了,返回的,就是终止的子进程的pid,并且将这个子进程从系统中除去。

等待的子进程有哪些呢?这点由pid决定,pid=-1,那么就是所有的子进程,pid大于0,那么就是一个子进程,当前pid表示的那个子进程。

  • options=WNOHANG时,如果没有终止的子进程,那么函数立即返回,返回0。
  • options=WUNTRACED时,和options=0类似,但这里还检测停止的子进程。options=0只检测终止的子进程。且,本options不会将子进程从系统中除去。
  • options=WNOHANG|WNUNTRACED时,立即返回,返回值要么是停止或者终止的pid,要么是0。

如果调用进程没有子进程,那么waitpid返回-1,并设置errno为ECHLILD;如果waitpid函数被一个信号中断,那么它返回-1,并设置errno为EINTR。

status是一个指向int类型的指针,waitpid返回后,其会有相应的值,但这个值的编码貌似比较复杂,c标准库提供了一些宏来处理它。

WIFEXITED(status)等。

wait函数等价于waitpid(-1, &status, 0)。

execve函数在当前进程的上下文中加载并运行一个新程序。fork一次调用两次返回,execve调用一次,从不返回。

fork函数在新的子进程中运行相同的程序,新的子进程是父进程的一个复制品。execve函数在当前进程的上下文中加载并运行一个新的程序,它会覆盖当前进程的地址空间,但并没有创建一个新进程。新的程序仍然有相同的pid,并且继承了调用execve函数时已打开的所有文件描述符。

8.5 信号

Unix信号是一种更高层次的软件形式的异常。

linux支持30种不同类型的异常。

每种信号类型都对应于某种系统事件。底层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程而言是不可见的。信号提供了一种机制,通知用户进程发生了这些异常。

  • 发送信号——内核通过更新目的进程上下文中的某个状态,告诉目的进程,有一个信号来了。
  • 接受信号——当目的进程被内核强迫以某种方式对信号的发送做出反应时,目的进程就接收了信号。进程可以忽略、终止、捕获。

发送信号的方式——/bin/kill,键盘发送信号,kill函数,alarm函数。

接收信号的默认行为——进程终止、进程终止并转储存储器、进程停止知道被SIGCONT信号重启、进程忽略该信号。

但接受信号的行为可以不是默认行为,通过设置信号处理程序。

当一个程序要捕获多个信号时,一些细微的问题就产生了。

  • 待处理信号被阻塞——Unix信号处理程序通常会阻塞当前处理程序正在处理的类型的待处理信号。
  • 待处理信号不会排队等待——任意类型至多只有一个待处理信号。
  • 系统调用可以被中断——像read、wait、accept这样的系统调用潜在的会阻塞进程一段较长的时间,称为慢速系统调用。这一条是说:父进程有一套自己的信号处理程序,放在那里,然后父进程用了一个read函数,这个函数会执行一段时间,在执行的这段时间里,来了一个信号,所以控制转移到了信号处理程序,也就是说控制从read函数里跳到了信号处理程序。当处理程序处理完毕了,其返回,在有的系统中,返回了,但read函数的调用不再继续了,而是直接返回给用户一个错误。当然在其他的系统中,比如linux,还是会返回到read函数继续执行。

之后的这小节的内容,在机器上运行代码理解的比较实在。

有一点要明白,父子进程的,对于任意一个进程,单独的进程,其只有一份控制,这份控制可以交给陷阱(系统调用),也可以交给信号处理程序,但貌似,信号处理程序有更高的优先级,当信号来的时候,如果不被阻塞,那么立刻的就跳到了信号处理程序中处理,而不管当前的控制是否在main中还是是否在系统调用也就是内核中。而,更有一点,就是信号处理程序处理完了返回时,系统调用是否恢复是因系统而异的。

同时,父子进程之间存在这一种竞争关系,一般是:父子之间相互发信号。

8.6 非本地跳转

c语言提供一种用户级异常控制流形式——非本地跳转。

异常控制流,现在想想这个名词——很贴切阿,控制,就是控制,正常的控制移动是顺序的,线性控制流,不正常的,就是控制跳走了,就是异常的,控制跳走了,跳到哪里去了?调到异常处理程序中去了,控制怎么跳走的?很多方法阿,比如系统调用——系统调用看起来起码还是顺序的,比如read,就停在read函数那里,等read函数执行完,事实上控制已经不再main中了,而是通过read函数跳到了内核。还比如信号,进程收到信号,就触发控制跳到信号处理程序中了。

现在讲的非本地跳转,也是如此。控制跳走了。

8.7 操作进程的工具

STRACE、ps、top、pmap、/proc

8.8 小结

硬件层——四种不同类型的异常:中断、故障、终止、陷阱。

操作系统层——内核用ECF提供进程的基本概念。

在操作系统和应用程序的接口处——信号

应用层——非本地跳转

(over)

转载于:https://www.cnblogs.com/rayhill/archive/2012/05/25/2486955.html

你可能感兴趣的:(操作系统,c/c++,java)