异常控制流
第十一周(11.16-11.22):
一、学习目标
1. 了解异常及其种类
2. 理解进程和并发的概念
3. 掌握进程创建和控制的系统调用及函数使用:fork,exec,wait,waitpid,exit,getpid,getppid,sleep,pause,setenv,unsetenv,
4. 理解数组指针、指针数组、函数指针、指针函数的区别
5. 理解信号机制:kill,alarm,signal,sigaction
6. 掌握管道和I/O重定向:pipe, dup, dup2
二、学习资源
1. 教材:第八章《异常控制流》
2. 课程资料:https://www.shiyanlou.com/courses/413
3. 实验九,课程邀请码:W7FQKW4Y
异常控制流
从给处理器加电开始,直到断电为止,程序计数器假设一个值的序列:
a0,a1,...,a(n-1)
其中,每个ak是某个相应的指令Ik的地址。每次从ak到a(k+1)的过渡称为控制转移。这样的控制转移序列叫做处理器的控制流。
作为程序员,理解ECF很重要:
-
理解ECF将帮助你理解重要的系统概念
-
理解ECF将帮助你理解应用程序是如何和操作系统实现交互的
-
理解ECF将帮助你编写有趣的新应用程序
-
理解ECF将有助于你理解和开发
-
理解ECF将帮助你理解软件异常如何工作
8.1异常
异常就是控制流中的突变,用来响应处理器状态中的某些变化。
在异常处理程序完成处理后,根据引起异常的事件的类型,会发生以下以下三种情况中的一种:
-
处理程序将控制返回给当前指令Icurr,即如果没有发生异常将会执行的下一条指令
-
处理程序将控制返回给Inext,即如果没有发生异常将会执行的下一条指令
-
处理程序终止被中断的程序
8.1.1异常处理
系统中可能的每种类型的异常都分配了一个唯一的非负整数的异常号。
异常号是到异常表中的索引,异常表的起始地址放在一个叫做异常表基址寄存器的特殊CPU寄存器里。
8.1.2异常的类别
异常的类别可以分为四类:
中断、陷阱、故障和终止
1.中断
中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。硬件中断不是由任何一条专门的指令造成的。
2.陷阱和系统调用
陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用
3.故障
故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。
4.终止
终止时不可恢复的致命错误造成的结果,通常是一些硬件上的错误
8.1.3Linux/IA32系统中的异常
1.Linux/IA32故障和终止
- 除法错误
- 一般保护故障
- 缺页
- 机器检查
Linux/IA32系统调用
Linux提供上百种系统调用,在应用程序想要请求内核服务时可以使用,包括读文件、写文件或是创建一个新进程。
8.2进程
异常是允许操作系统提供进程的概念所需要的基本构造块,进程是计算机科学中最深刻最成功的概念之一。
进程提供给应用程序的关键抽象:
- 一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器
- 一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用存储器系统
8.2.1逻辑控制流
即使在系统中通常有许多其他程序在运行,进程也可以向每个程序提供一种假象,好像它在独占地使用处理器。如果想用调试器单步地执行程序,我们会看到一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或者是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称逻辑流。
8.2.2并发流
一个逻辑流的执行时间上与另一个流重叠,称为并发流,这两个流被称为并发地运行。
多个流并发地执行的一般现象称为并发。
8.2.3私有地址空间
进程也为每个程序提供一种假象,好像它独占地使用系统地址空间。在一台有n位地址的机器上,地址空间是2n个可能地址的集合,0,1,。。。,2n-1.一个进程为每个程序提供它自己的私有地址空间。一般而言,和这个空间中某个地址相关联的那个存储器字节是不能被其他进程读或者写,从这个意义上说,这个地址空间是私有的。
8.2.4用户模式和内核模式
处理器通常是用某个控制寄存器中的一个模式位来提供这种功能的,该寄存器描述了进程当前享有的特权。当设置了模式位时,进程就运行在内核模式中。一个运行在内核模式的进程可以执行指令集中的任何的指令,并且可以访问系统中任何存储器的位置。
8.2.5上下文切换
操作系统内核使用一种称作上下文切换的较高的形式的异常控制流来实现多任务。
8.3系统调用错误处理
当Unix系统级函数遇到错误时,它们会典型地返回-1,并设置全局整数变量errno来表示什么出错了。程序员应该总是检查错误,但是不幸的是,许多人都忽略了错误检查,因为它使得代码变得臃肿,而且难以读懂。
通过使用错误处理包装函数,我么可以更进一步地简化我们的代码
8.4进程控制
8.4.1获取进程ID
每个进程都有一个唯一的整数进程ID。getpid函数返回调用进程的PID.
8.4.2创建和终止进程
从程序员的角度看,我们可以认为进程总是处于下面三种状态之一:
- 运行
- 停止
- 挂起
P493的例子
当在Unix系统上运行程序时,我们得到下面的结果:
unix>./fork
parent:x=0
child:x=2
这个简单的例子有一些微妙的方面
- 调用一次,返回两次
- 并发执行
- 相同的但是独立的地址空间
- 共享文件
8.4.3回收字进程
当一个进程由于某种原因终止时,内核并不是立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收。当父进程回收已终止的子进程时,内核将子进程的退出状态传递给父进程,然后抛弃已终止的进程,从此时开始,该进程就不存在了。一个终止了但还未被回收的进程称为僵死进程。
1.判断等待集合的成员
2.修改默认行为
3.检查已回收子进程的退出状态
4.错误条件
5.wait函数
8.4.4让进程休眠
sleep函数将一个进程挂起一段指定的时间
如果请求的时间量已经到了,sleep返回0,否则返回还剩下的要休眠的秒数
8.4.5加载并运行程序
exexve函数在当前进程的上下文中加载并运行一个新程序
如果成功,则不返回,如果错误,则返回-1
8.4.6利用fork和execve运行程序
像Unix外壳和Web服务器这样的程序大量使用了fork和execve函数。外壳是一个交互型的应用级程序,它代表用户运行其他程序。
8.5信号
一种高层的软件形式的异常,称为Unix信号,它允许进程中断其他进程。
8.5.1信号术语
传送一个信号到目的进程是由两个不同的步骤组成的:
- 发送信号
- 接受信号
8.5.2发送信号
Unix系统提供大量向进程发送信号的机制。所有这机制都是基于进程组这个概念的
- 1.进程组
- 2.用/bin/kill程序发送信号
- 3.从键盘发送信号
- 4.用kill函数发送信号
- 5.用alarm函数发送信号
8.5.3接受信号
下面是默认行为中的一种:
- 进程终止
- 进程终止并转储存储器
- 进程停止直到被SIGCONT信号重启
- 进程忽略该信号
signal函数可以通过下列三种方法之一来改变和信号signum相关联的行为
- 如果handler是SIG_IGN,那么忽略类型为signum的信号
- 如果handler是SIG_DFL,那么类型为signum的信号行为恢复为默认行为
- 否则,handler就是用户定义的函数的地址,这个函数称为信号处理程序,只要进程接受到一个类型为signum的信号,就会调用这个程序。
8.5.4信号处理问题
对于只捕获一个信号并终止的程序来说,信号处理是简单直接的。然而,当一个程序要捕获多个信号时,一些细微的问题就产生了。
- 待处理信号被阻塞
- 待处理信号不会排队等待
- 系统调用可以被中断
8.5.5可移植的信号处理
不同系统之间,信号处理语义的差异,是Unix信号处理的一个缺陷。
为了处理这个问题,POSIX标准定义了sigaction函数,它允许像Linux和Solaris这样与Posix兼容的系统上的用户,明确地指定他们想要的信号处理语义。
signal包装函数设置了一个信号处理程序,其信号处理语义如下:
- 只有这个处理程序当前正在处理的那种类型的信号被阻塞
- 和所有信号实现一样,信号不会排队等待
- 只要可能,被中断的系统调用会自动重启
- 一但设置了信号处理程序,它就会一直保持
8.5.6显式地阻塞和取消阻塞信号
应用程序可以使用sigprocmask函数显式地阻塞和取消阻塞信号
8.5.7同步流以避免讨厌的并发错误
8.6非本地跳转
C语言提供了一种用户级异常控制流形式,称为非本地跳转,它将控制直接从一个函数转移到另一个当前正在执行的函数,而不需要经过正常的调用。
8.7操作进程的工具
大量的监控和操作进程的有用的工具
STRACE
PS
TOP
PMAP
异常控制流(ECF)发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制