前言
从给处理器加电开始,直到断电为止,程序计数器假设一个值的序列
a0,a1,,,,,,an-1
其中,每个ak是某个相应的指令Ik的地址。每次从ak到ak+1的过度称为控制转移。这样的控制转移序列叫做处理器的控制流。
异常控制流(ECF):
现代系统通过使控制流发生突变来对系统状态变化做出反应。一般而言的这些突变就称为异常控制流。
学习ECF的原因:
根据异常的事件的类型,会发生以下三种情况中的一种:
在系统启动时(当计算机重启或者加电时),操作系统分配和初始化一张称为异常表的跳转表,使得条目k包含异常k的处理程序的地址。下图为异常表的格式:
在运行时,处理器检测到发生了一个事件,并且确定了相应的异常号k。随后,处理器触发异常,方法是执行间接过程调用,通过一场表的条目k转到相应的处理程序。
异常类似于过程调用,但又有一些不同之处:
异常处理程序运行在内核模式下,这意味着他们对所有的系统资源都有完全的访问权限。
一旦硬件出发了异常,剩下的工作就是由异常处理程序在软件中完成。
异常可以分为:
- 中断:异步发生,是来自处理器外部的I/O设备的信号的结果。总是返回到下一条指令。
- 陷阱:同步,来自有意的异常,总是返回到下一条指令。
- 故障:同步,来自潜在可恢复的错误,可能返回到当前指令。
- 终止:同步,来自不可恢复的错误,通常是一些硬件错误;不会返回。
陷阱最重要的用途是在用户程序是在用户程序和内核之提供一个像过程一样的接口,叫做** 系统调用**。
进程的经典定义就是一个执行中的程序的实例。 - 进程提供给应用程序的关键抽象:
一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占的使用处理器。
一个私有的地址空间,它提供一个假象,好像我们的程序独占的使用存储器系统。
进程从用户模式变为内核模式的唯一方法是通过诸如中断,故障或者陷入系统调用这样的异常。上下文切换 :
用它这个较高层形式的异常控制流来实现多任务。
内核为每个进程维持一个上下文。在进程执行的某些时刻,内核可以决定抢占当前进程。并重新开始一个先前被抢占的进程,这种决定就叫做***调度***,是由内核中称为调度器的代码处理的。
错误报告函数:
`void unix_error(char *msg)
{
fprintf(stderr,"%s: %s\n", msg, strerror(errno));
exit(0);
}
`
通过使用错误处理包装函数,我们可以更进一步的简化我们的代码。包装函数调用基本函数,检查错误,如果有问题就终止。比如,下面是fork函数的错误处理包装函数:
`pid_t Fork(void)
{
pid_t pid;
if ((pid = fork())< 0)
unix_error("Fork error");
return pid;
}
给定这个包装函数,我们对fork的调用就缩减为1行:
`pid = Fork();`
获取进程ID
进程处于的三种状态:运行、停止、终止。
进程会因为三种原因终止:
1):收到一个信号,该信号的默认行为是终止进程
2):从主程序返回
3):调用exit函数。
回收子进程 当一个进程由于某种原因终止时,内核并不马上清除,而是把它传递给父进程,当父进程回收完子进程时,抛弃,然后该进程就不存在了。 一个终止了但还未被回收的进程称为僵死进程,而内核就会安排init进程来回收他们。init进程的PID为1,并且是在系统初始化时由内核创建的。11/21/2015 9:54:13 PM
一个进程可以通过调用waitpid函数来等待它的子进程终止或者停止。
`#include <sys/types.h>
#include <sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
返回:如果成功,则为子进程的PID,如果WNOHANG,则为0,如果其他错误,则为-1 `
判定等待集合的成员
等待集合的成员是由参数pid来确定的:
- 若pid<0,那么等待集合就是一个单独的子进程,它的进程ID等于pid。
- 若pid =0,那么等待集合就是由父进程所有的子进程组成的。
可以通过将options设置为常量WNOHANG WUNTRACED(释义详见:P496) 的各种组合,修改默认行为。 错误条件 如果调用进程没有子进程,那么waitpid返回-1,并且设置errno为ECHILD。如果waitpid函数被一个信号中断,那么它返回-1,并设置errno为EINTR.
**wait函数**是waitpid函数的简单版本:
调用wait(&status)等价于调用waitpid(-1,&status, 0)。
execve函数在当前进程的上下文中加在并运行一个新程序。
`#inclue <unistd.h>
int execve(const char *filename, const char *argv[],
const char *envp[]);
如果成功,则不返回;如果错误,则返回-1.`
execve函数调用一次并从不返回!!!
外壳Unix是一个交互型的应用级程序,他代表用户运行其他程序。最早的外壳是sh程序,后出现了一些变种,比如csh、tcsh、ksh和bash。外壳执行一系列的读/求值步骤,然后终止。该步骤读取来自用户的一个命令行。求值步骤解析命令行,并代表用户运行程序。
在外壳命令行上输入
man 7 signal 得到如下列表:
传送一个信号到目的进程是由两种不同的步骤组成的:
- 发送信号
- 接收信号:进程可以忽略这个信号,终止或者通过执行一个称为 ** 信号处理程序 ** 的用户层函数捕获这个信号。
发送信号的前提或者是原因:
- 内核检测到一个系统事件,比如被零除错误或者子进程终止。
- 一个进程调用了kill函数,显式的要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。
待处理信号:只发出而没有被接收的信号。一种类型至多只会有一个待处理信号,最多只能被接收一次。
/bin/kill/程序可以向另外的进程发送任意的信号。(一个为负的PID会导致信号被发送到进程组PID中的每个进程。)
外壳为每个作业创建一个独立的进程组。典型地,进程组ID是取自作业中父进程中的一个。。
alarm函数 - 进程可以通过alarm函数向他自己发送SIGALRM信号。 - alarm函数安排内核在secs秒内发送一个SIGALRM信号给调用进程。返回待处理闹钟的秒数。
每个信号类型都有一个预定义的默认行为,是下面的几种:
进程可以通过使用signal函数修改和信号相关联的默认行为。唯一的例外就是SIGSTOP和SIGKILL,他们的默认行为是不能被改变的。
11/22/2015 4:16:19 PM
得到的教训是:不可以用信号来对其他进程中发生的事件计数
如何解决?
不同系统之间,信号处理语义的差异,是Unix信号处理的一个缺陷。为了处理这个问题,Posix标准定义了sigaction函数,它允许像linux和solaris这样的与posix兼容的系统上的用户,明确语义。
Signal包装函数设置了一个信号处理程序,其信号处理语义如下:
应用程序可以使用sigprocmask函数显式的阻塞和取消阻塞信号:
`#include <signal.h>
int sigprocmask(int how, const sigset_t *set, sigset_t *oldset);
int sigemptyset(sigset_t *set);
int sigfillset(sigset_t *set);
int sigaddset(sigset_t *set, int signum);
int sigdelset(sigset_t *set, int signum);
返回:如果成功则为0,若出错则为-1.
int sigismember(const sigset_t *set, int signum);
返回:若signum是set的成员则为1;若不是则为0,若出错则为-1 `
sigprocmask函数改变当前以阻塞信号的集合。具体的行为依赖于how的值:
它将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用--返回序列。非本地跳转是通过setjmp和longjmp函数来提供的。
重要应用:
- 允许从一个深层嵌套的函数调用中立即返回,通常是由检测到某个错误情况引起的。
- 使一个信号处理程序分支到一个特殊的代码位置,而不是返回到被信号到达中断了的指令的位置。
C语言和Java中的软件异常: - try语句中的catch子句看作类似于setjmp函数。 - throw语句就类似于longjmp函数。
在操作系统层,内核用ECF提供进程的基本概念。进程提供给应用两个重要的抽象:
在操作系统和应用程序之间的接口处,应用程序可以创建子进程,等待他们的子进程停止或终止,运行新的程序,以及捕获来自其他进程的信号。
本周的学习内容相比第十章有点多,但是大部分都是代码所以,你只要搞懂代码,我觉得就是对大篇幅文字的很好理解,文字写的很令人易懂,所以课后的题目可以自己做一点点了,总体自我感觉还是蛮好,8000多的字都是自己一个个敲上去的,没有半点粘贴。所以心里很满足。要继续这样下去。