预计学习时间:9h
实际花费时间:11h
第八章 异常控制流
CEF:
1.ECF是操作系统用来实现I/O、进程和虚拟存器的基本机制
2.应用程序通过使用一个叫做陷阱或者系统调用的ECF形式,向操作系统请求服务
3.ECF是计算机系统中实现并发的基本机制4.软件异常机制——C++和Java有try,catch,和throw,C中非本地跳转是setjmp和longjmp
一、异常
异常是控制流中的突变,用来响应处理器状态中的某些变化。
出现异常的处理方式:
1.处理器检测到有异常发生
2.通过异常表,进行间接过程调用,到达异常处理程序
3.完成处理后:①返回给当前指令②返回给下一条指令③终止
1、异常处理
系统中可能的每种异常都被分配了唯一一个非负整数的异常号,异常表中的条目k中包含异常k的处理程序地址。异常表的起始地址存放在一个叫做异常表基址寄存器的特殊寄存器中。
异常类和过程调用的不同之处:
· 返回地址是当前地址或者下一条指令
· 处理器也会把额外的处理器状态压回栈中,在处理程序返回时,重新开始被中断的程序会需要这些状态
· 如果控制从一个用户程序转移到内核,那么所有项目都会被压到内核栈中而不是用户栈
· 异常处理程序运行在内核模式下,意味着他们对所有的系统资源拥有完全的访问权限。
2、异常的类别
异常可分为四类:中断、陷阱、故障、终止。
1. 中断
中断是异步发生的,是来自处理器外部的I/O设备的信号的结果,不是由任何一条指令造成的。
硬件中断的异常处理程序通常称为中断处理程序。
剩下的异常类型陷阱、故障、终止是同步发生的,是执行当前指令的结果。我们把这类指令叫做故障指令。
2.陷阱
陷阱是有意的异常,最重要的用途是在用户程序和内核之间提供一个向过程一样的接口,叫做系统调用。
为了允许内核服务的受控访问,使用“syscall n”指令,跳转到一个异常处理程序的陷阱,处理程序对参数解码并调用适当的内核程序。
3.故障
故障由错误情况引起,可能能够被故障处理程序修正。故障发生时,处理器将控制转移给故障处理程序,若能修正,则将控制返回到引起故障的指令,重新执行;若不能修正,处理程序返回abort例程,终止引起故障的应用程序。
4.终止
终止是不可恢复的致命错误造成的结果,通常是硬件错误。终止处理程序将控制直接返回给abort例程,直接终止该应用程序。
3、Linux/A32系统中的异常
Linux/A32故障和终止
· 除法错误(异常0):应用试图除以0,或者除法指令的结果对于目标操作数过大。
· 一般故障保护(异常13):通常因为一个程序引用一个未定义的虚拟存储区域,或者试图写一个只读文本段。
· 缺页(异常14):处理程序将磁盘上虚拟存储器相应页面映射到物理存储器的一个页面,然后重新开始执行这条指令。
· 机器检查(异常18):在导致故障的指令执行中检测到致命的硬件错误。
Linux/A32系统调用
· 每个系统调用都对应着唯一的整数号,对应于一个到内核中跳转表的偏移量。
· IA32系统调用是通过一条称为 int n 的陷阱指令提供的。
· C程序通过syscall函数可以直接调用任何系统调用。
· 所有Linux系统调用都是通过通用寄存器而不是栈传递的,%eax包含系统调用号,%ebx、%ecx、%edx、esi%、%edi和%ebp包含最多6个参数。栈指针%esp不能使用,因为当进入内核模式时,内核会覆盖它。
二、进程
进程,就是一个执行中的程序的实例,系统中的每个程序都是定义在运行在某个进程的上下文中的。异常是允许操作系统提供进程的概念所需要的基的本构造块。
进程提供给应用程序的关键抽象:
· 一个独立的逻辑控制流
· 一个私有的地址空间
1、逻辑控制流
PC的值唯一地对应于包含在程序可执行目标文件中的指令,或者是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流。
进程轮流使用处理器,每个进程执行流的一部分,然后被抢占(暂时挂起)。
2、并发流
一个逻辑流的执行在时间上与另一个流重叠,称为并发流。
一个进程和其他进程轮流运行的概念称为多任务,一个进程执行它的控制流的一部分时间称为时间片。
3、私有地址空间
一个进程为每个程序提供他自己的私有地址空间,一般而言,和这个空间中某个地址相关联的存储器字节不能被其他程序读或写。
地址空间底部是保留给用户程序的,顶部保留给保留给内核,用来存放内核在代表进程执行时的指令。
4、用户模式和内核模式
处理器通过某个控制寄存器中的一个模式位来提供这种功能。
· 内核模式:设置模式位,进程可以执行指令集中的任何指令,并且访问系统中的任何存储器位置。
· 用户模式:不设置模式位,进程不允许执行特权指令,也不允许直接引用地址空间中内核区的代码和数据。
初始模式是用户模式,进入内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。
linux提供了/proc文件系统,允许用户模式进程访问内核数据结构的内容。
5、上下文切换
上下文:内核重新启动一个被抢占的进程所需的状态。包括描绘地址空间的页表、包含有关当前进程信息的进程表,以及半酣进程已打开文件的信息的文件表。
调度:进程执行过程中,内核决定抢占当前进程并开始一个先前被抢占的进程,由内核中的调度器完成。
内核为每个进程维持一个上下文。并通过一种称为上下文切换的较高层形式的异常控制流来实现多任务上下文切换:
· 保存当前进程的上下文;
· 恢复某个先前被抢占的进程被保存的上下文;
· 将控制传递给新恢复的进程。
三、系统调用错误处理
Unix系统级函数遇到错误时,会典型地返回-1,并设置全局变量errno来表示出错内容。
通过使用错误处理包装函数,可以进一步简化代码。
四、进程控制
1、获取进程ID
每个进程都有一个唯一的正数进程ID(PID)
#include <sys/types.h>#include <unistd.h>
pid_t getpid(void); 返回调用进程的PID
pid_t getppid(void); 返回父进程的PID(创建调用进程的进程)
2、创建和终止进程
1.进程总是处于下面三种状态之一:
运行:在CPU上执行,或者等待被执行且最终会被内核调度。
停止:被挂起且不会被调度
终止:永远停止。原因:
(1)收到信号,默认行为为终止进程
(2)从主程序返回
(3)调用exit函数
2.创建进程
父进程通过调用fork函数来创建一个新的运行子进程。fork函数定义如下:
#include <sys/types.h>#include <unistd.h>
pid_t fork(void);
fork函数只被调用一次,但是会返回两次:父进程返回子进程的PID,子进程返回0.如果失败返回-1.
3.fork函数:
调用一次,返回两次。一次返回到父进程,一次返回到新创建的子进程。
并发执行,父进程和子进程是并发运行的独立程序。内核能够以任意方式交替执行他们的逻辑控制流中的指令。
相同但是独立的地址空间,父进程和子进程地址空间都相同,但对于变量所做的改变都是独立的。
共享文件,子进程继承了父进程所有打开的文件。
3、回收子进程
1. 回收:当一个进程终止时,内核并不立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收。
2. 僵死进程:一个终止了但是还未被回收的进程称为僵死进程。
3. 回收子进程的两种方法:
· 内核的init进程
· 父进程waitpid函数
如果父进程没有回收它的僵死子进程就终止了,那么内核就会安排init进城来回收它们。init进程的PID为1,并且是在系统初始化时创建的。
一个进程可以通过调用waitpid函数来等待它的子进程终止或停止。
4. waitpid函数有点复杂,默认地(当options=0时),waitpid挂起调用进程的执行,直到它的等待集合中的一个子进程终止。
waitpid函数
①判断等待集合的成员
等待集合的成员是由参数 pid 来确定的:
如果 pid>0,那么等待集合就是一个单独的子进程,它的进程lD等于 pid
如果 pid = -1 ,那么等待集合就是由父进程所有的子进程组成的。
②修改默认行为
可以通过将 optioins 设置为常量 WNOHANG WUNTRAα 的各种组合,修改默认行为:
WNOHANG: 如果等待集合中的任何子进程都还没有终止,那么就立即返回(返回值为0)。默认的行为是挂起调用进程,直到有子进程终止。在等待子进程终止的同时,如果还想做些有用的工作,这个选项会有用。
WUNTRACED :挂起调用进程的执行,直到等待集合中的一个进程变成已终止或者被停止。返回的 PID 为导致返回的己终止或被停止子进程的 PID。默认的行为是只返回己终止的子进程。当你想要检查已终止和被停止的子进程时,这个选项会有用。
WNOHANG UNTRACED: 立即返回,如果等待集合中没有任何子进程被停止或已终止,那么返回值为 ,或者返回值等于那个被停止或者己终止的子进程的 PID 。
③检查已回收子进程的退出状态
如果 status 参数是非空的,那么 waitpid 就会在 status 参数中放上关于导致返回的子进程的状态信息。 wait.h 头文件定义了解释 status 参数的几个宏
WIFEXITED (status) :如果子进程通过调用 exit 或者一个返回 (return) 正常终止,就返回真。
WEXITSTATUS (status) 返回一个正常终止的子进程的退出状态。只有在 WIFEXITED返回为真时,才会定义这个状态。
WIFSIGNALED (status): 如果子进程是因为一个未被捕获的信号终止的,那么就返回真。
WTERMSIG (status): 返回导致子进程终止的信号的数量。只有在 WIFSIGNALED(status) 返回为真时,才定义这个状态。
WIFSTOPPED (status) :如果引起返回的子进程当前是被停止的,那么就返回真。
WSTOPSIG (status): 返回引起子进程停止的信号的数量。只有在 WIFSTOPPED(status) 返回为真时,才定义这个状态。
④错误条件
如果调用进程没有子进程,那么waitpid返回-1,并且设置 errno为ECHILD。如果waitpid函数被一个信号中断,那么它返回一1,并设置 errno为EINTR。
wait函数
wait函数是waitpid函数的简单版本。 调用 wait(&status) 等价于调用 waitpid(-l &status , 0)
一种更高层次的软件形式的异常,称为unix信号,它允许进程中断其他进程。
低层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程而言是不可见的。信号提供了一种机制,通知用户进程发生了这些异常。
1、信号术语
传送一个信号到目的进程是由两个步骤组成的:
发送信号。内核通过更新目的进程上下文中的某个状态,发送(递送)一个信号给目的进程。 发送信号可以有如下两种原因:
· 内核检测到一个系统事件。
· 一个进程调用了kill函数,显式地要求内核发送一个信号给目的进程,一个进程可以发送信号给它自己。
接收信号。当目的进程被内核强迫以某种方式的发送做出反应时,目的进程就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理程序的用户层函数不活这个信号。
· 一个只发出而没有被接收的信号叫做待处理信号。在任何时刻,一种类型至多只会有一个待处理信号。
· 一个进程可以有选择性地阻塞接收某种信号。当一种信号被阻塞时,他仍可以被发送,但是产生的待处理信号不会被接收,直到进程取消对这种信号的阻塞。
· 一个待处理信号最多只能被接收一次。
2、发送信号
进程组:每个进程都只属于一个进程组,进程组是由一个正整数进程组ID来标识的。 一个子进程和它的父进程同属于一个进程组,一个进程组可以通过使用setpgid函数来改变自己或者其他进程的进程组。
用/bin/kill程序发送信号:用/bin/kill程序可以向另外的进程发送任意的信号。
从键盘发送信号:从键盘发送信号外壳为每个作业创建一个独立的进程组。
用kill函数发送信号:进程通过调用kill函数发送信号给其他进程(包括它们自己)。
用alarm函数发送信号:进程可以通过调用alarm函数向他自己发送SIGALRM信号。
3、接收信号
当内核从一个异常处理程序返回,准备将控制传递给进程P时,他会检查进程P的未被阻塞的处理信号的集合。如果这个集合为空,那么内核将控制传递到P的逻辑控制流中的下一条指令;如果集合是非空的,那么内核选择集合中的某个信号K(通常是最小的K0,并且强制P接收信号K。收到这个信号会触发进程的某种行为。一旦进程完成了这个行为,那么控制就传递回P的逻辑控制流中的下一条指令。
每个信号类型都有一个预定的默认行为:
· 进程终止
· 进程终止并转储存储器
· 进程停止直到被SIGCONT型号重启
· 进程忽略该信号
signal函数可以通过下列三种方法之一来改变和信号signum相关联的行为:
如果handler是SIG_IGN,那么忽略类型为signum的信号
如果handler是SIG_DFL,那么类型为signum的信号行为恢复为默认行为
否则,handler就是用户定义的函数的地址,这个函数成为信号处理程序,只要进程接收到一个类型为signum的信号,就会调用这个程序,通过把处理程序的地址传递到signal函数从而改变默认行为,这叫做设置信号处理程序。
①当一个进程捕获了一个类型为K的信号时,为信号K设置的处理程序被调用,一个整数参数被设置为K。这个参数允许同一个处理函数捕获不同类型的信号。
②信号处理程序的执行中断main C函数的执行,类似于底层异常处理程序中断当前应用程序的控制流的方式,因为信号处理程序的逻辑控制流与主函数的逻辑控制流重叠,信号处理程序和主函数并发地运行。
4、信号处理问题
当一个程序要捕获多个信号时,一些细微的问题就产生了。
待处理信号被阻塞。Unix信号处理程序通常会阻塞当前处理程序正在处理的类型的待处理信号。
待处理信号不会排队等待。任意类型至多只有一个待处理信号。因此,如果有两个类型为K的信号传送到一个目的进程,而由于目的进程当前正在执行信号K的处理程序,所以信号K时阻塞的,那么第二和信号就简单地被简单的丢弃,他不会排队等待。
系统调用可以被中断。像read、wait和accept这样的系统调用潜在地会阻塞进程一段较长的时间,称为慢速系统调用。在某些系统中,当处理程序捕获到一个信号时,被中断的慢速系统调用在信号处理程序返回时不再继续,而是立即返回给用户一个错误的条件,并将errno设置为EINTR。
不可以用信号来对其他进程中发生的事件计较。
5、可移植的信号处理
Signal包装函数设置的信号处理程序的信号处理语义:
1. 只有这个处理程序当前正在处理的那种类型的信号被阻塞。
2.和所有信号实现一样,信号不会排队等候。
3.只要有可能,被中断的系统调用会自动重启。
4. 一旦设置了信号处理程序,它就会一直保持,知道signal带着handler参数为SIG_ IGN或者SIG_DFL被调用。
6、同步流以避免讨厌的并发错误
一般而言,流可能交错的数量是与指令的数量呈指数关系的。
以某种方式同步并交流,从而得到最大的可行的交错的集合,每个可行的交错都能得到正确的结果。
如何编写读写相同存储位置的并发流程序的问题,困扰着数代计算机科学家。比如,竞争问题。
六、非本地跳转
用户级异常控制流形式,称为非本地跳转,它将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用-返回序列。
setjmp函数在env缓冲区中保存当前调用环境,供后面longjmp使用,并返回0。调用环境包括程序计数器、栈指针和通用目的寄存器。
int setjmp(jmp_buf env);
int sigsetjmp(setjmp_buf env,int savesigs);
longjmp函数从env缓冲区中恢复调用环境,然后触发一个从最近一次初始化env的setjmp调用的返回。然后setjmp返回,并带有非零的返回值retval。
void longjmp(jmp_buf env,int retval);
void siglongjmp(sigjmp_buf env,int retval);
七、操作进程的工具
Linux系统提供了大量的监控和操作进程的有用工具:
1. STRACE:打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹。对于好奇的的工具。用-StatiC编译你的程序,能传到一个更干净的、不带学生而言,这是一个令人着迷有大量与共享库相关的输出的轨迹。
2. PS:列出当前系统中的进程(包括僵死进程)
3. TOP:打印出关于当前进程资源使用的信息。
4. PMAP:显示进程的存储器映射。proc:一个虚拟文件系统,以ASCII文本格式输出大量内核数数据结构的内容,用户程序可 cat 2 / proc / load avg” , 观察在Linux系统上的平均负载。
参考资料
1.教材《深入理解计算机系统》
2.博客每周测试解析
3.小组同学提问与老师的解析
4.20135202闫佳歆博客http://www.cnblogs.com/20135202yjx/p/4926597.html
5.《嵌入式Linux应用程序开发标准教程》