CSAPP(6):异常控制流

(一)异常

1

还有一些细节需要补充:

1. 事件什么时候发生?

事件可能与当前指令的执行直接相关,比如虚拟储存器缺页、算术溢出,除以0;也可能和当前指令没有关系,如IO请求、定时器信号。

2. 跳转到异常处理程序与过程调用有什么不同?

  1)过程调用时,处理器要讲返回地址压栈。然而,根据异常的类别,返回地址要么是当前指令,要么是下一条指令。(难道不需要压栈?那怎么返回?)

  2)处理器会将一些额外的处理器状态压入栈中,在处理程序返回时,重新开始被中断程序可能需要这些状态;

  3)如果控制从一个用户转移到内核,那么所有项目被压到内核栈中,而不是压到用户栈中;

  4)异常处理程序运行在内核模式下,这意味着它们对所有的系统资源都有完全的访问权限。

3.异常处理程序的返回

通过执行一条特殊的“从中断返回”指令,可选地返回到被中断的程序,该指令将适当状态弹回到处理器的控制和数据寄存器中。如果异常中断的是一个用户程序,就将状态恢复为用户模式。

2

3

(二)进程

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

进程的经典定义就是一个执行中的程序的实例。当我们运行一个程序的时候,就好像我们的程序是运行中的唯一程序,独占处理器和存储器。

那么,操作系统是如何实现这一点的?进程给应用程序提供两个抽象:

1.一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器;

2.一个私有地址空间,他提供一个假象,好像我们的程序独占地使用存储器系统。

这里有几个概念:

逻辑控制流

一个逻辑流在执行时间上与另一个流重叠,称为并发流(concurrent flow),这两个流被称为并发地运行;

如果两个流并发地运行在不同的处理器核或计算机上,那么我们称它们为并行流(parallel flow),它们并行地运行,且并行地执行。

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

私有地址空间

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

每个进程的虚拟存储器结构在前一章中已经讲过,不再赘述。

用户模式和内核模式

处理器通常是用某个控制寄存器中的一个模式位来提供这种功能的,该寄存器描述了进程当前享有的特权。

当设置了模式位,进程就运行在内核模式中;一个运行在内核模式的·进程可以执行指令集中的任何指令,并且可以访问系统中任何存储器位置。

没有设置模式位,进程就运行在用户模式中;用户模式的进程不允许执行特权指令(比如停止处理器,改变模式位,发起IO操作);也不允许直接引用地址空间中内核区内的代码和数据——必须通过系统调用接口间接地访问内核代码和数据。

进程从用户模式变为内核模式的唯一方式就是通过诸如中断、故障或陷入系统调用这样的异常。当异常发生时,控制传递给异常处理程序,处理器将模式从用户模式变为内核模式;当返回到应用程序时,处理器将模式从内核模式转变为用户模式。

上下文切换

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

内核为每个进程维持一个上下文。包括:通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,如描述地址空间的页表,包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。

当内核代表用户进行系统调用时,可能会发生上下文切换。如果系统调用因为等待某个事件发生而阻塞,那么内核可以让当前进程休眠,切换到另一个进程。比如read一个磁盘访问;比如sleep。

中断也有可能引发上下文切换,比如定时器中断发生时,内核能判定当前进程已经运行了足够的时间,并切换到另一个进程。

如果切换了进程,此时高速缓存中的内容已经对新进程没有效了。我们说:中断处理污染了高速缓存。

(三)进程控制

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

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

运行:进程要么在CPU上执行,要么在等待被执行且最后会被内核调度;

停止:进程的执行被挂起,且不会被调度。当收到SIGSTOP, SIGTSTP, SIGTTIN, SIGTTOU信号时,进程被停止;直到收到一个SIGCONT信号才会再次运行;

终止:进程永远地停止了,可能有三种原因:1)收到一个信号,该信号的默认行为是终止进程;2)从主程序返回;3)调用exit函数

4

5

6

回收子进程

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

如果父进程没有回收它的僵死子进程就终止了,那么内核就会安排init进程来回收它们。init进程的PID为1,并且是在系统初始化时由内核创建的。一个进程可以通过调用waitpid函数来等待它的子进程终止或停止。

-----------------------------------------------------------------------------------------------------------------------------------------------------------------------

现在,我们可能存在疑问:fork函数有什么用啊?按照上述方法那样用,自然没有什么用。但是一旦fork函数与execve结合起来,就能发挥很大的为例。可以写成Unix外壳程序和Web服务器程序。

7

上述程序的解释:

外壳程序主要是执行:读取来自用户的命令行,解析命令号,运行程序。

利用parseline函数,得到argv参数向量,1)返回bg:如果是0,代表前台执行(外壳等待它完成);如果是1,代表后台执行(外壳不等待它完成)。2)如果argv的第一个参数是内置的外壳指令,那么立即执行;如果时一个可执行文件,会在一个新的子进程的上下文中加载病运行这个文件。

现在的问题是:它并不会回收它的后台子进程。如何修改?需要信号

(四)信号

image

Linux有这么多信号。每种信号类型都对应于某种系统事件。

1. 低层的硬件异常是由内核异常处理程序处理的,正常情况下,对每个用户进程而言都是不可见的。信号提供了一种机制,通知用户进程发生了这些异常。例如除0对应SIGFPE;

2. 一个进程可以通过另一个进程发送一个SIGKILL信号来强制终止;

3. 当一个子进程终止或停止时,内核发送一个SIGHOLD信号给父进程。

传送信号到目的进程由两个不同的步骤组成:发送信号和接收信号

8

一个只发出而没有被接收的信号,称为待处理信号。

1.在任何时刻,一种类型至多只会有一个待处理信号; 其它的都会被丢弃。

2.一个待处理信号最多被接收一次。

一个进程可以有选择地阻塞接收某种信号,当一种信号被阻塞时,它仍可以被发送,但是产生的待处理信号不会被接收,直到阻塞取消。

内核为每个进程在pending位向量中维护着待处理信号的集合,在blocked位向量中维护着被阻塞的信号集合。只要发送了类型为k的信号,内核就会设置pending的第k位3;而只要接收了k信号,内核就会清楚pending的第k位。

9

现在回过头来看Unix后台程序回收的问题:

父进程必须回收子进程,以避免在系统中留下僵死进程。但是,我们也想让父进程在子进程运行时可以自由的做其他工作(否则,进程没有意义),所以,我们决定用SIGCHLD处理程序回收子程序,而不是显式的等待子进程终止。

10

下面再来看一个问题:由同步流引起的竞争关系。

11

这依赖于显式地阻塞和取消阻塞信号的函数:

image

(五)非本地跳转

原文地址:https://www.cnblogs.com/wangyanphp/p/6180954.html