第八章
异常
异常就是控制流中的突变,用来相应处理器状态中的某些变化。
状态变化称为事件。
异常表:
当处理器检测到有事件发生时,它就会通过异常表(一种跳转表),进行一个间接过程调用(异常),到异常处理程序。
根据引起异常的类型,会发生一下三种情况的一种:
1)处理程序将控制返回给当前指令I_curr,即当事件发生时正在执行下一条指令。
2)处理程序将控制返回给I_next,即如果没有发生异常将会执行的下一条指令。
3)处理程序终止被中断的程序。
异常处理
跳转表的分配和初始化是在系统启动时进行的,使得条目k包含异常k的处理程序的地址。
异常号是异常表中的索引,异常表的起始地址放在一个叫做异常表基址寄存器的特殊CPU寄存器。
异常类的一些与过程调用的重要不同之处:
- 过程调用时,在跳转到处理程序之前,处理器将返回地址压入栈中。然而,根据异常类型,返回地址是当前指令或者下一条指令(如果事件不发生,将会在当前指令后执行的指令)。
- 处理器也会把一些额外的处理器状态压到栈中,在处理程序返回时,重新开始被中断的程序会需要这些状态。
- 如果控制从一个用户程序转移到内核,那么所有的这些项目都被压到内核栈中,而不是压倒用户栈中。
- 异常处理程序运行在内核模式下,这意味着它对所有的系统资源都有完全的访问权限。
在处理完事件之后,可通过“从中断返回”指令,可选地返回到被中断的程序(向处理器的控制和数据寄存器弹回适当状态)。
异常的类别
异常分四类:
中断 陷阱 故障 终止
1.中断
异步发生,来自处理器外部的I/O设备信号的结果。
2.陷阱和系统调用
有意的异常,是执行一条指令的结果。
重要用途:在用户程序和内核之间提供一个像过程一样的接口,叫系统调用。
内核服务受控访问,提供指令syscall n。
syscall指令
导致一个异常处理程序的陷阱,这个处理程序对参数解码,并调用适当的内核程序。
系统调用和函数调用的不同
普通函数调用:运行在用户模式中,在用户模式中先置零函数可以执行的指令的类型,而且它们只能访问与调用函数相同的栈。
系统调用:运行在内核模式中,内核模式允许系统调用执行指令,并访问定义在内核中的栈。
3.故障
故障由错误情况引起,可能被故障处理程序修正。处理器将控制转移给故障处理程序。
如果错误能够被处理程序修正,它就将控制返回到引起故障的指令。
如果不能被修正,处理程序返回到内核中的abort例程,abort例程会终止引起故障的应用程序。
4.终止
终止是不可恢复的致命错误造成的结果。
从不将控制返回给应用程序,处理程序将控制返回给一个abort例程,该例程将会终止这个应用程序。
Linux/IA32系统中的异常
故障和终止
除法错误——应用试图除0。/结果对于目标数太大。
一般保护故障——程序引用了一个未定义的虚拟存储器区域。/程序试图写一个只读的文本段。
缺页——会重新执行产生故障的指令的一个异常示列。
机器检查——在导致故障的指令执行中检测到致命的硬件错误。
系统调用
Linux提供上百种系统调用,来源:/usr/include/sys/syscall.h
在IA32系统上,系统调用是通过一条称为int n的陷阱指令来提供的。
在c程序用syacall函数可以直接调用任何系统调用。(虽然实际没什么必要这样做)
所有到Linux系统调用的参数都是通过寄存器而不是栈传递的。
进程
进程
一个执行中的程序的实例。
进程提供的关键抽象:
- 一个独立的逻辑控制流,提供我们的程序独占地使用处理器的假象。
- 一个私有的地址空间,提供我们的程序独占地使用存储器系统的假象。
逻辑控制流
单步执行程序可看到程序计数器(PC)唯一对应于包含在程序的可执行目标文件中的指令,或者是包含在运行动态链接到程序的共享对象中的指令。这就是逻辑控制流了。
并发流
一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发地运行。
一个进程和其他进程轮流运行的概念称为多任务。
一个进程执行它的控制流的一部分的每一个时间段称为时间片。
并行流
如果两个流并发地运行在不同的处理器核或者计算机上,称为并行流。
并行流是并发流的真子集。
用户模式和内核模式
设置模式位,进程就运行在内核模式中。一个运行在内核模式的进程可以执行任何指令,可以访问任何系统中任何存储器位置。
没有设置模式位,进程就运行在用户模式中。此时进程不允许执行特权指令,也不允许用户模式中的进程直接引用地址空间中的内核区代码和数据。(必须通过系统调用接口间接地访问内核代码和数据)
Linux提供/proc文件系统,将许多内核数据结构的内容输出为一个用户程序可以读的文本文件的层次结构,允许用户模式进程访问内核数据结构的内容。
上下文切换
操作系统使用上下文切换实现多任务:
1)保存当前进程的上下文。
2)恢复某个先前被抢占的进程被保存的上下文。
3)将控制传递给这个新恢复的进程。
包装函数定义在一个叫做csapp.c的文件中,它们的原型定义在一个叫做csapp.h的头文件中。
进程控制
getpid函数 返回调用进程PID
getppid函数 返回父进程的PID(创建调用进程的进程)
#include<sys/tupes.h>
#inlcude<unistd.h>
pid_t getpid(void);
pid_t getppid(void);
//返回调用者/其父进程的PID
进程总是处于下面三种状态之一:
-
运行
-
停止
当收到SIGSTOP,SIGTSTP,SIDTTIN,SIGTTOU时,进程停止。
保持停止直到收到SIGCONT信号 -
终止
进程会因为三种原因终止:
1)收到一个信号,该信号默认为终止进程。
2)从主程序返回
3)调exit函数
exit函数 以status退出状态来终止进程。(或从主程序返回一个整数值来退出状态)
#include<stdlib.h>
void exit(int status);
//无返回值
fork函数 父进程创建一个新的运行子进程
#include<sys/types.h>
#inlcude<unistd.h>
pid_t fork(void);
//返回:子进程返回0,父进程返回子进程的PID,如果出错返回-1
当父进程调用fork时,子进程可以读写父进程中打开的任何文件。
父子进程最大的不同就是有不同的PID。
- fork进程只被调用一次,却会返回两次。
- 并发执行
- 相同但是独立的地址空间
- 共享文件
回收:内核将子进程的终止状态传递给父进程,然后抛弃进程。
僵死进程:一个终止但还未被回收的进程。
waitpid函数等待子进程终止或停止。
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid, int *status, int options);
//返回:如果成功,则为子进程的PID如果WHOHANG,则为0,如果其它错误,则为-1
默认地(options=0),waitpid挂起调用进程的执行直到它的等待集合中的一个子进程终止。
1.判定等待集合的成员
等待集合中的成员是由参数pid来确定的。
- 如果pid>0,那么等待集合是一个单独的子进程。进程ID=pid;
- pid=-1,等待集合是由父进程所有子进程组成的。
2.修改默认行为
通过options设置为常量WNOHANG和WUNTRACED的各种组合,修改默认行为:
- WNOHANG
如果等待集合中的任何子进程都还没有终止,
返回:0
默认行为:挂起调用进程,直到有子进程终止。
- WUNTRACED
挂起调用进程的执行,直到等待集合中的一个进程变成已终止或被停止。
返回:已停止或被停止子进程的PID
默认行为:只返回已终止的子进程。
- WNOHANG|WUNTRACED
立即返回
返回:等待集合中没有任何子进程被停止或已终止,返回0
返回值等于那个被停止的子进程的PID。
3.检查已回收子进程的退出状态
在wait.h头文件中定义了解释status参数的几个宏:
- WIFEXITED:如果子进程通过调用exit或一个返回正常终止,就返回真。
- WEXITSTATUS:返回一个正常终止的子进程的退出状态。只有在WIFEXITED返回为真时,才会定义这个状态。
- WIFSIGNALED:如果子进程是因为一个未被捕获的信号终止的,那么返回真。
- WTERMSIG:返回导致子进程终止的信号的编号。只有在WIFSIGNALED返回为真时才定义这个状态。
- WIFSTOPPED:如果引起返回的子进程当前是被停止的,那么返回真。
- WSTOPSIG:返回引起子进程停止的信号的数量。只有在WIFSTOPPED返回为真时才定义这个状态
4.错误条件
如果调用进程没有子进程,那么waitpid返回-1,并且设置errno为ECHILD。
如果waitpid被一个信号中断,那么他返回-1,并且设置errno为EINTR。
5.wait函数
#include <sys/types.h>
#include <sys/wait.h>
pid_t wait(int *status);//返回:如果成功返回子进程pid,如果出错,则为-1
调用wait(&status)等价于调用waitpid(-1,&status,0).
让进程休眠
sleep函数
将一个进程挂起一段指定的时间。
#include <unistd.h>
unsigned int sleep(unsigned int secs); //返回:还需要休眠的秒数。
如果请求时间到了,sleep返回0.
pause函数
让调用函数休眠,直到该进程收到一个信号
#include <unistd.h>
int pause(void);
//总是返回-1
加载并运行程序
execve函数
在当前进程的上下文中加载并运行一个新程序。
#include <unistd.h>
int execve(const char *filename, const char *argv[],
const char *envp[]);
//如果成功,则不返回,错误则返回-1
argv变量指向一个以null结尾的指针数组,其中每个指针都指向一个参数串。
envp变量指向一个以null结尾的指针数组,其中每个指针指向一个环节变量串,其中每个串都是形如“NAME=VALUE”的名字-值对。
Unix提供的操作环境数组的函数:
getnev函数
#include <stdlib.h>
char *getenv(const char *name);
//返回:若存在则为指向name的指针,若无匹配,则为null
在环境数组中搜寻字符串"name=value",如果找到了就返回一个指向value的指针,否则返回null。
setenv函数
unsetenv函数
#include <stdlib.h>
int setenv(const char *name, const char *newvalue, int overwrite);
//若成功返回0,若错误则返回-1
void unsetenv(const char *name);
//返回值:无
如果环境数组包含“name=oldvalue”的字符串,那么unsetenv会删除它,而setenv会用nevalue代替oldvaule,但只有overwrite非零才会这样。如果name不存在,那么setenv就把“name=newvalue”添加到数组中。
信号
得到信号列表:
man 7 signal
信号术语
传送一个信号到目的进程是由两个不同的步骤组成的。
-
发送信号。
发送信号的原因:
1)内核检测到一个系统事件
2)一个进程调用了kill函数,显式要求内核发送一个信号給目的进程。一个进程可以发送信号給它自己
-
接收信号
进程可以忽略终止或者通过执行一个称为信号处理程序的用户层函数捕获这个信号。
待处理信号:一只只发出没有被接收的信号。
在任何时刻,一种类型最多只会有一个待处理信号
一个待处理信号最多只能被接收一次。
发送信号
1.进程组:
每个进程都只属于一个进程组,进程组是由一个正整数进程组ID来标识的。
getpgrp函数
#include<unistd.h>
pid_t getgrp(void);
//返回调用进程的进程组ID
子父进程属于一个进程组。
setpid函数
通过使用setpid函数来改变自己的进程组
#include<unistd.h>
int setpid(pid_t,pid_t pgid);
//返回:若成功则为0,若错误则为-1
将pid的进程组改为pgid。如果pid为0,就使用当前进程的pid;如果pgid是0,那么就用pid指定的进程PID作为进程组ID。
2.用/bin/kill程序发送信号
使用完整路径/bin/kill.
例:unix> /bin/kill -9 15213
发送信号9(SIGKILL)给进程15213
3.从键盘发送信号
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int sig); //返回:如果成功则为0,若错误则为-1
键入ctrl-c会导致内核向每个前台进程组中的成员发送一个SIGNT信号。ctrl-z和SIGTSTP信号也是类似。
4.用kill函数发送信号
进程通过调用kill函数发送信号给其他进程(包括它们自己)。
如果pid大于0,那么kill函数发送信号sig给进程pid。如果pid小于0,那么kill发送sig给进程组abs(pid)中的每个进程。
5.用alarm函数发送信号
alarm函数
进程通过alarm函数向自己发送SIGALRM信号。
#inlcude<unistd.h>
unsigned int alarm(unsigned int secs);
//返回:前一次闹钟剩余的秒数,若以前没有设定闹钟,则返回0.
alarm函数安排内核在secs秒内发送一个SIGALRM信号给调用进程。
接收信号
进程p待处理信号集合为空时,内核将控制传递给p的逻辑控制流中的下一条指令I_next。
不为空,以信号k触发某种行为,然后再给I_next.
信号类型的默认行为:
- 进程终止
- 进程终止并转储存储器。
- 进程停止直到被SIGCONNT信号重启。
- 进程忽略该信号
SIGSTOP和SIGKILL的默认行为不能被修改。
signal函数
#include<signal.h>
typedef void (*sighandler_t)(int)
sighandler_t signal(int signum,sighandler_t handler)
//返回:若成功则为指向前次处理程序的指针。若出错则为SIG_ERR(不设置errno)。
-
handler是SIG_IGN,那么忽略类型为signum的信号。
-
handler是SIG_DFL,类型为signum的信号行为恢复为默认行为。
-
否则,handler就是用户定义的函数地址,这个函数称为信号处理程序。
设置信号处理程序:把处理程序的地址传递到signal函数从而改变默认行为。
捕获信号:调用信号处理程序。
处理信号:执行信号处理程序称为~。
处理程序执行return语句,控制传递回控制流中的进程被信号接收中断位置处指令。
信号处理问题
当一个程序要捕获多个信号时,一些细微的问题就产生了。
- 待处理信号被阻塞。
- 预处理信号不会排队等待。
- 系统调用可以被中断。
不可以用信号来对其他进程中发生的事件计数。
可移植的信号处理
sigaction函数
允许Linux和Solaris这样与Posix兼容的系统上的用户,明确指定他们想要的信号处理定义。(不常用)
显式地阻塞和取消阻塞信号
sigprocmask函数
显式地阻塞和取消阻塞选择信号。
非本地跳转
将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用-返回序列。非本地跳转是通过setjmp和龙jmp函数来提供的。
setjmp函数在env缓冲区中保存当前调用环境(包括程序计数器,栈指针和通用目的寄存器),供longjmp使用,并返回0.
longjmp函数从env缓冲区中恢复调用环境,触发一个最近一次初始化env的setjmp调用的返回。然后setjmp返回,并带有非零的返回值retval。
操作进程的工具
STRACE:打印一个正在运行的程序和它的子进程调用的每一系统用的轨迹。
PS:列出当前系统中的进程(包括僵尸进程)。
TOP:打印出关于当前进程资源使用的信息。
PMAP:显示进程的存储器映射。
/proc:一个虚拟文件系统,以ASCII文本格式输出大量内核数据结构的内容。
小结
异常控制流(ECF)发生在计算机系统的各个层次,是计算机系统中提供并发地基本机制。
陷阱就像是用来实现向应用提供到操作系统代码的受控的入口点的系统调用的函数调用。
用时:大约11小时
这次看书比较细致,觉得挺有成就感的,异常控制流只是一种计算机提供的并发地基本机制,是一种处理方式,没有特别难理解的部分,主要是对各个函数的应用~
参考资料:深入理解计算机系统