信息安全系统设计基础第十一周学习总结
第八章 异常控制流
一、学习目标
-
了解异常及其种类
-
理解进程和并发的概念
-
掌握进程创建和控制的系统调用及函数使用:fork,exec,wait,waitpid,exit,getpid,getppid,sleep,pause,setenv,unsetenv,
-
理解数组指针、指针数组、函数指针、指针函数的区别
-
理解信号机制:kill,alarm,signal,sigaction
-
掌握管道和I/O重定向:pipe, dup, dup2
二、学习计时
学习计时 |
共6.5小时 |
读书 |
2小时 |
代码 |
1.5小时 |
作业 |
1小时 |
博客 |
2小时 |
三、教材第八章知识点总结
(一)异常
8.1.1 异常处理
1. 异常的定义
异常是控制流中的突变,用来响应处理器状态中的某些变化。
2. 异常处理程序完成处理后发生的情况
- 处理程序将控制返回给当前指令Icurr,即当事件发生时正在执行的命令。
- 处理程序将控制返回给Inext,即如果没有发生异常将会执行的下一条指令。
- 处理程序终止被中断的程序。
3.系统中可能的每种类型的异常都分配了一个唯一的非负整数的异常号。
4. 异常表的起始地址放在一个叫做异常表基址寄存器的特殊CPU寄存器里。
5. 异常表与过程调用的不同之处
- 过程调用时,在跳转到处理程序之前,处理器将返回地址压入栈中。而根据异常的类型,返回类型要么是当前指令,要么是下一条指令。
- 处理器也把一些额外的处理器状态压到栈里,在处理程序返回时,重新开始被中断的程序会需要这些状态。
- 如果控制从一个用户程序转移到内核,则这些所有项目都被压到内核栈中,而不是压到用户栈中。
- 异常处理程序运行在内核模式下,意味着它们对所有的系统资源都有完全的访问权限。
8.1.2 异常的类别
异常可分为四类:中断、陷阱、故障、终止。
1. 中断
- 中断是异步发生的,是来自处理器外部的I/O设备的信号的结果,不是由任何一条指令造成的。
- 硬件中断的异常处理程序通常称为中断处理程序。
- 剩下的异常类型陷阱、故障、终止是同步发生的,是执行当前指令的结果。我们把这类指令叫做故障指令。
2. 陷阱和系统调用
- 陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫系统调用。
- 系统调用和普通的函数调用在实现上的不同点:普通的函数运行在用户模式中,限制了函数可以执行的指令类型,且它们只能访问与调用函数相同的栈。系统调用运用在内核模式中,允许系统调用执行指令,并访问定义在内核中的栈。
3. 故障
故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能修正这个错误情况,它就将控制返回到引起故障的指令从而重新执行它。否则,处理程序返回到内核中的abort例程,abort例程会终止引起故障的应用程序。
4. 终止
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误。终止处理程序从不将控制返回给应用程序,处理程序将返回给一个abort例程,该例程会终止这个应用程序。
8.1.3 Linux/IA32系统的异常
1. Linux/IA32的故障和终止
- 除法错误:当应用试图除以0时,或者当一个除法指令的结果对于目标操作数太大的时候,就会发生除法错误(异常0)。
- 一般保护故障(异常13)
- 缺页(异常14)
- 机器检查(异常18)
2. Linux/IA32系统调用
将系统调用和它们相关联的包装函数称为系统级函数。
(二)进程
1. 进程:一个执行中的程序的实例。系统中每个程序都运行在某个进程的上下文中。
2. 进程提供给应用程序的关键抽象
- 一个独立的逻辑控制流
- 一个私有的地址空间
8.2.1 逻辑控制流
调用调试器单步执行程序时会看到一系列程序计数器(PC)的值,这些值唯一地对应于包含在程序的可执行目标文件中的指令。这个PC值的序列叫做逻辑控制流。
8.2.2 并发流
1. 并发的定义:多个流并发的执行的一般现象称为并发(一段时间内P1、P2交替运行)
2. 并行流:并发流的真子集,两个流并发地运行在不同的处理器核或计算机上,并行地运行且并行地执行。
3. 多任务:多个进程并发叫做多任务。
4. 并行:并发流在不同的cpu或计算机上,叫做并行。
8.2.3 私有地址空间
定义:进程也为每个程序提供一种假象,好像它独占地使用系统地址空间。在一台有 位地址的机器上,地祉空间是 个可能地址的集合 0, 1,…, -l. 一个进程为每个程序提供它自己的私有地址空间。
8.2.4 用户模式和内核模式
1. 处理器通常用某个控制寄存器中的一个模式位来提供这种功能,该寄存器描述了进程当前享有的特权。当设置了模式位时,进程就运行在内核模式中。(有时叫做超级用户模式)
2. 运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过异常。
3. linux提供了/proc文件系统,它允许用户模式进程访问内核数据结构的内容。
8.2.5 上下文切换
1. 上下文切换:操作系统内核使用叫上下文切换的异常控制流来实现多任务。
2. 上下文切换:
- 保存当前进程的上下文;
- 恢复某个先前被抢占的进程被保存的上下文;
- 将控制传递给这个新恢复的进程
3. 调度:内核中的调度器实现调度。
4. 当内核代表用户执行上下文切换时,可能会发生上下文切换。如果系统调用发生阻塞,那么内核可以让当前进程休眠,切换到另一个进程,如read系统调用,或者sleep会显示地请求让调用进程休眠。一般,即使系统调用没有阻塞,内核亦可以决定上下文切换,而不是将控制返回给调用进程。
5. 中断也可能引起上下文切换。如,定时器中断。
(三)系统调用错误处理
当Unix系统级函数遇到错误时,它们典型地会返回―1,并设置全局整数变量errno来表示什么出错了。程序员应该总是检查错误,但是不幸的是,许多人都忽略了错误检查,因为它使代码变得臃肿,而且难以读懂。比如,下面是我们调用Unix fork函数时会如何检查错误:
通过使用错误处理包装函数,我们可以更进一步地简化我们的代码。对于一个给定的基本函数foo,我们定义一个具有相同参数的包装函数Foo,但是第一个字母大写了。包装函数调用基本函数,检查错误,如果有任何问题就终止。比如,下面是fork函数的错误处理包装函数:
(四)进程控制
8.4.1 获取进程ID
1.每个进程都有一个唯一的正数(非零)进程 ID (PID). getpid 函数返回调用进程的 PID。getppid 画数返回它的父进程的 PID (创建调用进程的进程〉。
#include<sys/types.h>
#include<unistd.h>
pit_t getpid(void);//返回调用进程的PID
pit_t getppid(void);//返回它的父进程的PID
2. getpid 、getppid 函数返回一个类型为 pid_t 的整数值,在 Linux 系统上它在 types.h中被定义为 int。
8.4.2. 创建和终止进程
1.进程总是处于下面三种状态之-:
- 运行。进程要么在CPU上执行,要么在等待被执行且最终会被内核调度。
- 停止。进程的执行被挂起(suspend),且不会被调度。当收到SIGSTOP、SIGTSTP、SIDTTN或者 SIGTTOU 信号时,进程就停止,并且保持停止直到它收到一个 SIGCONT
信号,在这个时刻,进程再次开始运行。 -
终止。进程永远地停止了。进程会因为三种原因终止:
1) 收到一个信号,该信号的默认行为是终止进程。 2) 从主程序返回。 3) 调用 exit 函数。
2. 父进程通过调用fork函数创建一个新的运行子程序
#include<sys/types.h>
#include<unistd.h>
pid_t fork(void);
子进程返回0,父进程返回子进程的PID(大于0),出错返回-1。
注:父进程调用fork时,子进程可以读写父进程中打开的任何文件,父进程和新建的子进程最大的区别在于他们有不同的PID。
3. fork函数的特点
- 只被调用一次,返回两次。一次在调用父进程中,返回子进程的PID;一次在新建的子进程中,返回0。
- 并发执行:父进程和子进程是并发运行的独立进程,内核能以任意方式交替执行它们逻辑控制流中的指令。
- 相同但独立的地址空间。
- 共享文件
8.4.3 回收子进程
1. 回收:当一个进程终止时,内核并不立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收。
2. 僵死进程:一个终止了但是还未被回收的进程称为僵死进程。
3. 回收子进程的两种方法:
- 内核的init进程
- 父进程waitpid函数
如果父进程没有回收它的僵死子进程就终止了,那么内核就会安排init进城来回收它们。init进程的PID为1,并且是在系统初始化时创建的。
一个进程可以通过调用waitpid函数来等待它的子进程终止或停止。
4. waitpid函数有点复杂,默认地(当options=0时),waitpid挂起调用进程的执行,直到它的等待集合中的一个子进程终止。
- waitpid函数
①判断等待集合的成员
等待集合的成员是由参数 pid 来确定的:
如果 pid>0,那么等待集合就是一个单独的子进程,它的进程lD等于 pid
如果 pid = -1 ,那么等待集合就是由父进程所有的子进程组成的。
②修改默认行为
可以通过将 optioins 设置为常量 WNOHANG WUNTRAα 的各种组合,修改默认行为:
WNOHANG: 如果等待集合中的任何子进程都还没有终止,那么就立即返回(返回值为0)。默认的行为是挂起调用进程,直到有子进程终止。在等待子进程终止的同时,如果还想做些有用的工作,这个选项会有用。
WUNTRACED :挂起调用进程的执行,直到等待集合中的一个进程变成已终止或者被停止。返回的 PID 为导致返回的己终止或被停止子进程的 PID。默认的行为是只返回己终止的子进程。当你想要检查已终止和被停止的子进程时,这个选项会有用。
WNOHANG UNTRACED: 立即返回,如果等待集合中没有任何子进程被停止或已终止,那么返回值为 ,或者返回值等于那个被停止或者己终止的子进程的 PID 。
③检查已回收子进程的退出状态
如果 status 参数是非空的,那么 waitpid 就会在 status 参数中放上关于导致返回的子进程的状态信息。 wait.h 头文件定义了解释 status 参数的几个宏
WIFEXITED (status) :如果子进程通过调用 exit 或者一个返回 (return) 正常终止,就返回真。
WEXITSTATUS (status) 返回一个正常终止的子进程的退出状态。只有在 WIFEXITED返回为真时,才会定义这个状态。
WIFSIGNALED (status): 如果子进程是因为一个未被捕获的信号终止的,那么就返回真。
WTERMSIG (status): 返回导致子进程终止的信号的数量。只有在 WIFSIGNALED(status) 返回为真时,才定义这个状态。
WIFSTOPPED (status) :如果引起返回的子进程当前是被停止的,那么就返回真。
WSTOPSIG (status): 返回引起子进程停止的信号的数量。只有在 WIFSTOPPED(status) 返回为真时,才定义这个状态。
④错误条件
如果调用进程没有子进程,那么waitpid返回-1,并且设置 errno为ECHILD。如果waitpid函数被一个信号中断,那么它返回一1,并设置 errno为EINTR。
- wait函数
wait函数是waitpid函数的简单版本。 调用 wait(&status) 等价于调用 waitpid(-l &status , 0)
8.4.4 让进程休眠
1.sleep函数将一个进程挂起一段指定的时间。
如果请求的时间量已经到了,sleep返回0,否则返回还剩下的要休眠的秒数。后一种情况是可能的,如果因为sleep函数被一个信号中断而过早地返回。
2.pause函数让调用函数休眠,直到该进程收到一个信号。
8.4.5 加载并运行程序
1.execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境变量列表envp。只有当出现错误时,例如找不到filename,execve才会返回到调用程序。所以,与fork一次调用返回两次不同,execve调用一次并从不返回。
2. 参数中每个指针都指向一个参数串。按照惯例,argv[0]是可执行目标文件的名字。环境变量的列表是由一个类似的数据结构表示的。envp变量指向一个以null结尾的指针数组,其中每个指针指向个环境变量串,其中每个串都是形如“NAME=VALUE”的名字一值对。
8.4.6 利用fork和execve运行程序
1. 像Unix外壳和Web服务器这样的程序大量使用了fork和e×ecve函数。外壳是一个交互型的应用程序,它代表用户运行其他程序。最早的外壳是Sh程序,后面出现了一些变种,比如csh、tcsh、ksh和bash。外壳执行一系列的读/求值(readeaUte)步骤然后终止。
2. 如果builtin_command返回0,那么外壳创建一个子进程,并在子进程中执行所请求的程序。如果用户要求在后台运行该程序,那么外壳返回到循环的顶部,等待下一个命令行否则,外壳使用Waitpid函数等待作业终止。当作业终止时,外壳就开始下一轮迭代。注意这个简单的外壳是有缺陷的,因为它并不回收它的后台子进程。修改这个缺陷就要求使用信号。
(五)信号
1. 一种更高层次的软件形式的异常,称为unix信号,它允许进程中断其他进程。
2. 低层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程而言是不可见的。信号提供了一种机制,通知用户进程发生了这些异常。
8.5.1 信号术语
传送一个信号到目的进程是由两个步骤组成的:
1.发送信号。内核通过更新目的进程上下文中的某个状态,发送(递送)一个信号给目的进程。 发送信号可以有如下两种原因:
- 内核检测到一个系统事件。
- 一个进程调用了kill函数,显式地要求内核发送一个信号给目的进程,一个进程可以发送信号给它自己。
2.接收信号。当目的进程被内核强迫以某种方式的发送做出反应时,目的进程就接收了信号。进程可以忽略这个信号,终止或者通过执行一个称为信号处理程序的用户层函数不活这个信号。
- 一个只发出而没有被接收的信号叫做待处理信号。在任何时刻,一种类型至多只会有一个待处理信号。
- 一个进程可以有选择性地阻塞接收某种信号。当一种信号被阻塞时,他仍可以被发送,但是产生的待处理信号不会被接收,直到进程取消对这种信号的阻塞。
- 一个待处理信号最多只能被接收一次。
8.5.2 发送信号
1.进程组:每个进程都只属于一个进程组,进程组是由一个正整数进程组ID来标识的。 一个子进程和它的父进程同属于一个进程组,一个进程组可以通过使用setpgid函数来改变自己或者其他进程的进程组。
2.用/bin/kill程序发送信号:用/bin/kill程序可以向另外的进程发送任意的信号。
3.从键盘发送信号:从键盘发送信号外壳为每个作业创建一个独立的进程组。
4.用kill函数发送信号:进程通过调用kill函数发送信号给其他进程(包括它们自己)。
5.用alarm函数发送信号:进程可以通过调用alarm函数向他自己发送SIGALRM信号。
8.5.3 接收信号
1. 当内核从一个异常处理程序返回,准备将控制传递给进程P时,他会检查进程P的未被阻塞的处理信号的集合。如果这个集合为空,那么内核将控制传递到P的逻辑控制流中的下一条指令;如果集合是非空的,那么内核选择集合中的某个信号K(通常是最小的K0,并且强制P接收信号K。收到这个信号会触发进程的某种行为。一旦进程完成了这个行为,那么控制就传递回P的逻辑控制流中的下一条指令。
2. 每个信号类型都有一个预定的默认行为:
- 进程终止
- 进程终止并转储存储器
- 进程停止直到被SIGCONT型号重启
- 进程忽略该信号
3. signal函数可以通过下列三种方法之一来改变和信号signum相关联的行为:
- 如果handler是SIG_IGN,那么忽略类型为signum的信号
- 如果handler是SIG_DFL,那么类型为signum的信号行为恢复为默认行为
-
否则,handler就是用户定义的函数的地址,这个函数成为信号处理程序,只要进程接收到一个类型为signum的信号,就会调用这个程序,通过把处理程序的地址传递到signal函数从而改变默认行为,这叫做设置信号处理程序。
①当一个进程捕获了一个类型为K的信号时,为信号K设置的处理程序被调用,一个整数参数被设置为K。这个参数允许同一个处理函数捕获不同类型的信号。
②信号处理程序的执行中断main C函数的执行,类似于底层异常处理程序中断当前应用程序的控制流的方式,因为信号处理程序的逻辑控制流与主函数的逻辑控制流重叠,信号处理程序和主函数并发地运行。
8.5.4 信号处理问题
1.当一个程序要捕获多个信号时,一些细微的问题就产生了。
- 待处理信号被阻塞。Unix信号处理程序通常会阻塞当前处理程序正在处理的类型的待处理信号。
- 待处理信号不会排队等待。任意类型至多只有一个待处理信号。因此,如果有两个类型为K的信号传送到一个目的进程,而由于目的进程当前正在执行信号K的处理程序,所以信号K时阻塞的,那么第二和信号就简单地被简单的丢弃,他不会排队等待。
- 系统调用可以被中断。像read、wait和accept这样的系统调用潜在地会阻塞进程一段较长的时间,称为慢速系统调用。在某些系统中,当处理程序捕获到一个信号时,被中断的慢速系统调用在信号处理程序返回时不再继续,而是立即返回给用户一个错误的条件,并将errno设置为EINTR。
2.不可以用信号来对其他进程中发生的事件计较。
8.5.5 可移植的信号处理
Signal包装函数设置的信号处理程序的信号处理语义:
1. 只有这个处理程序当前正在处理的那种类型的信号被阻塞。
2.和所有信号实现一样,信号不会排队等候。
3.只要有可能,被中断的系统调用会自动重启。
4. 一旦设置了信号处理程序,它就会一直保持,知道signal带着handler参数为SIG_ IGN或者SIG_DFL被调用。
8.5.6 显式地阻塞和取消阻塞信号
8.5.7 同步流以避免讨厌的并发错误
1. 一般而言,流可能交错的数量是与指令的数量呈指数关系的。
2. 以某种方式同步并交流,从而得到最大的可行的交错的集合,每个可行的交错都能得到正确的结果。
3.如何编写读写相同存储位置的并发流程序的问题,困扰着数代计算机科学家。比如,竞争问题。
(六)非本地跳转
1.c语言提供了一种用户级异常控制流形式,称为本地跳转。通过setjmp和longjmp函数来提供。
2.setjmp函数只被调用一次,但返回多次:一次是当第一次调用setjmp,而调用环境保存在缓冲区env中时,一次是为每个相应的longjmp调用。另一方面,longjmp只调用一次,但从不返回。sig—函数是setjmp和longjmp函数的可以被信号处理程序使用的版本。
3.非本地跳转的一个重要应用就是允许从一个深层嵌套的函数调用中立即返回,通常是由检测到某个错误情况引起的。
非本地跳转的另一个重要应用是使一个信号处理程序分支到一个特殊的代码位置,而不是返回到达中断了的指令位置。
(七)操作进程的工具
Linux系统提供了大量的监控和操作进程的有用工具:
1. STRACE:打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹。对于好奇的的工具。用-StatiC编译你的程序,能传到一个更干净的、不带学生而言,这是一个令人着迷有大量与共享库相关的输出的轨迹。
2. PS:列出当前系统中的进程(包括僵死进程)
3. TOP:打印出关于当前进程资源使用的信息。
4. PMAP:显示进程的存储器映射。proc:一个虚拟文件系统,以ASCII文本格式输出大量内核数数据结构的内容,用户程序可 cat 2 / proc / load avg” , 观察在Linux系统上的平均负载。
(八)小结
-
异常控制流(ECF)发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制在硬件层,异常是由处理器中的事件触发的控制流中的突变。控制流传递给一个软件处理程序,该处理程序进行一些处理,然后返回控制给被中断的控制流。
-
有四种不同类型的异常:中断、故障、终止和陷阱。当一个外部旧设备,例如定时器芯片或者一个磁盘控制器,设置了处理器芯片上的中断引脚时(对于任意指令)中断会异步地发生控制返回到故障指令后面的那条指令。一条指令的执行可能导致故障和终止同时发生故障处理程序会重新启动故障指令,而终止处理程序从不将控制返回给被中断的流。最后,陷阱就像是用来实现向应用提供到操作系统代码的受控的入口点的系统调用的函数调用。
-
在操作系统层,内核用ECF提供进程的基本概念。进程提供给应用两个重要的抽象:(1)逻辑控制流,它提供给每个程序一个假象,好像它是在独占地使用处理器(2)私有地址空间,它提供给每个程序一个假象,好像它是在独占地使用主存。
-
在操作系统和应用程序之间的接口处,应用程序可以创建子进程,等待它们的子进程停止或者终止,运行新的程序,以及捕获来自其他进程的信号。信号处理的语义是微妙的,并且随系统不同而不同。然而,在与POSIX兼容的系统上存在着一些机制,允许程序清楚地指定期望的信号处理语义。
-
最后,在应用层,C程序可以使用非本地跳转来规避正常的调用/返回栈规则,并且直接从一个函数分支到另一个函数。
四、学习总结
这周的第八章学习内容主要是关于异常控制流(ECF),其中进程控制这部分内容,老师在上课时就已经讲解过。最特别的就是fork函数调用返回两次的性质,我通过阅读代码已经掌握了它的用法。虽然对于本章8.5.6节并发编程这一章的代码我还不太能完全理解,但是等学到第十二章时应该能进一步理解这个问题。本章内容与操作系统这门课的联系紧密,结合在一起能更好地融会贯通。今后也会定时温习这部分内容,及时巩固所学知识。最后,感谢娄老师的悉心教导!
五、参考资料
- 《深入理解计算机系统》
- 《嵌入式Linux应用程序开发标准教程》
- 课程资料:https://www.shiyanlou.com/courses/413 实验十,课程邀请码:W7FQKW4Y