2017-2018-1 20155306 《信息安全系统设计基础》第十四周学习总结
教材学习内容总结 第8章 异常控制流
现代系统通过使控制流发生突变来对这些情况做出反应。一般而言,我们把这些突变称为异常控制流ECF。异常控制流发生在计算机系统的各个层次。
- 理解ECF将帮助你理解重要的系统概念。ECF是操作系统用来实现I/O、进程和虚拟内存的基本机制。
- 理解ECF将帮助你理解应用程序是如何与操作系统交互的。应用程序通过使用一个叫做陷阱或者系统调用的ECF形式,向操作系统请求服务。
- 理解ECF将帮助你编写有用的新应用程序。操作系统为应用程序提供了强大的ECF机制,用来创建新进程、等待进程终止、通知其他进程系统中的异常事件,以及检测和响应这些事件。
- 理解ECF将帮助你理解并发。ECF是计算机系统中实现并发的基本机制。
- 理解ECF将帮助你理解软件异常如何工作。像C++和Java这样的语言通过try、catch以及throw语句来提供软件异常机制。软件异常允许程序进行非本地跳转(即违反通常的调用/返回栈规则的跳转)来响应错误情况。非本地跳转是一种应用层ECF,在C中是通过setjump和longjump函数来提供的。
8.1 异常
异常是异常控制流的一种形式,它是一部分由硬件实现的,一部分由操作系统实现的。
异常就是控制流中的突变,用来响应处理器中的某些变化。
状态变化称为事件,事件可能和当前指令的执行直接相关,也可能和当前指令的执行没有关系。
在任何情况下,当处理器检测到有事件发生时,它就会通过一张叫做异常表的跳转表,进行一个间接过程调用(异常),到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序)。当异常处理程序完成处理后,根据引起异常的事件的类型,会发生以下三种情况中的一种:
(1)处理程序将控制返回给当前指令,即当事件发生时正在执行的指令。
(2)处理程序将控制返回给下一指令,即如果没有发生异常将会执行的下一条指令。
(3)处理程序终止被中断的程序。
8.1.1异常处理
系统中可能的每种类型的异常都分配了一个唯一的非负整数的异常号。其中一些号码是由处理器的设计者分配的,其他号码是由操作系统内核(操作系统常驻内存的部分)的设计者分配的。前者的示例包括零除、缺页、内存访问违例、断点以及算术运算溢出。后者的示例包括系统调用和来自外部I/O设备的信号。
在系统启动时(当计算机重启或者加电时),操作系统分配和初始化一张称为异常表的跳转表,使得表目k包含异常k的处理程序的地址。
异常号是到异常表中的索引,异常表的起始地址放在一个叫做异常表基址寄存器的特殊CPU寄存器里。
异常类似于过程调用,但是有一些重要的不同之处:
- 过程调用时,在跳转到处理程序之前,处理器将返回地址压入栈中。然而,根据异常的类型,返回地址要么是当前指令(当事件发生时正在执行的指令),要么是下一条指令(如果事件不发生,将会在当前指令后执行的指令)。
- 处理器也把一些额外的处理器状态压到栈里,在处理程序返回时,重新开始执行被中断的程序会需要这些状态。
- 如果控制从用户程序转移到内核,那么所有这些项目都被压到内核栈中,而不是压到用户栈中。
- 异常处理程序运行在内核模式下,这意味着它们对所有的系统资源都有完全的访问权限。
8.1.2异常的类别
1.中断
中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。
当处理程序返回时,它就将控制返回给下一指令(也即如果没有发生中断,在控制流中会在当前指令之后的那条指令)。
剩下的异常类型(陷阱、故障和终止)是同步发生的,是执行当前指令的结果。我们把这类指令叫做故障指令。
2.陷阱和系统调用
陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
3.故障
故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就会将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的abort例程,abort例程会终止引起故障的应用程序。
一个经典的故障示例是缺页异常,当指令引用一个虚拟地址,而与该地址相对应的物理页面不再内存中,因此必须从磁盘中取出时,就会发生故障。
4.终止
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如DRAM或者SRAM位被损坏时发生的奇偶错误。终止处理程序从不将控制返回给应用程序。
8.1.3Linux/IA32系统中的异常
1.Linux/x86-64故障和终止
- 除法错误。当应用试图除以零时,或者当一个除法指令的结果对于目标操作数来说太大了的时候,就会发生除法错误(异常0)。Unix不会试图从除法错误中恢复,而是选择终止程序。
- 一般保护故障。许多原因都会导致不为人知的一般保护故障(异常13),通常是因为一个程序引用了一个未定义的虚拟内存区域,或者因为程序试图写一个只读的文本段。Linux不会尝试恢复这类故障。
- 缺页(异常14)是会重新执行产生故障的指令的一个异常示例。处理程序将适当的磁盘上虚拟内存的一个页面映射到物理内存的一个页面,然后重新执行这条产生故障的指令。
- 机器检查。机器检查(异常18)是在导致故障的指令执行中检测到致命的硬件错误时发生的。机器检查处理程序从不返回控制给应用程序。
2.Linux/86-64系统调用
每个系统调用都有一个唯一的整数号,对应于一个到内核中跳转表的偏移量。
在x86-64系统上,系统调用是通过一条称为syscall的陷阱指令来提供的。
所有的到Linux系统调用的参数都是通过通用寄存器而不是栈传递的。按照惯例,寄存器%rax包含系统调用号,寄存器%rdi、%rsi、%rdx、%r10、%r8和%r9包含最多六个任意的参数。
系统级write函数的第一个参数将输出发送到stdout。第二个参数是要写的字节序列,而第三个参数是要写的字节数。
8.2 进程
异常是允许操作系统内核提供进程概念的基本构造块。
进程的经典定义就是一个执行中的程序的实例。系统中的每个程序都是运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
每次用户通过向shell输入一个可执行目标文件的名字,运行程序时,shell就会创建一个新的进程,然后在这个新进程的上下文中运行这个可执行目标文件。
进程提供给应用程序的关键抽象:
- 一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
- 一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
8.2.1逻辑控制流
一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或者是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流,或者简称逻辑流。
关键点在于进程是轮流使用处理器的。每个进程执行它的流的一部分,然后被抢占,然后轮到其他进程,就像是在独占地使用处理器。
8.2.2并发流
一个逻辑流的执行在时间上与另一个流重叠,称为并发流,这两个流被称为并发并发地运行。
多个流并发地执行的一般现象称为并发。一个进程和其他进程轮流运行的概念称为多任务。一个进程执行它的控制流的一部分的每一个时间段叫做时间片。
如果两个流并发地运行在不同的处理器核或者计算机上,那么我们称它们为并发流,它们并行地运行,且并行地执行。
8.2.3私有地址空间
进程也为每个程序提供一种假象,好像它独占地使用系统空间。一般而言,和这个空间中某个地址相关联的那个存储器字节是不能被其他进程读或者写的。
地址空间底部是保留给用户程序的,顶部保留给保留给内核,用来存放内核在代表进程执行时的指令。
8.2.4用户模式和内核模式
处理器通常是用某个控制寄存器中的一个模式位来提供这种功能的,该寄存器描述了进程当前享有的特权。当设置了模式位时,进程就运行在内核模式中。
进程从用户模式变成内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。
Linux提供了一种聪明的机制,叫做/proc文件系统,它允许用户模式进程访问内核数据结构的内容。
8.2.5上下文切换
操作系统内核使用一种称为上下文切换的较高形式的异常控制流来实现多任务。上下文切换机制是建立在较低层异常机制之上的。
上下文就是内核重新启动一个被抢占的进程所需的状态。由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,如页表、进程表、文件表。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。这种决定就叫做调度,是由内核中称为调度器的代码处理的。
上下文切换:(1)保存当前进程的上下文,(2)恢复某个先前被抢占的进程被保存的上下文,(3)将控制传递给这个新恢复的进程。
当内核代表用户执行系统调用时,可能会发生上下文切换,中断也可能引发上下文切换。
8.3系统调用错误处理
Unix系统级函数遇到错误时,会典型地返回-1,并设置全局变量errno来表示出错内容。
通过使用错误处理包装函数,可以进一步简化代码。
8.4进程控制
8.4.1获取进程ID
每个进程都有一个唯一的正数(非零)进程ID(PID)。
#include<sys/types.h>
#include<unistd.h>
pid_t getpid(void);
pid_t getppid(void);
返回:调用者或其父进程的PID
8.4.2创建和终止进程
进程总是处于以下三种状态之一:
- 运行。在CPU上执行,或者等待被执行且最终会被内核调度。
- 停止。进程的执行被挂起,且不会被调度。(与信号有关)
- 终止。进程永远停止。进程终止的原因:1)收到一个信号,默认行为是终止程序2)从主程序返回3)调用exit函数。
#include<stdlib.h>
Void exit(int status);
该函数无返回值。
父进程通过调用fork函数创建一个新的运行子进程。只被调用一次,却会返回两次。在父进程中,fork返回子进程的PID。在子进程中,fork返回0。
特点:
- 调用一次,返回两次。
- 并发执行。内核能够以任意方式交替执行它们的逻辑控制流中的指令。
- 相同的但是独立的地址空间。
- 共享文件。子进程继承了父进程所有的打开文件。
8.4.3回收子进程
一个终止了但还未被回收的进程称为僵死进程。
如果父进程没有回收它的僵死进程就终止了,那么内核就会安排init进程来回收它们。Init进程的PID为1。
一个进程可以通过调用waitpid函数来等待它的子进程终止或者停止。
#include<sys/types.h>
#include<sys/wait.h>
pid_t waitpid(pid_t pid,int *status,int options);
1.判断等待集合的成员
如果pid>0,那么等待的集合就是一个单独的子进程,它的进程ID等于pid。
如果pid=-1,那么等待集合就是由父进程所有的子进程组成的。
2.修改默认行为
通过将optioins设置为常量WNOHANG和WUNTRACED的各种组合。
WNOHANG:如果等待集合中的任何子进程都还没有终止,那么就立即返回(返回值为0)。
WUNTRACED:挂起调用进程的执行,直到等待集合中的一个进程变成已终止或者被停止的。
WNOHANG|WUNTRACED:立即返回,如果等待集合中没有任何子进程被停止或已终止,那么返回值为0,或者返回值等于那个被停止或者已终止的子进程的PID。
3.检查已回收子进程的退出状态
wait.h头文件定义了解释status参数的几个宏:
WIFEXITED:如果子进程通过调用exit或者一个返回(return)正常终止,就返回真。
WEXITSTATUS:返回一个正常终止的子进程的退出状态。只有在WIFEXITED返回为真时,才会定义这个状态。
WIFSIGNALED:如果子进程是因为一个未被捕获的信号终止的,那么就返回真。
WTERMSIG:返回导致子进程终止的信号的编号。只有在WIFSIGNALED返回为真时,才定义这个状态。
WIFSTOPPED:如果引起返回的子进程当前是被停止的,那么就返回真。
WSTOPSIG:返回引起子进程停止的信号的数量。只有在WIFSTOPPED返回为真时,才定义这个状态。
4.错误条件
如果调用进程没有子进程,那么waitpid返回-1,并且设置errno为ECHILD。如果waitpid函数被一个信号中断,那么它返回-1,并设置errno为EINTR
5.wait函数
wait函数是waitpid函数的简单版本:
#include<sys/types.h>
#include<sys/wait.h>
pid_t wait(int *status);
调用wait(&status)等价于调用waitpid(-1,&status,0)。
8.4.4让进程休眠
sleep函数将一个进程挂起一段指定的时间。
#include<unistd.h>
unsigned int sleep(unsigned int secs);
pause函数,让调用函数休眠,直到该进程收到一个信号。
#include<unisted.h>
int pause(void);
8.4.5加载并运行程序
execve函数在当前进程的上下文中加载并运行一个新程序。
#include<unistd.h>
int execve(const char *filename,const char *argv[],
const char *envp[])
可执行文件filename,带参数列表argv和环境变量列表envp。execve调用一次从不返回。
#include<stdlib.h>
char *getenv(const char *name);
返回:若存在则为指向name的指针,若无匹配的,则为NULL。
getenv函数在环境数组中搜索字符串“name=value”。如果找到了,它就返回一个指向value的指针,否则它就返回NULL。
#include<stdlib.h>
int setenv(const char *name,const char *newvalue,int overwrite);
void unsetenv(const char *name);
如果环境数组包含一个形如“name=oldvalue”的字符串,那么unsetenv会删除它,而setenv会用newvalue代替oldvalue,但是只有在overwrite非零时才会这样。如果name不存在,那么setenv就把“name=value”添加到数组中。
8.4.6 利用fork和execve运行程序
shell是一个交互型的应用级程序,代表用户运行其他程序,执行一系列的读/求值步骤,然后终止。读步骤读取来自于命令行,求值步骤解析命令行,并代表用户运行程序。
8.5 信号
一种更高层的软件形式的异常,称为Unix信号,它允许进程中断其他进程。
8.5.1 信号术语
传送一个信号到目的进程是由两个不同步骤组成的:
发送信号。1)内核检测到一个系统事件,2)一个进程调用了kill函数,显式地要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己。
接收信号。一个只发出而没有被接收的信号叫做待处理信号。在任何时刻,一种类型只会有一个待处理信号。一个进程可以有选择性地阻塞接收某种信号。一个待处理信号最多只能被接收一次。
8.5.2发送信号
1.进程组
每个进程都只属于一个进程组,进程组是由一个正整数进程组ID来标识的。
一个子进程和它的父进程同属于一个进程组。
2.用/bin/kill程序发送信号
/bin/kill程序可以向另外的进程发送任意的信号。如 unix> /bin/kill -n m
3.从键盘发送信号
Unix外壳使用作业这个抽象概念来表示为对一个命令行求值而创建的进程。
int kill(pid_t pid,int sig);
4.用kill函数发送信号
进程通过调用kill函数发送信号给其他进程。
如果pid大于0,则发送信号sig给进程pid;若小于0,则发送信号给sig给进程组abs(pid)中的每个进程。
5.用alarm函数发送信号
进程可以通过调用alarm函数向它自己发送SIGALRM信号。
#include<unistd.h>
unsigned int alarm(unsigned int secs);
返回:前一次闹钟剩余的秒数,若以前没有设定闹钟,则为0。
alarm函数安排内核在secs秒内发送一个SIGNALRM信号给调用进程。
8.5.3接收信号
当内核从一个异常处理程序返回,准备将控制传递给进程p时,它会检查进程p的未被阻塞的待处理信号的集合。
如果这个集合为空(通常情况下),那么内核将控制传递到p的逻辑控制流中的下一条指令。
8.5.4阻塞和解除阻塞信号
sigprocmask函数改变当前已阻塞信号的集合,具体行为依赖how的值。
SIG_BLOCK:添加set中的信号到blocked中。
SIG_UNBLOCK:从blocked中删除set中的信号。
SIG_SETMASK:blocked=set。
int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);
8.6非本地跳转
C语言提供了一种用户级异常控制流形式,称为非本地跳转。通过setjmp和longjmp函数来提供。
stejmp函数在env缓冲区中保存当前调用环境,一共后面longjmp使用,并返回0。调用环境包括程序计数器、栈指针和通用目的寄存器。
longjmp函数从env缓冲区中恢复调用环境,然后触发一个从最近一次初始化env的setjmp调用的返回。然后setjmp返回,并带有非零的返回值retval。
setjmp函数只被调用一次,但返回多次。longjmp函数被调用一次,但从不返回。
8.7操作进程的工具
STRACE:打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹。
PS:列出当前系统中的进程(包括僵死进程)。
TOP:打印出关于当前进程资源使用的信息。
PMAP:显示出进程的存储器映射。
代码托管
(statistics.sh脚本的运行结果截图)
教材课后习题
8.9—8.12
** 8.13—8.17 **
8.18—8.23
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第一周 | 20/20 | 1/2 | 10/15 | 第一章 |
第三周 | 130/210 | 1/2 | 21/36 | 第二章 |
第四周 | 70/ 280 | 1/4 | 10/46 | 第十章 |
第五周 | 91 / 371 | 1/6 | 23/69 | 第三章 |
第六周 | 308 / 648 | 1/8 | 31/100 | 第八、十章 |
第七周 | 2200 / 2848 | 1/10 | 25/125 | 第四章 |
第八周 | 1072 / 3492 | 1/12 | 41/166 | 第十一,十二章 |
第九周 | 508/4000 | 1/14 | 30/196 | 第六章 |
第十周 | 1377 / 3492 | 1/14 | 12/208 | 实验 实时系统 |
第十一周 | 350 / 5727 | 1/16 | 35/243 | 第九章 |
第十三周 | 200 / 5927 | 1/17 | 30/273 | 第六章 |
第十四周 | 228 / 5927 | 1/19 | 31/304 | 第八章 |
尝试一下记录「计划学习时间」和「实际学习时间」,到期末看看能不能改进自己的计划能力。这个工作学习中很重要,也很有用。
耗时估计的公式
:Y=X+X/N ,Y=X-X/N,训练次数多了,X、Y就接近了。
-
计划学习时间:30小时
-
实际学习时间:31小时
-
改进情况:
(有空多看看现代软件工程 课件
软件工程师能力自我评价表)