学习目的:
1. 了解异常及其种类
2. 理解进程和并发的概念
3. 掌握进程创建和控制的系统调用及函数使用
4. 理解数组指针、指针数组、函数指针、指针函数的区别
5. 理解信号机制:kill,alarm,signal,sigaction
6. 掌握管道和I/O重定向:pipe, dup, dup2
一、 异常
(一)异常处理
异常是异常控制流的一种形式,由硬件和操作系统实现的
异常就是控制流中的突变,用来响应处理器状态中的某些变化
产生:处理器中的变化(事件)触发从应用程序到异常处理程序的突发的控制转移,也就是异常;
类型:被零除,缺页,存储器访问违例,断点,算术溢出;系统调用,来着外部I/O设备的信号
处理:在任何情况下,当处理器检测到有事件发生时,它就会通过一张叫做异常表的跳转表进行一个间接过程调用,
到一个专门处理这类时间的操作系统子程序(异常处理程序);
结果:当 exception handler处理结束之后,会有三种结果
1.处理程序将控制返回给事件发生的时候正在执行的指令;
2.处理程序将控制返回给如果没有发生异常将会执行的下一条指令;
3.处理程序终止被终端的程序
【注:异常表,其条目k中包含着异常k的处理程序的地址。其中,异常表的起始地址放在异常表基址寄存器中,而异常号是异常表中的索引,相当于偏移地址】
(二)异常的类别
(1)中断
中断是异步发生的,是来自处理器外部的I/O设备的信号的结果.硬件中断的异常处理程序通常称为中断处理程序
下图概述了一个中断的处理:
(2)陷阱和系统调用
陷阱是有意的异常,是执行一条指令的结果.陷阱最重要的用途是在用户程序和内核之间
提供一个像过程一样的接口,叫做系统调用
请求服务:读文件(read),创建一个新的进程(fork),加载一个新的程序(execve),终止当前进程(exit)
(3) 故障
故障由错误情况引起,他可能能够被故障处理程序修正
一个经典的故障事例是缺页异常,当指令引用一个虚拟地址,而与该地址相对应的物理页面不在存储器中,
因此必须从磁盘中取出时,就会发生故障
(4) 终止
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误
(三)Linux/IA32系统中的异常
(1) Linux/IA32故障和终止
除法错误,一般保护故障,缺页,机器检查
(2) Linux/IA32系统调用
Linux提供上百种系统调用,当应用程序想要请求内核服务时可以使用,,包括读文件,写文件,创建一个新的进程
二、进程
进程提供给程序的关键抽象:
(1)一个独立的逻辑控制流,它提供一个假象,好象程序独占的使用处理器
(2)一个私有的地址空间,它提供一个假象,好象程序独占的使用存储器系统
(一)逻辑控制流
如果想用调试器单步执行程序我们会看到一系列的程序计数器(PC)的值,这些值唯一的对应于包含在程序的
可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令
这个PC值得序列叫做逻辑控制流,简称逻辑流
(二)并发流
1.一个逻辑流的执行在时间上与另一个流重叠:并发流.并发的运行
注:多个流并发的执行的一般现象称为并发
2.一个进程和其他进程轮流运行的概念称为多任务
3.一个进程执行他的控制流的一部分的每一时间段叫作时间片,多任务也叫做时间分片
并行流是并发流的一个真子集,如果两个流并发的运行在不同的处理器核或者计算机上,那么我们称他们为并行流
(并行的运行,并行的执行)
(三)私有地址空间
n位地址的机器:地址空间是2^n个可能地址的集合
一个进程为每个程序提供他自己的私有地址空间
私有:和这个空间中某个地址相关联的那个存储器字节是不能被其他进程读或写的
(四)用户模式和内核模式
(五)上下文切换
操作系统内核使用一种称为上下文切换的较高层形式的异常控制流来实现多任务
三、系统调用错误处理
重要知识点:
fork函数
-
调用一次,返回两次:fork函数被父进程调用一次,但返回两次:一次返回到父进程,一次返回到新创建的子进程。
-
并发执行:父进程和子进程是并发运行的独立进程。
-
相同但是独立的地址空间。
-
共享文件:子进程继承了父进程所有打开的文件。
四、进程控制
(一)获取进程ID
getpid函数
(二)创建和终止进程
从程序员的角度,可以认为进程总是处于下面三种状态之一:
- 运行
- 停止
- 终止:三种原因:收到一个信号,该信号的默认行为是终止进程;从主程序返回;调用exit函数
(三)回收子进程
1.一个终止了但还未被回收的进程称为僵死进程
2.一个进程由某种原因终止时,内核并不是立即把它从系统中清除。
进程会被保持在一种已终止的状态中,直到被它的父进程回收。
3.当父进程回收已终止的进程时,内核将子进程的退出状态传递给父进程,
然后抛弃已终止的进程,从此时开始,该进程就不存在了。
如果父进程没有回收它的僵死子进程就终止了, 那么内核就会安排 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。
作用:等待一个进程的子进程终止或停止。
1.当默认options=0时,waitpid挂起调用进程的执行,直到它的等待集合中的一个子进程终止。如果等待集合中的一个进程在刚调用的时刻就已经终止了,那么waitpid就立即返回。
2.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返回为真时,才定义这个状态。
(4)错误条件
如果调用进程没有子进程,那么waitpid返回-1,并且设置errno为ECHILD。如果waitpid函数被一个信号中断,那么它返回-1,并设置errno为EINTR。
(5)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.进程组:每个进程都只属于一个进程组
-
每个进程都只属于一个进程组,进程组是由一个正整数进程组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。
2.用/bin/kill程序发送信号
3.从键盘发送信号
4.用kill函数发送信号
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)中的每个进程。
5.用alarm函数发送信号
alarm函数安排内核在secs秒内发送一个SIGALRM信号给调用进程。
#include<unistd.h>
unsigned int alarm(unsigned int secs);
返回:前一次闹钟剩余的秒数,若以前没有设定闹钟,则为0。
(三)接受信号
每个信号类型都有一个预定义的默认行为
- 进程终止
- 进程终止并转储存储器
- 进程停止直到被SIGCONT信号重启
- 进程忽略该信号
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函数被调用一次,但从不返回。
七、操作进程的工具
STRACE:打印一个正在运行的进程和它的子进程调用的每个系统调用的轨迹。
PS:列出当前系统中的进程。
TOP:打印出关于当前进程资源使用的信息。
PMAP:显示进程的存储器映射。
参考资料
《深入理解计算机系统》第8章异常控制流