第八章 异常控制流
控制转移序列称为控制流。
从从一条指令到下一条指令称为转移控制。
异常控制流:现代操作系统通过使控制流发生突变来对系统状态做出反应,这些突变称为异常控制流。
异常控制流ECF:即这些突变。
1.ECF是操作系统用来实现I/O、进程和虚拟存器的基本机制 2.应用程序通过使用一个叫做陷阱或者系统调用的ECF形式,向操作系统请求服务 3.ECF是计算机系统中实现并发的基本机制 4.软件异常机制——C++和Java有try,catch,和throw,C中非本地跳转是setjmp和longjmp
8.1 异常
1.异常是异常控制流的一种形式,一部分由由硬件实现,一部分由操作系统实现。由于系统的不同而有所不同。
异常就是控制流的突变。
2.异常的剖析:
3.异常处理
系统为每种类型的异常分配的唯一的非负整数的异常号。
系统启动时操作系统就会初始化一张称为异常表的跳转表,使得条目k包含异常k的处理程序的地址。
异常号是到异常表中的索引,异常表的起始地址放在异常表基址寄存器的特殊CPU寄存器里。
4.异常的类别
分为四种:中断、陷阱、故障和终止
(1)中断
-
异步发生
-
来自处理器外部的I/O设备的信号的结果
-
将控制返回给下一条指令
(2)陷阱和系统调用
-
陷阱是有意的异常
-
是执行一条指令的结果
-
最重要的用途:在用户和内核间提供一个像过程一样的接口,叫系统调用
(3)故障
- 由错误状况引起,可能能够被故障处理程序修正
-
故障发生时,处理器将控制转移给故障处理程序,如果能够修正,返回引起故障的指令,重新执行指令,否则返回abort例程,终止
-
典型示例:缺页异常,如图:
(4)终止
-
是不可恢复的致命错误造成的结果
-
通常是一些硬件错误
-
终止示例:将控制返回abort例程,如图:
3.Linux/IA32系统中的异常
高达256种异常类型,如图8:
(1)Linux/IA32故障和终止
-
除法错误/浮点异常:异常0,选择中止程序
-
一般保护故障/段故障:异常13,选择中止程序
-
缺页:异常14,重新执行产生故障的指令
-
机器检查:异常18,不返回控制给应用程序
(2)Linux/IA32系统调用
系统调用示例,如图9所示:
8.2 进程
在操作系统层:逻辑控制流,私有地址空间,多任务,并发,并行,上下文,上下文切换,调度。
进程就是一个执行中的程序实例。系统中的每个程序都是运行在某个进程的上下文中的。
进程提供给应用程序的关键抽象:a)一个独立的逻辑控制流 ;b)一个私有的地址空间
1.逻辑控制流
一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或者是包含在运行时动态链接到程序的共享对象中的指令,这个PC值的序列就叫做逻辑控制流,或者简称逻辑流。
- 示例:三进程系统,如图所示
进程是轮流使用处理器的。每个进程执行它的流的一部分,然后被抢占(暂时挂起),然后轮到其他进程。对于运行在改程序上下文的其他程序,它看上去在独占的使用处理器。
2.并发流
并发流:并发流一个逻辑流的执行在时间上与另一个流重叠。
并发:多个流并发执行的一般现象称为并发。
多任务:多个进程并发叫做多任务。
并行:并发流在不同的cpu或计算机上。
3.私有地址空间
一个进程为每个程序提供它自己的私有地址空间。
运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过异常。
- 进程地址空间,如图11所示
4.用户模式和内核模式
需要限制一个应用可以执行的指令以及可访问的地址空间范围来实现进程抽象,通过特定控制寄存器的一个模式位来提供这种机制。设置了模式位时,进程运行在内核模式中,进程可以执行任何指令和访问任何存储器位置。没设置模式位时,进程运行在用户模式中,进程不允许执行特权指令和访问地址空间中内核区内的代码和数据。用户程序必须通过系统调用接口间接地访问内核代码和数据。
用户程序的进程初始是在用户模式中的,必须通过中断、故障或陷入系统调用这样的异常来变为内核模式。
Linux的聪明机制——/proc文件系统,包含内核数据结构的内容的可读形式,运行用户模式进程访问。
5.上下文切换
-
上下文切换:操作系统内核使用叫上下文切换的异常控制流来实现多任务。
-
上下文切换机制:
- (1)保存当前进程的上下文;
- (2)恢复某个先前被抢占的进程被保存的上下文;
-
(3)将控制传递给这个新恢复的进程
-
调度:内核中的调度器实现调度。
-
当内核代表用户执行上下文切换时,可能会发生上下文切换。
-
如果系统调用发生阻塞,那么内核可以让当前进程休眠,切换到另一个进程,如read系统调用,或者sleep会显示地请求让调用进程休眠。一般,即使系统调用没有阻塞,内核亦可以决定上下文切换,而不是将控制返回给调用进程。
-
示例:进程A与B之间上下文切换,如图12所示
8.3 系统调用错误处理
-
在Linux中,可以使用 man syscalls 查看全部系统调用的列表。
-
系统级函数遇到错误时,通常返回-1,并设置全局变量 errno 。
8.4进程控制
1.获取进程ID
#include <sys/types.h> #include <unistd.h> pid_t getpid(void); /*返回调用进程的PID*/ pid_t getppid(void); /*返回它的父进程的PID(创建调用进程的进程)*/
2.创建和终止进程
进程的三种状态——运行、停止和终止。
-
运行:要么在CPU上执行,要么在等待被执行,且最终被内核调度。
-
停止:进程的执行被挂起,且不会被调度。收到 SIGSTOP 、 SIGTSTP 、 SIDTTIN 、 SIGTTOU 信号,进程停止,收到 SIGCONT 信号,进程再次开始运行。
-
终止:永远停止。原因可能是:收到终止进程的信号,从主程序返回,调用 exit 函数。
创建进程
父进程通过调用fork创建一个新的运行子进程:父进程与子进程有相同(但是独立的)地址空间,有相同的文件藐视符集合。
fork函数定义如下:
#include <sys/types.h> #include <unistd.h> pid_t fork(void);
fork 函数调用一次,返回两次;父子进程是并发运行的,不能假设它们的执行顺序;两个进程的初始地址空间相同,但是是相互独立的;它们还共享打开的文件。因为有相同的程序代码,所以如果调用 fork 三次,就会有八个进程。
3.回收子进程
-
当一个进程终止时,内核并不立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收。
-
僵死进程:一个终止了但是还未被回收的进程称为僵死进程。父进程回收终止的子进程时,内核将子进程退出状态传给父进程,然后抛弃该进程。如果回收前父进程已经终止,那么僵死进程由 init 进程回收。
一个进程可以通过调用waitpid函数来等待它的子进程终止或停止。
waitpid函数的定义如下:
#include <sys/types.h> #include <sys/wait.h> pid_t waitpid(pid_t pid, int *status, int options);
(1)判断等待集合的成员
等待集合的成员由参数pid来确定:
-
如果pid>0:等待集合是一个单独子进程,进程ID等于pid
-
如果pid=-1:等待集合是由父进程所有的子进程组成
(2)修改默认行为
将options设置为常量WNOHANG和WUNTRACED的各种组合,修改默认行为:
(3)检查已回收子进程的退出状态——status
(4)让进程休眠
sleep函数使一个进程挂起一段指定的时间。
#include <unistd.h> unsigned int sleep(unsigned int secs);
pause函数让调用函数休眠
#include <unistd.h> int pause(void);
(5)加载并运行程序
-
filename:可执行目标文件
-
argv:参数列表
-
envp:环境列表
8.5信号
在操作系统和应用程序之间:进程之间传送信号
一种更高层次的软件形式的异常,称为unix信号,它允许进程中断其他进程。
1.信号术语
(1)发送信号
发送信号:/bin/kill , kill函数,键盘,alarm函数
-
用kill函数发送信号:发送SIGKILL信号
-
用alarm函数发送信号:发送SOGALARM信号
(2)接收信号
进程可以通过使用signal函数来修改和信号相关的默认行为。唯一的例外是SIGSTOP和SIGKILL,它们的默认行为不能被修改。
2.发送信号
(1)进程组
-
每个进程都只属于一个进程组。
-
进程组是由一个正整数进程组ID来标识的。
-
getpgrp函数返回当前进程组id
-
setpgid函数修改自己或其他进程组
(2)用/bin/kill程序发送信号
/bin/kill程序可以向另外的进程发送任意的信号,比如 /bin/kill -9 15213
即为:发送信号9给进程15213
(3)从键盘发送信号
在任何时刻,至多只有一个前台作业和0个或多个后台作业。外壳为每个作业创建一个独立的进程组,一个作业对应一个进程组。
(4)用kill函数发送信号
发送SIGKILL信号
(5)用alarm函数发送信号
发送SOGALARM信号
3.接收信号
当内核从一个异常处理程序返回,准备将控制传递给进程P时,他会检查进程P的未被阻塞的处理信号的集合。如果这个集合为空,那么内核将控制传递到P的逻辑控制流中的下一条指令;如果集合是非空的,那么内核选择集合中的某个信号K(通常是最小的K0,并且强制P接收信号K。收到这个信号会触发进程的某种行为。一旦进程完成了这个行为,那么控制就传递回P的逻辑控制流中的下一条指令。
每个信号类型都有一个预定的默认行为:
-
进程终止
-
进程终止并转储存储器
-
进程停止直到被SIGCONT型号重启
-
进程忽略该信号
4.信号处理问题
当一个程序要捕获多个信号时,一些细微的问题就产生了。
-
待处理信号被阻塞。Unix信号处理程序通常会阻塞当前处理程序正在处理的类型的待处理信号。
-
待处理信号不会排队等待。任意类型至多只有一个待处理信号。因此,如果有两个类型为K的信号传送到一个目的进程,而由于目的进程当前正在执行信号K的处理程序,所以信号K时阻塞的,那么第二和信号就简单地被简单的丢弃,他不会排队等待。
-
系统调用可以被中断。像read、wait和accept这样的系统调用潜在地会阻塞进程一段较长的时间,称为慢速系统调用。在某些系统中,当处理程序捕获到一个信号时,被中断的慢速系统调用在信号处理程序返回时不再继续,而是立即返回给用户一个错误的条件,并将errno设置为EINTR。
5.可移植的信号处理
Signal包装函数设置的信号处理程序的信号处理语义:
-
只有这个处理程序当前正在处理的那种类型的信号被阻塞
-
和所有信号实现一样,信号不会排队等候
-
只要有可能,被中断的系统调用会自动重启。
-
一旦设置了信号处理程序,它就会一直保持,知道signal带着handler参数为SIGIGN或者SIGDFL被调用。
6.显式地阻塞和取消阻塞信号
7.同步流以避免讨厌的并发错误
以某种方式同步并交流,从而得到最大的可行的交错的集合,每个可行的交错都能得到正确的结果。
8.6非本地跳转
c语言中,用户级的异常控制流形式,通过setjmp和longjmp函数提供。
setjump函数在env缓冲区中保存当前调用环境,以供后面longjmp使用,并返回0。sig—函数是setjmp和longjmp函数的可以被信号处理程序使用的版本。
8.7操作进程的工具
Linux系统提供了大量的监控和操作进程的有用工具:
STRACE:打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹。对于好奇的的工具。用-StatiC编译你的程序,能传到一个更干净的、不带学生而言,这是一个令人着迷有大量与共享库相关的输出的轨迹。
PS:列出当前系统中的进程(包括僵死进程)
TOP:打印出关于当前进程资源使用的信息。
PMAP:显示进程的存储器映射。proc:一个虚拟文件系统,以ASCII文本格式输出大量内核数数据结构的内容,用户程序可 cat 2 / proc / load avg” , 观察在Linux系统上的平均负载。
总结
这周学习内容比较多,前面还看得比较懂,后面有些乱,希望老师上课时讲一下非本地跳转的知识,我对这里还是没有理解。
参考资料
教材:《深入理解计算机系统》