20135203齐岳 信息安全系统设计基础第十一周学习总结
学习计时:10/13共小时(计划/实际)
-
读书:5/7
-
代码:1/1
-
作业:1/1
-
博客:3/4
第八章 异常控制流
一、学习目标
- 了解异常及其种类
- 理解进程和并发的概念
- 掌握进程创建和控制的系统调用及函数用:fork,exec,wait,waitpid,exit,getpid,getppid,sleep,pause,setenv,unsetenv,
- 理解数组指针、指针数组、函数指针、指针函数的区别
- 理解信号机制:kill,alarm,signal,sigaction
- 掌握管道和I/O重定向:pipe, dup, dup2
二、学习任务
- 阅读教材,完成课后练习(书中有参考答案)
- 考核:练习题把数据变换一下
- 加分题:课后作业最多两人一组,互相不能重复,1星题目每人最多加一分,2星题目每人最多加二分,3星题目每人最多加三分,4星题目每人最多加四分。
三、学习过程
8.1 异常
从处理器运行开始到结束,程序计数器假设一个序列的值a0a1......an-1,这个控制转义序列叫做处理器的控制流,异常,就是控制流中的突变,用来响应处理器状态中的某些变化。
状态的变化称为事件,在任何情况下,当处理器检测到有事件发生时,会通过一张叫做异常表的跳转表,进行一个间接过程调用到专门处理程序——异常处理程序。当异常处理程序完成之后,根据引起引起异常的事件类型,会发生以下三种情况之一:
-
处理程序将控制返回给当前指令,即事件发生之时正在执行的指令。
-
处理程序将控制返回给如果没有异常将会执行的下一条指令。
-
处理程序终止被中断的程序。
异常处理
系统中可能的每种异常都被分配了唯一一个非负整数的异常号,异常表中的条目k中包含异常k的处理程序地址。异常表的起始地址存放在一个叫做异常表基址寄存器的特殊寄存器中。
异常类和过程调用的不同之处:
-
返回地址是当前地址或者下一条指令
-
处理器也会把额外的处理器状态压回栈中,在处理程序返回时,重新开始被中断的程序会需要这些状态。
-
如果控制从一个用户程序转移到内核,那么所有项目都会被压到内核栈中而不是用户栈。
-
异常处理程序运行在内核模式下,意味着他们对所有的系统资源拥有完全的访问权限。
异常的类别
1.中断
中断是异步发生的,是来自处理器外部的I/O设备的信号的结果。因此是异步的。硬件中断的异常处理程序通常称为中断处理程序。
其余异常类型都是同步发生的,是执行当前指令的结果。这一类指令称为故障指令。
2.陷阱
陷阱是有意的异常,最重要的用途是在用户程序和内核之间提供一个向过程一样的接口,叫做系统调用。
为了允许内核服务的受控访问,使用“syscall n”指令,跳转到一个异常处理程序的陷阱,处理程序对参数解码并调用适当的内核程序。
3.故障
故障由错误情况引起,可能能够被故障处理程序修正。故障发生时,处理器将控制转移给故障处理程序,若能修正,则将控制返回到引起故障的指令,重新执行;若不能修正,处理程序返回abort例程,终止引起故障的应用程序。
4.终止
终止是不可恢复的致命错误造成的结果,通常是硬件错误。终止处理程序将控制直接返回给abort例程,直接终止该应用程序。
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不能使用,因为当进入内核模式时,内核会覆盖它。
进程
进程,就是一个执行中的程序的实例,系统中的每个程序都是定义在运行在某个进程的上下文中的。异常是允许操作系统提供进程的概念所需要的基的本构造块。
逻辑控制流
PC的值唯一地对应于包含在程序可执行目标文件中的指令,或者是包含在运行时动态链接到程序的共享对象中的指令。这个PC值的序列叫做逻辑控制流。
进程轮流使用处理器,每个进程执行流的一部分,然后被抢占(暂时挂起)。
并发流
一个逻辑流的执行在时间上与另一个流重叠,称为并发流。
一个进程和其他进程轮流运行的概念称为多任务,一个进程执行它的控制流的一部分时间称为时间片。
私有地址空间
一个进程为每个程序提供他自己的私有地址空间,一般而言,和这个空间中某个地址相关联的存储器字节不能被其他程序读或写。
地址空间底部是保留给用户程序的,顶部保留给保留给内核,用来存放内核在代表进程执行时的指令。
用户模式和内核模式
处理器通过某个控制寄存器中的一个模式位来提供这种功能。
-
内核模式:设置模式位,进程可以执行指令集中的任何指令,并且访问系统中的任何存储器位置。
-
用户模式:不设置模式位,进程不允许执行特权指令,也不允许直接引用地址空间中内核区的代码和数据。
初始模式是用户模式,进入内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。
/proc文件系统,允许用户模式进程访问内核数据结构的内容。
上下文切换
上下文:内核重新启动一个被抢占的进程所需的状态。包括描绘地址空间的页表、包含有关当前进程信息的进程表,以及半酣进程已打开文件的信息的文件表。
调度:进程执行过程中,内核决定抢占当前进程并开始一个先前被抢占的进程,由内核中的调度器完成。
内核为每个进程维持一个上下文。并通过一种称为上下文切换的较高层形式的异常控制流来实现多任务上下文切换:
- 保存当前进程的上下文;
- 恢复某个先前被抢占的进程被保存的上下文;
- 将控制传递给新恢复的进程。
8.3系统调用错误处理
Unix系统级函数遇到错误时,会典型地返回-1,并设置全局变量errno来表示出错内容。
通过使用错误处理包装函数,可以进一步简化代码。
8.4进程控制
获取进程ID
每个进程有一个唯一的非零正数进程ID(PID)。
pid_t getpid(void); /*返回调用进程的PID*/
pid_t getppid(void); /*返回它的父进程的PID*/
创建和终止进程
进程总是处于以下三种状态之一:
- 运行。在CPU上执行,或者等待被执行且最终会被内核调度。
- 停止。进程的执行被挂起,且不会被调度。(与信号有关)
- 终止。进程永远停止。进程终止的原因:1)收到一个信号,默认行为是终止程序2)从主程序返回3)调用exit函数。
exit函数以status退出状态来终止进程。
void exit(int ststus)
父进程通过调用fork函数创建一个新的运行子进程。
pid_t fork(void); /*子进程返回0,父进程返回子进程的pid,出错则返回-1。*/
fork函数:
- 调用一次,返回两次。一次返回到父进程,一次返回到新创建的子进程。
- 并发执行,父进程和子进程是并发运行的独立程序。内核能够以任意方式交替执行他们的逻辑控制流中的指令。
- 相同但是独立的地址空间,父进程和子进程地址空间都相同,但对于变量所做的改变都是独立的。
- 共享文件,子进程继承了父进程所有打开的文件。
回收子进程
当一个进程由于某种原因终止,内核不会将他马上清除,而是将进程保持在已终止的状态中,直到被他的父进程回收。
系统通过调用waitpid函数来等待它的子进程终止或停止。
pid_t waitpid(pid_t pid,int *status,int options);
- 判定等待集合的成员:pid>0,则等待集合为单独的进程;pid=-1,等待集合是由父进程所有的子进程组成的。
- 修改默认行为:可以通过将options设置为常量,修改默认行为。
- 检查已回收子进程的退出状态:如果status参数是非空的,则status参数会放上关于导致返回的子进程的状态信息。
-
错误条件:如果调用进程没有子进程,那么waitpid返回-1,并设置errno为ECHILD,如果函数被一个信号中断,那么返回-1,并设置errno为EINTR。
-
wait函数
pid_t wait(int *status);
调用wait(&status)等价于调用waitpid(-1,&status,0)。
让进程休眠
sleep函数将一个进程挂起一段指定的时间。
unsigned int sleep(unsigned int secs); /*返回还要休眠的秒数*/
pause函数让调用函数休眠,直到进程收到一个信号。
int pause(void); /*总是返回-1*/
加载并运行程序
execve函数在当前进程的上下文中加载并运行一个新程序。
int execve(const char*filename,const char *argv[],const char *envp);
/*成功则不返回,错误返回-1*/
利用fork和execve运行程序
外壳:一个交互型的应用级程序,代表用户运行其他程序,执行一系列的读/求值步骤,然后终止。读步骤读取来自于命令行,求值步骤解析命令行,并代表用户运行程序。
8.5信号
信号:一条小消息,通知进程系统中发生了一个某种类型的事件。
信号术语
-
发送信号:内核通过更新目的进程上下文中的某个状态,发送一个信号给目的进程。
-
接收信号:当目的进程被内核强迫以某种方式对信号的发送做出反应时,目的进程就接收了信号。通过信号处理程序捕获信号。
发送信号
进程组
每个进程都只属于一个进程组,由一个正整数进程组ID来标识。
getpgrp函数返回当前进程的进程组ID。
pid_t getpgrp(void);
setpgrp函数改变自己或者其他进程的进程组。
int setpgrp(pid_t pid,pid_t pgid);
用/bin/kill程序发送信号
/bin/kill 程序可以向另外的进程发送任意的信号。一个为负的PID会导致信号被发送到进程组PID中的每个进程。
从键盘发送信号
Unix外壳使用作业这个抽象概念来表示为对一个命令行求值而创建的进程。
int kill(pid_t pid,int sig);
用kill函数发送信号
进程通过调用kill函数发送信号给其他进程(包括自己)。
如果pid大于0,则发送信号sig给进程pid;若小于0,则发送信号给sig给进程组abs(pid)中的每个进程。
用alarm函数发送信号
进程通过调用alarm函数向他自己发送SIGALRM信号
unsigned int alarm(unsigned int secs); /*返回前一次闹钟剩余的秒数,若没有则返回0*/
接收信号
内核从一个异常处理的程序返回,准备将控制传递给p时,会检查p未被阻塞的待处理信号的集合,若集合为空,则转到p逻辑控制流中的下一条指令;若集合不为空,则内核选择集合中的某个信号k,并且强制p接受信号k,触发进程的某种行为,完成后转到p逻辑控制流中的下一条指令。
k的默认行为有:
-
进程终止。
-
进程终止并转储存储器。
-
进程停止直到被SIGCONT信号重启。
-
进程忽略该信号。
信号处理问题
一个程序捕获多个信号时产生的问题:
-
待处理信号被阻塞。
-
待处理信号不会排队等待。
-
系统调用可以被中断。
不可以用信号来对其他进程中发生的事件计数。
可移植的信号处理
为了处理不同系统之间信号处理语义的差异,通过sigaction函数使不同系统上兼容的用户明确指定想要的信号处理语义。
int sigaction(int signum,struct sigaction *act,struct sigaction *oldact);
显示地阻塞和取消阻塞信号
int sigprocmask(int how,const sigset_t *set,sigset_t *oldset);
sigprocmask函数改变当前已阻塞信号的集合,具体行为依赖how的值。
- SIG_BLOCK:添加set中的信号到blocked中。
- SIG_UNBLOCK:从blocked中删除set中的信号。
- SIG_SETMASK:blocked=set。
同步流以避免并发错误
以某种方式同步并发流,从而得到最大的可行的交错的集合,每个可行的交错都能得到正确的结果。
8.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);
8.7操作进程的工具
-
STRACE:打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹。
-
PS:列出当前系统中的进程(包括僵死的进程)。
-
TOP:打印出关于当前进程资源使用的信息。
-
PMAP:显示进程的存储器映射。
-
/proc:一个虚拟文件系统,以ASCII格式输出大量内核数据结构内容。
四、遇到的问题
1.练习题8.2 ,为什么子进程第一次输出的结果是2,如果x++语句执行了两次的话printf语句不是也同样执行了两次吗?为什么会先输出2再输出1?