异常控制流学习记录
重要知识点
控制流
从给处理器加电开始,直到断电为止,程序计数器假设一个值的序列
a0,a1,...,an-1
其中,每个ak是某个相应指令Ik的地址。每次从ak到ak+1的过度称为控制转移。这样的控制转移序列叫做处理器的控制流。
异常控制流
现代系统通过使控制流发生突变来对一些情况做出反应。这些突变被称为异常控制流。
异常
控制流中的突变,用来响应处理器状态中的某些变化。它是异常控制流的一种形式,它一部分由硬件实现,一部分由操作系统实现。
异常的类别
-
中断:来自I/O设备信号的结果,是异步发生的,总是返回到下一条指令。
-
陷阱:是有意的异常,同步发生的,总是返回到下一条指令。
- 重要用途:在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
-
故障:潜在可恢复异常,同步发生,可能返回到当前指令。
-
终止:不可恢复异常,同步发生,不会返回。
IA32系统中的异常示例
异常号 | 描述 | 异常类别 |
---|---|---|
0 | 除法异常 | 故障 |
13 | 一般保护故障 | 故障 |
14 | 缺页 | 故障 |
18 | 机器检查 | 终止 |
32~127 | 操作系统定义的异常 | 中断 |
128(0x80) | 系统调用 | 陷阱 |
129~255 | 操作系统定义的异常 | 中断或陷阱 |
Liunx/IA32系统调用
编号 | 名字 | 描述 |
---|---|---|
1 | exit | 结束进程 |
2 | fork | 创建新进程 |
3 | read | 读文件 |
4 | write | 写文件 |
5 | open | 打开文件 |
6 | close | 关闭文件 |
7 | waitpid | 等待子进程结束 |
11 | execve | 加载和运行程序 |
19 | lseek | 定位到文件偏移处 |
20 | getpid | 获得进程ID |
27 | alarm | 设置传送进程信号的警告时钟 |
29 | pause | 挂起进程直到信号到达 |
37 | kill | 发送信号到另一个进程 |
48 | signal | 安装一个信号处理程序 |
63 | dup2 | 复制文件描述符 |
64 | getppid | 获得父进程ID |
65 | getpgrp | 获得进程组 |
67 | sigaction | 安装可以移植的信号处理程序 |
90 | mmap | 将存储器页映射到文件 |
106 | stat | 获得有关文件的信息 |
进程
-
经典定义:一个执行中的程序实例。
-
进程具有四个要素:一段供该进程运行的程序;进程专用的系统堆栈空间;进程控制块(Linux中的具体实现为task_struct结构);独立的存储空间。
- 进程缺少以上四要素中的一个条件时,被称为线程。
- linux中所有的进程都是由进程号为1的init进程衍生而来的。
-
进程给应用程序提供的抽象:
- 逻辑控制流:它提供给每个程序一个假象,好像它在独占地使用处理器。
- 私有地址空间:它提供给每一个程序一个假象,好像它是在独占地使用主存。
并发与多任务
- 并发流:一个逻辑流的执行在时间上与另一个流重叠。这两个流被称为并发地运行。
-
并发:多个流并发地执行的现象。
-
时间片:每个进程执行它的控制流的一部分的每一个时间段。
-
多任务:一个进程和其他进程轮流运行,又称为时间分片。
用户模式与特权模式
- 用户模式:在用户模式中的进程不允许执行特权命令,也不允许用户模式中的进程直接引用地址空间中内核区的代码和数据,任何这样的尝试都会导致致命的保护故障。反之,用户程序必须通过系统调用接口间接地访问内核代码和数据。
- 内核模式:运行在内核模式中的进程可以执行指令集中的任何指令,并且可以访问系统中任何存储器的位置。
上下文切换
上下文切换是操作系统内核用来实现多任务的较高层次的异常控制流。
-
切换机制如下:
- 保存当前进程的上下文。
- 恢复某个先前被抢占的进程被保存的上下文。
- 将控制传递给这个新恢复的进程。
获取进程ID
- getpid():返回调用进程ID。(返回值类型为pid_t在linux的中被定义为int)
- getppid():返回它的父进程的PID。(返回值类型为pid_t在linux的中被定义为int)
进程的状态
-
运行:进程在CPU上执行或是等待被执行且最终会被内核调度。
-
停止:进程的执行被挂起,且不会被调度。
- 停止原因:收到SIGSTOP、SIGTSTP、SIDTTIN或者SIGTTOU信号时,进程停止。(注:当收到SIGCONT信号时,进程再次开始运行。)
-
终止:进程永远地停止了。
-
终止原因:
1.收到一个信号,该信号的默认行为是终止进程。
2.从主程序返回。
3.调用exit函数。
-
fork函数
-
调用一次,返回两次:fork函数被父进程调用一次,但返回两次:一次返回到父进程,一次返回到新创建的子进程。
-
并发执行:父进程和子进程是并发运行的独立进程。
-
相同但是独立的地址空间。
-
共享文件:子进程继承了父进程所有打开的文件。
回收子进程
一个进程由某种原因终止时,内核并不是立即把它从系统中清除。进程会被保持在一种已终止的状态中,直到被它的父进程回收。当父进程回收已终止的进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程,从此时开始,该进程就不存在了。
- 僵死进程:一个终止了但还未被回收的进程。
- 如果父进程没有回收它的僵死子进程就终止了, 那么内核就会安排 init进程 来回收它们 init进程 的PID为1。
waitpid函数
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);
如果成功,返回子进程的PID,如果WNOHANG,则为0,如果其他错误,则为-1。
用于等待一个进程的子进程终止或停止。
-
当默认options=0时,waitpid挂起调用进程的执行,直到它的等待集合中的一个子进程终止。如果等待集合中的一个进程在刚调用的时刻就已经终止了,那么waitpid就立即返回。
- waitpid返回导致waitpid返回的已终止子进程的PID,并且将这个已终止的子进程从系统中去除。
函数参数作用:
1. 判定等待集合的成员
等待集合的成员由pid确定:
-
如果pid>0,那么等待集合就是一个独立的子进程,它的进程ID等于pid。
-
如果pid=-1,那么等待集合就是由父进程所有子进程组成的。
2 .修改默认行为
可以通过将options设置为常量WNOHANG和WUNTRACED的各种组合,修改默认行为:
-
WNOHANG:如果等待集合中的任何子进程都还没有终止,那么就立即返回(返回值为0)。
-
WUNTRACED:挂起调用进程的执行,直到等待集合中的一个进程变成已终止或者被停止。返回的PID为导致返回的已终止或被终止的子进程PID。
-
WNOHANG|WUNTRACED:立即返回,如果等待集合中没有任何子进程被停止或已终止,那么返回值为0,或者返回值等于那个被停止或者已终止的子进程的PID。
3 .检查已回收子进程的退出状态
如果status参数是非空的,那么waipid就会在status参数中放上关于导致返回的子进程的状态信息。wait.h头文件定义了解释status参数的几个宏:
-
WIFEXITED:如果子进程通过调用exit或者一个return正常终止。
-
WEXITSTATUS:返回一个正常终止的子进程的退出状态。只有在WIFEXITED返回为真时,才会定义这个状态。
-
WIFSGNALED:如果子进程是因为一个未被捕获的信号终止的,那么就返回真。
-
WTERMSIG:返回导致子进程终止的信号的编号。只有在WIFSGNALED返回为真时,才定义这个状态。
-
WIFSTOPPED:如果引起返回的子进程当前被停止的,那么就返回真。
-
WSTOPSIG:返回引起子进程停止的信号数量。只有在WIFSTOPPED返回为真时,才定义这个状态。
注:如果调用进程没有子进程,那么waitpid返回-1,并且设置errno为ECHILD。如果waitpid函数被一个信号中断,那么它返回-1,并设置errno为EINTR。
- wait函数
#include<sys/type.h>
#include<sys/wait.h>
pid_t wait(int *status);
返回:如果成功,则为子进程的PID,如果出错,则为-1
wait(&status)等价于调用waitpid(-1.&status,0)。
进程休眠
- sleep函数:将一个进程挂起一段指定的时间。
#include<unisd.h>
unsigned int sleep(unsigned int secs);
返回:还要休眠的秒数。
- pause函数:该函数让函数休眠,直到该进程收到一个信号。
#include<unistd.h>
int pause(void);
总是返回-1
execve函数
excve函数在当前进程的上下文中加载并运行一个新程序。
#include<unistd.h>
int execve(const char *filename, const char *argv[], const char *envp[]);
如果成功,则不返回,如果错误,则返回-1.
execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。
信号
Unix信号:一种更高层的软件形式的异常,它允许进程中断其他进程。
- 传送一个信号到目的进程是由两个不同步骤组成:
1 .发送信号:内核通过更新目的进程上下文中的某个状态,发送(递送)一个信号给目的的进程。发送信号的原因:1)内核检测到一个系统事件。2)一个进程调用了kill函数,显示地要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。
2 .当目的进程被内核强迫以某种方式对信号的发送做出反应时,目的进程就接收了信号。
进程组
-
每个进程都只属于一个进程组,进程组是由一个正整数进程组ID来标识的。
-
getpgrp函数:返回当前进程组的ID。
#include<unistd.h>
pid_t getpgrp(void);
返回:调用进程的进程组ID。 -
默认地,一个进程和它的父进程同属于一个进程组。
-
setpgid函数:改变自己或者其他进程的进程组。
#include<unistd.h>
int setpgid(pid_t pid,pid_t pgid);
返回:若成功则为0,若错误则为-1。
kill函数
#include<sys/types.h>
#include<signal.h>
int kill(pid_t pid,int sig);
返回:若成功则为0,若错误则为-1
进程通过调用kill函数发送信号给其他进程(包括它们自己)。
如果pid大于零,那么kill函数发送信号sig给进程pid。如果pid小于零,那么kill发送信号sig给进程组abs(pid)中的每个进程。
alarm函数
alarm函数安排内核在secs秒内发送一个SIGALRM信号给调用进程。
#include<unistd.h>
unsigned int alarm(unsigned int secs);
返回:前一次闹钟剩余的秒数,若以前没有设定闹钟,则为0。
signal函数
进程可以通过使用signal函数修改和信号相关联的默认行为,除了SIGSTOP和SIGKILL,它们的默认行为是不能被修改的。
#include<signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum, sighandler_t handler);
返回:若成功则为指向前次处理程序的指针,若出错则为SIG_ERR(不设置errno)
signal函数可以通过下列三种方法之一来改变和信号signum相关联的行为:
-
如果handler是SIG_IGN,那么忽略类型为signum的信号。
-
如果handler是SIG_DFL,那么类型为signum的信号行为恢复为默认行为。
-
否则,handler就是用户定义的函数的地址,这个函数称为 信号处理函数程序 ,只要进程接收一个类型为signum的信号就会调用这个程序。
- 设置信号处理程序:通过把处理程序的地址传递到signal函数从而改变默认行为。
- 捕获信号:调用信号处理程序。
- 处理信号:执行信号处理程序。
非本地跳转
非本地跳转:
C语言提供了一种用户级异常控制流形式,它将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用-返回序列。
其通过setjump和longjmp函数提供的。
#include<setjmp.h>
int setjmp(jmp_buf env);
int signsetjmp(sigjmp_buf env, int savesigs);
返回:setjmp 返回0, longjmp 返回非零
setjmp函数在env缓冲区中保存当前调用环境,以供后面longjmp使用。调用环境包括程序计数器、栈指针和通用目的寄存器。
#include<setjmp.h>
void longjmp(jmp_buf env, int retval);
void siglongjmp(sigjmp_buf env, int retval);
longjmp函数从env缓冲区中恢复调用环境,然后触发一个最近一次初始化env的setjmp调用的返回。然后setjmp返回, 并带有非零的返回值retval。
- setjmp函数被调用一次返回多次,longjmp函数被调用一次,但从不返回。
linux中操作进程的工具
STRACE:打印一个正在运行的进程和它的子进程调用的每个系统调用的轨迹。
PS:列出当前系统中的进程。
TOP:打印出关于当前进程资源使用的信息。
PMAP:显示进程的存储器映射。
参考资料
《深入理解计算机系统》第8章异常控制流。
进程控制视频8.1。