信息安全系统设计基础第十一周学习总结
第八章 异常控制流
前言:
1、从给处理器加电开始,直到断电位置,程序计数器假设一个值的序列:a0,a1,...,an-1,其中,每个ak是某个相应地指令Ik的地址。每次从ak到ak+1的过渡称为控制转移。这样的控制转移序列叫做处理器的控制流。最简单的一种控制流是一个“平滑的”序列,其中每个Ik和Ik+1在存储器中都是相邻的。 2、系统通过使控制流发生突变来对系统状态的变化做出反应,这些突变称为异常控制流。 3、应用程序通过使用一个叫做陷阱或者系统调用的ECF形式,向操作系统请求服务。 4、操作系统为应用程序提供了强大的ECF机制,用来创建新进程、等待进程终止、同志其他进程系统中的异常事件,以及检测和相应这些事件。 5、ECF是计算机系统中实现并发的基本机制。终端应用程序、进程和线程执行的异常处理程序和终端应用程序执行的信号处理程序都是在运行中的并发的例子。 6、C++和Java是通过try、catch和throw语句来提供软件异常机制。软件异常允许程序进行非本地跳转(违反通常的调用/返回栈规则的跳转)来相应错误情况。非本地跳转是一种应用层ECF,在C中是通过setjmp和longjmp函数提供的。
8.1异常
需要知道的概念:
异常是异常控制流的一种形式,它一部分是由硬件实现的,一部分是由操作系统实现的。
异常就是控制流中的突变,用来相应处理器状态中的某些变化。
当处理器状态中发生一个重要的变化时,处理器正在执行某个当前指令Icurr。
在处理器中,状态被编码为不同的位和信号。
状态变化称为事件,事件可能和当前指令的执行直接相关。
在任何情况下,当处理器检测到有事件发生时,它就会通过一张叫做异常表的跳转表,进行一个间接过程调用(异常),到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序)。当异常处理程序完成处理后,根据引起异常的事件的类型,会发生以下三种情况中的一种:
①处理程序将控制返回给当前指令Icurr,即当事件发生时正在执行的指令。
②处理程序将控制返回给Inext,即如果没有发生异常将会执行的下一条指令。
③处理程序终止被中断的程序。
8.1.1 异常处理
系统中可能的每种类型的异常都分配了一个唯一的非负整数的异常号。其中一些号码是由处理器的设计者分配的,其他号码是由操作系统内核(操作系统常驻存储器的部分)的设计者分配的。前者的示例包括被零除、缺页、存储器访问违例、断点以及算术溢出。后者的示例包括系统调用和来自外部I/O设备的信号。
异常表的起始地址放在一个叫做异常在基址寄存器的特殊CPU 寄存器里。 异常类似于过程调用,但是有一些重要的不同之处。 过程调用时,在跳转到处理程序之前,处理器将返回地址压人找中。 处理器也把一些额外的处理器状态压到栈里,在处理程序返回时,重新开始被中断的程序会需要这些状态。 如果控制从一个用户程序转移到内核,那么所有这些项目都被压到内核栈中,而不是压到用户栈中。 异常处理程序运行在内核模式下,这意味着它们对所有的系统资源都有完全的访问权限。
8.1.2 异常的类别
异常可以分为四类:中断( interrupt)、陷阱 (trap)、故障 (fault) 和终止 (abort)。
1.中断
- 中断是异步发生的,是来自处理器外部的1/ 设备的信号的结果。
- 硬件中断的异常处理程序通常称为中断处理程序。
- 剩下的异常类型(陷阱、故障和终止〉是同步发生的,是执行当前指令的结果。我们把这类指令叫做故障指令。
2.陷阱和系统调用
陷阱是有意的异常,是执行一条指令的结果。就像中断处理程序一样,陷阱处理程序将控制返回到下一条指令。陷阱最重要的用途是在用户程序和内核之间提供一个像过程一样的接口,叫做系统调用。
3.故障
故障由错误情况引起,它可能能够被故障处理程序修正。当故障发生时,处理器将控制转移给故障处理程序。如果处理程序能够修正这个错误情况,它就将控制返回到引起故障的指令,从而重新执行它。否则,处理程序返回到内核中的 abort 例程, abort 例程会终止引起故障的应用程序。
4.终止
终止是不可恢复的致命错误造成的结果,通常是一些硬件错误,比如 DRAM 或者SRAM被损坏时发生的奇偶错误。终止处理程序从不将控制返回给应用程序。
8.1.3 Linux/IA32系统中的异常
1、Linux/IA32故障和终止:除法错误、一般保护、故障、缺页、机器检查
2、linuxllA32 系统调用:Linux 提供上百种系统调用,当应用程序想要请求内核服务时可以使用,包括读文件、写文件或是创建一个新进程。
8.2 进程
- 进程(操作系统层):逻辑控制流,私有地址空间,多任务,并发,并行,上下文,上下文切换,调度。
- 进程就是一个执行中的程序实例。系统中的每个程序都是运行在某个进程的上下文中的。
- 进程提供给应用程序的关键抽象:a)一个独立的逻辑控制流 ;b)一个私有的地址空间。
- 异常是允许操作系统提供进程 (process) 的概念所需要的基本构造块,进程是计算机科学中
最深刻最成功的概念之一。- 进程提供给应用程序的关键抽象:
- 一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器。
- 一个私有的地址空间,它提供一个假象,好像我们的程序独占地使用存储器系统。
8.2.1 逻辑控制流
- 如果想用调试器单步执行程序,我们会看到一系列的程序计数器 (PC)值,这些值唯一地对应于包含在程序的可执行目标文件中的指令,或者是包含在运行时动态链接到程序的共享对象中的指令。这个 PC值的序列叫做这辑控制流,或者简称逻辑流。
- 每个进程执行它的流的一部分,然后被抢占 (preempted) (暂时挂起),然后轮到其他进程。
8.2.2 并发流
并发流:并发流一个逻辑流的执行在时间上与另一个流重叠,叫做并行流
并发:多个流并发执行的一般现象称为并发。
多任务:多个进程并发叫做多任务。
并行:并发流在不同的cpu或计算机上,叫做并行。
8.2.3 私有地址空间
定义:进程也为每个程序提供一种假象,好像它独占地使用系统地址空间。在一台有 位地址的机器上,地祉空间是 个可能地址的集合 0, 1,…, -l. 一个进程为每个程序提供它自己的私有地址空间。
8.2.4 用户模式和内核模式
运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过异常。
linux提供了/proc文件系统,它允许用户模式进程访问内核数据结构的内容。
8.2.5 上下文切换
上下文切换:操作系统内核使用叫上下文切换的异常控制流来实现多任务。 上下文切换:a)保存当前进程的上下文;b)恢复某个先前被抢占的进程被保存的上下文; c)将控制传递给这个新恢复的进程 调度:内核中的调度器实现调度。 当内核代表用户执行上下文切换时,可能会发生上下文切换。如果系统调用发生阻塞,那么内核可以让当前进程休眠,切换到另一个进程,如read系统调用,或者sleep会显示地请求让调用进程休眠。一般,即使系统调用没有阻塞,内核亦可以决定上下文切换,而不是将控制返回给调用进程。 中断也可能引起上下文切换。如,定时器中断。
8.4.6 利用fork和execve运行程序
1.像Unix外壳和Web服务器这样的程序大量使用了fork和e×ecve函数。外壳是一个交互型的应用程序,它代表用户运行其他程序。最早的外壳是Sh程序,后面出现了一些变种,比如csh、tcsh、ksh和bash。外壳执行一系列的读/求值(readeaUte)步骤然后终止。 2.如果builtin_command返回0,那么外壳创建一个子进程,并在子进程中执行所请求的程序。如果用户要求在后台运行该程序,那么外壳返回到循环的顶部,等待下一个命令行否则,外壳使用Waitpid函数等待作业终止。当作业终止时,外壳就开始下一轮迭代。注意这个简单的外壳是有缺陷的,因为它并不回收它的后台子进程。修改这个缺陷就要求使用信号,我们将在下一节中讲述信号。
8.5 信号
1.一种更高层次的软件形式的异常,称为unix信号,它允许进程中断其他进程。 2.低层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程而言是不可见的。信号提供了一种机制,通知用户进程发生了这些异常。
8.5.1 信号术语
传送一个信号到目的进程是由两个步骤组成的:
1.发送信号。内核通过更新目的进程上下文中的某个状态,发送(递送)一个信号给目的进程。 发送信号可以有如下两种原因: 1)内核检测到一个系统事件。 2)一个进程调用了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.每个信号类型都有一个预定的默认行为:
(1)进程终止
(2)进程终止并转储存储器
(3)进程停止直到被SIGCONT型号重启
(4)进程忽略该信号
3.signal函数可以通过下列三种方法之一来改变和信号signum相关联的行为:
(1)如果handler是SIG_IGN,那么忽略类型为signum的信号
(2)如果handler是SIG_DFL,那么类型为signum的信号行为恢复为默认行为
(3)否则,handler就是用户定义的函数的地址,这个函数成为信号处理程序,只要进程接收到一个类型为signum的信号,就会调用这个程序,通过把处理程序的地址传递到signal函数从而改变默认行为,这叫做设置信号处理程序。
①当一个进程不活了一个类型为K的信号时,为信号K设置的处理程序被调用,一个整数参数被设置为K。这个参数允许同一个处理函数捕获不同类型的信号。 ②信号处理程序的执行中断main C函数的执行,类似于底层异常处理程序中断当前应用程序的控制流的方式,因为信号处理程序的逻辑控制流与主函数的逻辑控制流重叠,信号处理程序和主函数并发地运行。
8.5.4 信号处理问题
1.当一个程序要捕获多个信号时,一些细微的问题就产生了。
(1)待处理信号被阻塞。Unix信号处理程序通常会阻塞当前处理程序正在处理的类型的待处理信号。
(2)待处理信号不会排队等待。任意类型至多只有一个待处理信号。因此,如果有两个类型为K的信号传送到一个目的进程,而由于目的进程当前正在执行信号K的处理程序,所以信号K时阻塞的,那么第二和信号就简单地被简单的丢弃,他不会排队等待。
(3)系统调用可以被中断。像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.如何编写读写相同存储位置的并发流程序的问题,困扰着数代计算机科学家。比如,竞争问题。
8.6 非本地跳转
1.c语言提供了一种用户级异常控制流形式,称为本地跳转。通过setjmp和longjmp函数来提供。
2.setjmp函数只被调用一次,但返回多次:一次是当第一次调用setjmp,而调用环境保存在缓冲区env中时,一次是为每个相应的longjmp调用。另一方面,longjmp只调用一次,但从不返回。sig—函数是setjmp和longjmp函数的可以被信号处理程序使用的版本。
3.非本地跳转的一个重要应用就是允许从一个深层嵌套的函数调用中立即返回,通常是由检测到某个错误情况引起的。
非本地跳转的另一个重要应用是使一个信号处理程序分支到一个特殊的代码位置,而不是返回到达中断了的指令位置。
程序输出结果如下:
8.7 操作进程的工具
Linux系统提供了大量的监控和操作进程的有用工具:
①STRACE:打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹。对于好奇的的工具。用-StatiC编译你的程序,能传到一个更干净的、不带学生而言,这是一个令人着迷有大量与共享库相关的输出的轨迹。 ②PS:列出当前系统中的进程(包括僵死进程) ③TOP:打印出关于当前进程资源使用的信息。 ④PMAP:显示进程的存储器映射。proc:一个虚拟文件系统,以ASCII文本格式输出大量内核数数据结构的内容,用户程序可 cat 2 / proc / load avg” , 观察在Linux系统上的平均负载。
8.8 小结
1.异常控制流(ECF)发生在计算机系统的各个层次,是计算机系统中提供并发的基本机制在硬件层,异常是由处理器中的事件触发的控制流中的突变。控制流传递给一个软件处理程序,该处理程序进行一些处理,然后返回控制给被中断的控制流。
2.有四种不同类型的异常:中断、故障、终止和陷阱。当一个外部旧设备,例如定时器芯片或者一个磁盘控制器,设置了处理器芯片上的中断引脚时(对于任意指令)中断会异步地发生控制返回到故障指令后面的那条指令。一条指令的执行可能导致故障和终止同时发生故障处理程序会重新启动故障指令,而终止处理程序从不将控制返回给被中断的流。最后,陷阱就像是用来实现向应用提供到操作系统代码的受控的入口点的系统调用的函数调用。
3.在操作系统层,内核用ECF提供进程的基本概念。进程提供给应用两个重要的抽象:(1)逻辑控制流,它提供给每个程序一个假象,好像它是在独占地使用处理器(2)私有地址空间,它提供给每个程序一个假象,好像它是在独占地使用主存。
4.在操作系统和应用程序之间的接口处,应用程序可以创建子进程,等待它们的子进程停止或者终止,运行新的程序,以及捕获来自其他进程的信号。信号处理的语义是微妙的,并且随系统不同而不同。然而,在与POSIX兼容的系统上存在着一些机制,允许程序清楚地指定期望的信号处理语义。
5.最后,在应用层,C程序可以使用非本地跳转来规避正常的调用/返回栈规则,并且直接从一个函数分支到另一个函数。
==========================================================================================
习题记录:
练习题 8.6:编写一个叫做myecho的程序,它打印出它的命令行参数和环境变量。
#include <unistd.h> #include <sys/types.h> #include <stdlib.h> #include <stdio.h> int main(int argc, char *argv[], char *envp[]) { printf("Command line arguments:\n"); for (int i = 0; i < argc; ++ i) printf(" argv[%d]: %s\n", i, argv[i]); printf("Enviroment variables:\n"); for (int i = 0; envp[i]; ++ i) printf(" envp[%d]: %s\n", i, envp[i]); return 0; }
练习题 8.7:编写名为snooze的程序,有一个命令行参数,使用该参数调用练习题8.5中的snooze函数,然后终止。编写程序,使得用户可以通过在键盘上输入 crtl-c
中断snooze函数。
// snooze.c #include <sys/types.h> #include <unistd.h> #include <stdio.h> #include <signal.h> #include <stdlib.h> #include <errno.h> #include <string.h> extern int errno; unsigned int snooze(unsigned int secs); void handler(int sig); int main(int argc, char *argv[]) { unsigned int rest_seconds = 0; unsigned int secs = argv[1][0] - '0'; if (signal(SIGINT, handler) == SIG_ERR) { fprintf(stderr, "signal error: %s\n", strerror(errno)); exit(0); } rest_seconds = snooze(secs); printf("User hits crtl-c after %u seconds\n", secs - rest_seconds); return 0; } unsigned int snooze(unsigned int secs) { int rest_seconds = sleep(secs); printf("Sleep for %u of %u seconds\n", secs - rest_seconds, secs); return rest_seconds; } void handler() {}
练习题8.20:使用execve编写一个名为myls的程序,该程序的行为和 /bin/ls 程序一样。
// myls.c #include <unistd.h> #include <sys/types.h> #include <stdlib.h> extern char **environ; int main(int argc, char *argv[]) { execve("/bin/ls", argv, environ); exit(0); }
练习题 8.22
// mysystem.c #include <unistd.h> #include <sys/types.h> #include <stdlib.h> #include <stdio.h> #include <errno.h> extern int erron; extern char **environ; extern int EINTP; int mysystem(char *command) { pid_t pid; int status; if (command == NULL) return -1; if ((pid = fork()) == -1) return -1; if (pid == 0) { char *argv[4]; argv[0] == "sh"; argv[1] == "-c"; argv[2] == command; argv[3] == NULL; execve("bin/sh", argv, environ); exit(-1); // control should never come here } while (1) { if (waitpid(pid, &status, 0) == -1) { if (errno != EINTR) exit(-1); } else { if (WIFEXITED(status)) return WEXITSTATUS(status); else return status; } } }
习题8.24
// 8.24.h #include <stdio.h> #include <unistd.h> #include <string.h> #include <sys/types.h> #include <sys/wait.h> #include <error.h> #include <signal.h> extern int errno; extern int ECHILD; extern void psignal(int signal, const char *str); #define NCHILDREN 2 #define MAXLINE 80 char buf[MAXLINE]; int main() { int status; pid_t pid; for (int i = 0; i < NCHILDREN; ++ i) { pid = fork(); if (pid == 0) *(char *)main = 1; } while (pid = wait(&status) > 0) { if (WIFEXITED(status)) printf("child %d terminated normally with exit status = %d\n", pid, WEXITSTATUS(status)); else if (WIFSIGNALED(status)) { sprintf(buf, "child %d terminated by signal %d: ", pid, WTERMSIG(status)); psignal(WTERMSIG(status), buf); } } if (errno != ECHILD) { fprintf(stderr, "%s: %s\n", "wait error", strerror(errno)); exit(0); } return 0; }
习题8.25 编写fgets函数的一个版本tfgets,他5秒中后就会超时。tfgets 函数接收和 fgets 相同的参数。如果用户在5秒内不键入一个输入行,tfgets返回NULL。
否则,返回一个指向输入行的指针。
// tfgets.c #include <unistd.h> #include <sys/types.h> #include <signal.h> #include <string.h> #include <setjmp.h> #include <stdio.h> #include <stdlib.h> static sigjmp_buf env; void handler(int signal) { alarm(0); longjmp(env, 1); } char *tfgets(char *buffer, int buffer_size, FILE *stream) { signal(SIGALRM, handler); alarm(5); if (!sigsetjmp(env, 1)) return fgets(buffer, buffer_size, stream); else return NULL; } int main() { char *str; char buffer[100]; while (1) { if (tfgets(buffer, sizeof(buffer),stdin) != NULL) printf("read: %s", buffer); else printf("time out\n"); } exit(0); }
遇到的问题及解决方法:
本周的代码挺多的。还有些难。我遇到的问题就是根据代码理解函数功能这一部分。还将继续加强。
心得体会:
这次的内容就是第八章的内容。与课上老师说过的内容很多重复的内容。因为老师的强调,我就更加认真去看了这部分的内容,也接了一个和这一章内容有关的实践项目。这学期还有一门操作系统的必修课,学到了很多与进程有关的知识。我想通过这一章的学习和实践项目的学习巩固这部分知识并且做到学科间融合。但是这一章的好几个函数的运行和原理还是需要多多琢磨,期间遇到了很多的问题都是自己的理解不够透彻。但是这些都是计算机操作系统的重点,博客行文至此知识一个阶段的学习,将来还会时常学习这部分,做到温故而知新。
参考文献:
1.《深入理解计算机系统》pdf
2.《操作系统》教材、PPT
3.习题解析:http://www.lxway.com/489660494.htm
4.内容总结:http://www.lxway.com/126892501.htm
5.课程资料:https://www.shiyanlou.com/courses/413 实验十,课程邀请码:W7FQKW4Y