第十一周学习总结
第八章 异常控制流
一般而言,我们把这些突变称为异常控制流ECF。
- ECF是操作系统用来实现I/O、进程和虚拟储存器的基本机制。
- 应用程序通过使用一个叫做陷阱或者系统调用的ECF形式,向操作系统请求服务。
- 操作系统为应用程序提供了强大的ECF机制。
- ECF是计算机系统中实现并发的基本机制。
- 像C++和Java这样的语言通过try、catch以及throw语句来提供软件异常机制。
8.1 异常
异常是异常控制流的一种形式,它是一部分由硬件实现的,一部分由操作系统实现的。
异常就是控制流中的突变。
状态变化称为事件,事件可能和当前指令的执行直接相关,也可能和当前指令的执行没有关系。
当异常处理程序完成处理后,根据引起异常的事件的类型,会发生以下三种情况中的一种:
- (1)处理程序将控制返回给当前指令,即当事件发生时正在执行的指令。
- (2)处理程序将控制返回给下一指令,即如果没有发生异常将会执行的下一条指令。
- (3)处理程序终止被终端的程序。
8.1.1异常处理
异常号:分配了一个唯一的非负整数的。
操作系统分配和初始化一张称为异常表的跳转表,使得条目k包含异常k的处理程序的地址。
异常类似于过程调用,区别:
- 根据异常的类型,返回地址要么是当前指令,要么是下一条指令。
- 处理器也把一些额外的处理器状态压到栈里,在处理程序返回时,重新开始被中断的程序会需要这些状态。
- 如果控制从一个用户程序转移到内核,那么所有这些项目都被压到内核栈中,而不是压到用户栈中。
- 异常处理程序运行在内核模式下,对所有的系统资源都有完全的访问权限。
8.1.2异常的类别
1.中断
异步发生 来自处理器外部的I/O设备的信号的结果 返回给下一指令
2.陷阱和系统调用
陷阱是有意的异常,是执行一条指令的结果。陷阱最重要的用途是系统调用。
3.故障
故障由错误情况引起,它可能能够被故障处理程序修正。将控制返回到引起故障的指令,终止程序。一个经典的故障示例是缺页异常。
4.终止
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误。
8.1.3Linux/IA32系统中的异常
1.Linux/IA32故障和终止
- 除法错误(异常0)选择终止程序。
- 一般保护故障(异常13)终止程序。
- 缺页(异常14)返回。
- 机器检查(异常18)终止程序。
2.Linux/IA32系统调用
每个系统调用都有一个唯一的整数号,对应于一个到内核中跳转表的偏移量。
系统调用是通过一条称为int n的陷阱指令来提供的,其中n可能是IA32异常表中256个条目中任何一个的索引。
所有的到Linux系统调用的参数都是通过通用寄存器而不是栈传递的。寄存器%eax包含系统调用号,寄存器%ebx、%ecx、%edx、%esi、%edi和%ebp包含最多六个任意的参数。栈指针%esp不能使用。
8.2 进程
进程的经典定义就是一个执行中的程序的实例。系统中的每个程序都是运行在某个进程的上下文中的。上下文是有程序正确运行所需的状态组成的。
关键抽象:
- 一个独立的逻辑控制流,好像程序独占地是使用处理器。
- 一个私有的地址空间,好像程序地使用存储器系统。
8.2.1逻辑控制流
一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或者是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称逻辑流。
关键点在于进程是轮流使用处理器的。每个进程执行它的流的一部分,然后被抢占,然后轮到其他进程,就像是在独占地使用处理器。
8.2.2并发流
一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发并发地运行。
多个流并发地执行的一般现象称为并发。一个进程和其他进程轮流运行的概念称为多任务。一个进程执行它的控制流的一部分的每一个时间段叫做时间片。
如果两个流并发地运行在不同的处理器核或者计算机上,那么我们称它们为并发流,它们并行地运行,且并行地执行。
8.2.3私有地址空间
进程也为每个程序提供一种假象,好像它独占地使用系统空间。一般而言,和这个空间中某个地址相关联的那个存储器字节是不能被其他进城读或者写的。
8.2.4用户模式和内核模式
处理器通常是用某个控制寄存器中的一个模式位来提供这种功能的,该寄存器描述了进程当前享有的特权。当设置了模式位时,进程就运行在内核模式中。
进程从用户模式变成内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。
Linux提供了一种聪明的机制,叫做/proc文件系统,它允许用户模式进程访问内核数据结构的内容。
8.2.5上下文切换
操作系统内核使用一种称为上下文切换的较高形式的异常控制流来实现多任务。上下文切换机制是建立在较低层异常机制之上的。
上下文就是内核重新启动一个被抢占的进程所需的状态。由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,如页表、进程表、文件表。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。这种决定就叫做调度,是由内核中称为调度器的代码处理的。
上下文切换:(1)保存当前进程的上下文,(2)恢复某个先前被抢占的进程被保存的上下文,(3)将控制传递给这个新恢复的进程。
当内核代表用户执行系统调用时,可能会发生上下文切换,中断也可能引发上下文切换。
8.3系统调用错误处理
通过使用错误处理包装函数,我们可以更进一步地简化我们的代码。
8.4进程控制
8.4.1获取进程ID
每个进程都有一个唯一的正数(非零)进程ID(PID)。
#include<sys/types.h>
#include<unistd.h>
pid_t getpid(void);
pid_t getppid(void);
返回:调用者或其父进程的PID
8.4.2创建和终止进程
运行
停止
终止:1)收到一个信号,该信号的默认行为是终止进程;2)从主程序返回;3)调用exit函数。
#include<stdlib.h>
Void exit(int status);
该函数无返回值。
父进程通过调用fork函数创建一个新的运行子进程。只被调用一次,却会返回两次。在父进程中,fork返回子进程的PID。在子进程中,fork返回0。
- 调用一次,返回两次。
- 并发执行。内核能够以任意方式交替执行它们的逻辑控制流中的指令。
- 相同的但是独立的地址空间。
- 共享文件。子进程继承了父进程所有的打开文件。
8.4.3回收子进程
一个终止了但还未被回收的进程称为僵死进程。
如果父进程没有回收它的僵死进程就终止了,那么内核就会安排init进程来回收它们。Init进程的PID为1。
一个进程可以通过调用waitpid函数来等待它的子进程终止或者停止。
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);
1.判断等待集合的成员
如果pid>0,那么等待的集合就是一个单独的子进程,它的进程ID等于pid。
如果pid=-1,那么等待集合就是由父进程所有的子进程组成的。
2.修改默认行为
通过将optioins设置为常量WNOHANG和WUNTRACED的各种组合。
WNOHANG:如果等待集合中的任何子进程都还没有终止,那么就立即返回(返回值为0)。
WUNTRACED:挂起调用进程的执行,直到等待集合中的一个进程变成已终止或者被停止的。
WNOHANG|WUNTRACED:立即返回,如果等待集合中没有任何子进程被停止或已终止,那么返回值为0,或者返回值等于那个被停止或者已终止的子进程的PID。
3.检查已回收子进程的退出状态
wait.h头文件定义了解释status参数的几个宏:
WIFEXITED:如果子进程通过调用exit或者一个返回(return)正常终止,就返回真。
WEXITSTATUS:返回一个正常终止的子进程的退出状态。只有在WIFEXITED返回为真时,才会定义这个状态。
WIFSIGNALED:如果子进程是因为一个未被捕获的信号终止的,那么就返回真。
WTERMSIG:返回导致子进程终止的信号的编号。只有在WIFSIGNALED返回为真时,才定义这个状态。
WIFSTOPPED:如果引起返回的子进程当前是被停止的,那么就返回真。
WSTOPSIG:返回引起子进程停止的信号的数量。只有在WIFSTOPPED返回为真时,才定义这个状态。
4.错误条件
如果调用进程没有子进程,那么waitpid返回-1,并且设置errno为ECHILD。如果waitpid函数被一个信号中断,那么它返回-1,并设置errno为EINTR
5.wait函数
wait函数是waitpid函数的简单版本:
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *status);
调用wait(&status)等价于调用waitpid(-1,&status,0)。
8.4.4让进程休眠
sleep函数将一个进程挂起一段指定的时间。
#include<unistd.h>
unsigned int sleep(unsigned int secs);
pause函数,让调用函数休眠,直到该进程收到一个信号。
#include<unisted.h>
int pause(void);
8.4.5加载并运行程序
execve函数在当前进程的上下文中加载并运行一个新程序。
#include<unistd.h>
int execve(const char *filename,const char *argv[],
const char *envp[])
可执行文件filename,带参数列表argv和环境变量列表envp。execve调用一次从不返回。
#include<stdlib.h>
char *getenv(const char *name);
返回:若存在则为指向name的指针,若无匹配的,则为NULL。
getenv函数在环境数组中搜索字符串“name=value”。如果找到了,它就返回一个指向value的指针,否则它就返回NULL。
#include<stdlib.h>
int setenv(const char *name,const char *newvalue,int overwrite);
void unsetenv(const char *name);
如果环境数组包含一个形如“name=oldvalue”的字符串,那么unsetenv会删除它,而setenv会用newvalue代替oldvalue,但是只有在overwrite非零时才会这样。如果name不存在,那么setenv就把“name=value”添加到数组中。
8.5 信号
一种更高层的软件形式的异常,称为Unix信号,它允许进程中断其他进程。
8.5.1 信号术语
传送一个信号到目的进程是由两个不同步骤组成的:
发送信号。1)内核检测到一个系统事件,2)一个进程调用了kill函数,显式地要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。
接收信号。一个只发出而没有被接收的信号叫做待处理信号。在任何时刻,一种类型只会有一个待处理信号。一个进程可以有选择性地阻塞接收某种信号。一个待处理信号最多只能被接收一次。
8.5.2发送信号
1.进程组
每个进程都只属于一个进程组,进程组是由一个正整数进程组ID来标识的。
一个子进程和它的父进程同属于一个进程组。
2.用/bin/kill程序发送信号
/bin/kill程序可以向另外的进程发送任意的信号。如 unix> /bin/kill -n m
3.从键盘发送信号
4.用kill函数发送信号
进程通过调用kill函数发送信号给其他进程。
5.用alarm函数发送信号
进程可以通过调用alarm函数向它自己发送SIGALRM信号。
#include<unistd.h>
unsigned int alarm(unsigned int secs);
返回:前一次闹钟剩余的秒数,若以前没有设定闹钟,则为0。
alarm函数安排内核在secs秒内发送一个SIGNALRM信号给调用进程。
8.5.3接收信号
当内核从一个异常处理程序返回,准备将控制传递给进程p时,它会检查进程p的未被阻塞的待处理信号的集合。如果这个集合为空(通常情况下),那么内核将控制传递到p的逻辑控制流中的下一条指令。
8.5.4信号处理问题
- 待处理信号被阻塞。
- 待处理信号不会排队等待。
- 系统调用可以被中断。
由此得到的重要教训是,不可以用信号来对其他进程中发生的事件计数。
8.5.5可移植的信号处理
不同系统之间,信号处理语义的差异(比如一个被中断的慢速系统调用是重启还是永久放弃)是Unix信号处理的一个缺陷。
sigaction函数:
#include<signal.h>
int sigaction(int signum,struct sigaction *act
struct sigaction *oldact);
8.6非本地跳转
C语言提供了一种用户级异常控制流形式,称为非本地跳转。通过setjmp和longjmp函数来提供。
stejmp函数在env缓冲区中保存当前调用环境,一共后面longjmp使用,并返回0。调用环境包括程序计数器、栈指针和通用目的寄存器。
longjmp函数从env缓冲区中恢复调用环境,然后触发一个从最近一次初始化env的setjmp调用的返回。然后setjmp返回,并带有非零的返回值retval。
setjmp函数只被调用一次,但返回多次。longjmp函数被调用一次,但从不返回。
8.7操作进程的工具
- STRACE:打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹。
- PS:列出当前系统中的进程(包括僵死进程)。
- TOP:打印出关于当前进程资源使用的信息。
- PMAP:显示出进程的存储器映射。