信息安全系统设计基础第十周学习总结
第八章 异常控制流
现代系统通过使控制流发生突变来对某些情况做出反应,我们把这些突变称作异常控制流。
- 硬件层:硬件检测到的事件会触发控制突然转移到异常处理程序
- 操作系统层:内核通过上下文转换将控制从一个用户进程转移到另一个用户进程。
- 应用层:一个进程可以发送信号到另一个进程,而接收者会将控制突然转移到它的一个信号处理程序
第一节 异常
异常是控制流中的突变,它一部分是由硬件实现的,另一部分是由操作系统实现的。
在任何情况下,当处理器检测到有事发生,他就会通过一张叫异常表的跳转表,进行一个间接的过程调用,到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序)。
当异常处理程序完成处理后,根据引起异常的事件类型,会发生以下情况:
- 处理程序将控制返回给当前命令
- 处理程序将控制返回给Inext,即如果没有发生异常将会执行的下一条指令
- 处理程序终止被中断的程序
异常号:系统中可能的每种类型的异常都分配了一个唯一的非负整数的异常号
异常表基址寄存器:异常号是到异常表中的索引,异常表的起始地址放在一个叫做异常表基址寄存器的特殊CPU寄存器里
异常的类别:
-
中断:异步发生,来自处理器外部的I/O设备的信号的结果,将控制返回给下一条指令
-
陷阱:陷阱是有意的异常,是执行一条指令的结果,最重要的用途:在用户和内核间提供一个像过程一样的接口,叫系统调用
-
故障:由错误状况引起,可能能够被故障处理程序修正,故障发生时,处理器将控制转移给故障处理程序,如果能够修正,返回引起故障的指令,重新执行指令,否则返回abort例程,终止
-
终止:是不可恢复的致命错误造成的结果,通常是一些硬件错误,终止示例:将控制返回abort例程
Linux/IA32系统中的异常
- Linux/IA32故障和终止:除法错误、一般保护、故障、缺页、机器检查
- linuxllA32 系统调用:Linux 提供上百种系统调用,当应用程序想要请求内核服务时可以使用,包括读文件、写文件或是创建一个新进程
第二节 进程
在操作系统层:逻辑控制流,私有地址空间,多任务,并发,并行,上下文,上下文切换,调度
进程就是一个执行中的程序实例
系统中的每个程序都是运行在某个进程的上下文中的
进程提供给应用程序的关键抽象:
- 一个独立的逻辑控制流
- 一个私有的地址空间
进程提供给应用程序的关键抽象:
- 一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器
- 一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用存储器系统
逻辑控制流
一系列的程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或者是包含在运行时动态链接到程序的共享对象中的指令,这个PC值的序列就叫做逻辑控制流,或者简称逻辑流
进程是轮流使用处理器的。每个进程执行它的流的一部分,然后被抢占(暂时挂起),然后轮到其他进程
对于运行在改程序上下文的其他程序,它看上去在独占的使用处理器
并发流
- 发流:并发流一个逻辑流的执行在时间上与另一个流重叠,叫做并行流
- 并发:多个流并发执行的一般现象称为并发
- 多任务:多个进程并发叫做多任务
- 并行:并发流在不同的cpu或计算机上,叫做并行
私有地址空间
定义:进程也为每个程序提供一种假象,好像它独占地使用系统地址空间
运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过异常
用户模式和内核模式
需要限制一个应用可以执行的指令以及可访问的地址空间范围来实现进程抽象,通过特定控制寄存器的一个模式位来提供这种机制。
- 设置了模式位时,进程运行在内核模式中,进程可以执行任何指令和访问任何存储器位置
- 没设置模式位时,进程运行在用户模式中,进程不允许执行特权指令和访问地址空间中内核区内的代码和数据
用户程序必须通过系统调用接口间接地访问内核代码和数据
用户程序的进程初始是在用户模式中的,必须通过中断、故障或陷入系统调用这样的异常来变为内核模式
上下文切换
上下文切换:操作系统内核使用叫上下文切换的异常控制流来实现多任务。
上下文切换机制:
- 保存当前进程的上下文
- 恢复某个先前被抢占的进程被保存的上下文
- 将控制传递给这个新恢复的进程
当内核代表用户执行上下文切换时,可能会发生上下文切换
如果系统调用发生阻塞,那么内核可以让当前进程休眠,切换到另一个进程,如read系统调用,或者sleep会显示地请求让调用进程休眠
一般,即使系统调用没有阻塞,内核亦可以决定上下文切换,而不是将控制返回给调用进程
第三节 系统调用错误处理
系统级函数遇到错误时,通常返回-1,并设置全局变量 errno
第四节 进程控制
获取进程ID
每个进程都有一个唯一的正数(非零)进程 ID (PID). getpid 函数返回调用进程的 PID。getppid 画数返回它的父进程的 PID (创建调用进程的进程〉
getpid getppid 函数返回一个类型为 pid_t 的整数值,在 Linux 系统上它在 types.h中被定义为 int
创建和终止进程
程的三种状态:
- 运行:要么在CPU上执行,要么在等待被执行,且最终被内核调度
- 停止:进程的执行被挂起,且不会被调度。收到 SIGSTOP 、 SIGTSTP 、 SIDTTIN 、 SIGTTOU 信号,进程停止,收到 SIGCONT 信号,进程再次开始运行
- 终止:永远停止。原因可能是:收到终止进程的信号,从主程序返回,调用 exit 函数
fork 函数:
- 调用一次,返回两次
- 父子进程是并发运行的,不能假设它们的执行顺序
3.两个进程的初始地址空间相同,但是是相互独立的 - 它们还共享打开的文件
因为有相同的程序代码,所以如果调用 fork 三次,就会有八个进程
回收子进程
当一个进程终止时,内核并不立即把它从系统中清除
相反,进程被保持在一种已终止的状态中,直到被它的父进程回收
僵死进程:一个终止了但是还未被回收的进程称为僵死进程
回收子进程的两种方法:1,内核的init进程 2,父进程waitpid函数
- 如果父进程没有回收它的僵死子进程就终止了,那么内核就会安排init进城来回收它们。init进程的PID为1,并且是在系统初始化时创建的
- 一个进程可以通过调用waitpid函数来等待它的子进程终止或停止
判断等待集合的成员
等待集合的成员由参数pid来确定:
- 如果pid>0:等待集合是一个单独子进程,进程ID等于pid
- 如果pid=-1:等待集合是由父进程所有的子进程组成
修改默认行为
将options设置为常量WNOHANG和WUNTRACED的各种组合,修改默认行为:
- 检查已回收子进程的退出状态——status
- 让进程休眠
sleep函数使一个进程挂起一段指定的时间
pause函数让调用函数休眠
加载并运行程序
execve函数加载并运行:
- 可执行目标文件filename
- 带参数列表argv
- 环境变量列表envp
只有当出现错误时,例如找不到filename,execve才会返回到调用程序,所以,与fork一次调用返回两次不同,execve调用一次并从不返回
参数中每个指针都指向一个参数串:
- argv[0]是可执行目标文件的名字
- 环境变量的列表是由一个类似的数据结构表示的
- envp变量指向一个以null结尾的指针数组,其中每个指针指向个环境变量串,其中每个串都是形如“NAME=VALUE”的名字一值对
第五节 信号
底层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程而言是不可见的。
其他信号对应于内核或者其他用户进程中较高层的软件事件。
信号术语
发送信号的两个不同步骤:
- 发送信号:内核通过更新目的进程上下文中的某个状态,发送(递送)一个信号给目的进程。
发送信号的两个原因: - 内核监测到一个系统事件,比如被零除错误或者子进程终止
- 一个进程调用了kill函数,显式地要求内核发送一个信号给目的进程。一个进程可以发送信号给它自己
- 接收信号:信号处理程序捕获信号的基本思想
待处理信号:一个只发出而没有被接收的信号
一个进程可以有选择性地阻塞接收某种信号
待处理信号不会被接收,直到进程取消对这种信号的阻塞
一个待处理信号最多只能被接受一次,pending位向量:维护着待处理信号集合,blocked向量:维护着被阻塞的信号集合
发送信号
进程组
每个进程都只属于一个进程组,进程组是由一个正整数进程组ID来标识的
getpgrp函数返回当前进程的进程组ID:默认地,一个子进程和它的父进程同属于一个进程组。
用/bin/kill/程序发送信号 一个为负的PID会导致信号被发送到进程组PID中的每个进程。
从键盘发送信号:
作业:表示对一个命令行求值而创建的进程。外壳为每个作业创建一个独立的进程组。
用kill函数发送信号 :
进程通过调用kill函数发送信号给其他的进程。父进程用kill函数发送SIGKILL信号给它的子进程。
用alarm函数发送信号 :
在任何情况下,对alarm的调用都将取消任何待处理的闹钟,并且返回任何待处理的闹钟在被发送前还剩下的秒数。
接收信号
当内核从一个异常处理程序返回,准备将控制传递该进程p时,它会检查进程p的未被阻塞的待处理信号的集合。如果这个集合是非空的,那么内核选择集合中的某个信号k,并且强制p接收信号k。
进程可以通过使用signal函数修改和信号相关联的默认行为。 唯一例外是SIGSTOP和SIGKILL,它们的默认行为是不能被修改的。
#include <signal.h>
typedef void (*sighandler_t)(int);
sighandler_t signal(int signum,sighandler_t handler);
//返回:若成功,返回指向前次处理程序的指针;若出错,为SIG_ERR
signal函数改变和信号signum相关联的行为的三种方法:
- handler是SIG_ IGN,忽略类型为signum的信号
- handler是SIG_ DFL,类型为signum的信号行为恢复为默认行为。
- 否则,handler就是用户定义的函数地址。这个函数称为信号处理程序。
- 设置信号处理程序:通过把处理程序的地址传递到signal函数从而改变默认行为。
捕获信号:调用信号处理程序。
处理信号:执行信号处理程序。
因为信号处理程序的逻辑控制流与主函数的逻辑控制流重叠,信号处理程序和主函数并发执行。
信号处理问题
- 待处理信号被阻塞:
- 待处理信号不会排队等待;
- 注意:不可以用信号来对其他进程中发生的事件计数。
可移植的信号处理
信号处理语义的差异,是UNIX信号处理的一个缺陷。
显式地阻塞和取消阻塞信号
sigprocmask函数改变当前已阻塞信号的信号。
how的值:
- SIG_ BLOCK :添加set中的信号到blocked中
- SIG_ UNBLOCK:从blocked中删除set中的信号
- SIG_ SETMASK:blocked = set
同步流以避免讨厌的并发错误
基本的问题是以某种方式同步并发流,从而得到最大的可行的交错的集合,每个可行的交错都能得到正确的结果。
非本地跳转
- 非本地跳转:不需要经过正常的调——返回序列。非本地跳转是通过setjmp和longjmp函数来提供的。
- setjmp函数在env缓冲区中保存当前调用环境,以供后面longjmp使用,并返回0,调用环境包括程序计数器、栈指针和通用目的寄存器。
- setjmp函数和longjmp函数的区别:
setjmp函数只被调用一次,但返回多次
当第一次调用setjmp,而调用环境保存在缓冲区env中时;
一次是为每个相应的longjmp调用 ;
另一方面,longjmp函数被调用一次,但从不返回。
非本地跳转的应用:
- 允许从一个深层嵌套的函数调用中立即返回,通常是由检测到某个错误情况引起的
- 一个信号信息处理程序分支到一个特殊的代码位置,而不是返回到被信号到达中断了的指令的位置。
对sigsetjmp函数的初始调用保存调用环境和信号的上下文。
操作进程的工具
异常控制流发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制
- 在硬件层,异常是由处理器中的事件触发的控制流中的突变。
返回到故障指令后面的那条指令。一条指令的执行可能导致故障和终止同时发生。 - 在操作系统层,内核用ECF提供进程的基本概念。
在应用层,C程序可以使用非本地跳转来规避正常的调用/返回栈规则,并且直接从一个函数分支到另一个函数。
参考资料
1、《深入理解计算机系统》课本 第八章
2、实验楼实验指导书:https://www.shiyanlou.com/courses/413 实验
3、每周重点:http://group.cnblogs.com/topic/73069.html