2017-2018-1 20155216 《信息安全系统设计基础》第十三周学习总结
异常
异常是控制流中的突变,用来响应处理器状态中的某些变化。
控制流:控制转移序列。
控制转移:从一条指令到下一条指令。
异常控制流:现代操作系统通过使控制流发生突变来对系统状态做出反应,这些突变称为异常控制流。
1、处理程序将控制返回给当前指令Icurr,即当事件发生时正在执行的指令。
2、处理程序将控制返回给Inext,如果没有发生异常将会执行的下一条指令。
3、处理程序终止被中断的程序。
异常处理
异常表:当处理器检测到有事件发生时,它会通过跳转表,进行一个间接过程调用(异常),到异常处理程序。
异常号:系统中可能的某种类型的异常都分配了一个唯一的非负整数的异常号。异常号是到异常表中的索引。
一旦硬件触发了异常,异常处理程序则由软件完成。
异常的类别
中断处理:异步是指硬件中断不是由任何一条指令造成的,而是由外部I/O设备的事件造成的。
陷阱和系统调用:系统调用是一些封装好的函数,内部通过指令int n实现。
陷阱最重要的用途是提供系统调用。系统调用运行在内核模式中,并且可以访问内核中的栈。
系统调用的参数是通过通用寄存器而不是栈来传递的,如,%eax存储系统调用号,%ebx,%ecx,%edx,%esi,%edi,%ebp最多存储六个参数,%esp不能用,因为进入内核模式后,会覆盖掉它。
故障处理:根据故障是否能够被修复,故障处理程序要么重新执行引起故障的指令,要么终止。
终止处理:是不可恢复的致命错误造成的结果,通常是一些硬件错误。终止处理程序将控制传递给一个内核abort例程,该例程会终止这个应用程序。
Linux系统调用示例:
进程
进程是一个执行中程序的实例。系统中每个程序都是运行在某个进程的上下文中的。上下文由程序正确运行所需的状态组成,包括程序的存放在存储器中的代码和数据、栈、通用目的寄存器的内容、程序计数器、环境变量和打开文件描述符的集合。
进程(操作系统层):逻辑控制流,私有地址空间,多任务,并发,并行,上下文,上下文切换,调度。
进程就是一个执行中的程序实例。系统中的每个程序都是运行在某个进程的上下文中的。
进程提供给应用程序的关键抽象:
a) 一个独立的逻辑控制流 ;
b)一个私有的地址空间
逻辑控制流
程序计数器(PC)值的序列叫做逻辑控制流,简称逻辑流。如下图所示,处理器的一个物理控制流分成了三个逻辑流,每个进程一个。
并发流
并发流:并发流一个逻辑流的执行在时间上与另一个流重叠。
并发:多个流并发执行的一般现象称为并发。
多任务:多个进程并发叫做多任务。
并行:并发流在不同的cpu或计算机上,叫并行。
练习题8.1
逻辑流在时间上和其他逻辑流重叠的进程称为并发进程。
A,B和B,C执行时间上有重合,说明它们各自的执行是重叠的,所以是并发的。 A,C执行时间无重合,所以不是并发。进程执行控制流的一部分的时间段称为时间片,进程和其他进程轮换运行称为多任务,也称时间分片。
私有地址空间
一个进程为每个程序提供它自己的私有地址空间。
运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过异常。
linux提供了/proc文件系统,它允许用户模式进程访问内核数据结构的内容。
进程为每个程序提供私有地址空间,和这个空间中某地址相关联的存储器字节不能被其他进程读写。和私有地址空间关联的存储器内容一般不同,但空间有相同的结构。
用户模式和内核模式
需要限制一个应用可以执行的指令以及可访问的地址空间范围来实现进程抽象,通过特定控制寄存器的一个模式位来提供这种机制。设置了模式位时,进程运行在内核模式中,进程可以执行任何指令和访问任何存储器位置。没设置模式位时,进程运行在用户模式中,进程不允许执行特权指令和访问地址空间中内核区内的代码和数据。用户程序必须通过系统调用接口间接地访问内核代码和数据。
用户程序的进程初始是在用户模式中的,必须通过中断、故障或陷入系统调用这样的异常来变为内核模式。
Linux有一种 /proc 文件系统,包含内核数据结构的内容的可读形式,运行用户模式进程访问。
上下文切换
内核为每个进程维持一个上下文,它是内核重新启动一个被抢占进程所需的状态。包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构(页表、进程表和文件表等)的值。
内核通过上下文切换来实现多任务,它是一种高级的异常控制流,建立在低级异常机制上。
内核决定抢占当前进程,重新开始一个先前被抢占的进程,称为调度了一个新进程,由内核中的调度器代码处理。使用上下文切换来将控制转移到新进程。上下文切换保存当前进程的上下文,恢复先前被抢占进程保存的上下文,将控制传递给新恢复的进程。系统调用和中断可以引发上下文切换。
上下文切换:操作系统内核使用叫上下文切换的异常控制流来实现多任务。
上下文切换:
a) 保存当前进程的上下文;b) 恢复某个先前被抢占的进程被保存的上下文;
c) 将控制传递给这个新恢复的进程。
调度:内核中的调度器实现调度。
当内核代表用户执行上下文切换时,可能会发生上下文切换。如果系统调用发生阻塞,那么内核可以让当前进程休眠,切换到另一个进程,如read系统调用,或者sleep会显示地请求让调用进程休眠。一般,即使系统调用没有阻塞,内核亦可以决定上下文切换,而不是将控制返回给调用进程。
系统调用错误处理
在Linux中,可以使用 man syscalls 查看全部系统调用的列表。系统级函数遇到错误时,通常返回-1,并设置全局变量 errno。
strace 命令可以打印程序和它的子进程调用的每个系统调用的轨迹。
进程控制
进程有三种状态:
运行:进程在CPU上执行,或等待被执行(会被调度)。
停止:进程被挂起(不会被调度)。收到 SIGSTOP 、 SIGTSTP 、 SIDTTIN 、 SIGTTOU 信号,进程停止,收到 SIGCONT 信号,进程再次开始运行。
终止:进程永远停止。原因可能是:收到终止进程的信号,从主程序返回,调用 exit 函数。
创建新进程可以使用 fork 函数。新创建的子进程和父进程几乎相同,它获得父进程用户级虚拟地址空间和文件描述符的副本,主要区别是它们的PID不同。 fork 函数调用一次,返回两次;父子进程是并发运行的,不能假设它们的执行顺序;两个进程的初始地址空间相同,但是是相互独立的;它们还共享打开的文件。
练习题8.2
子进程输出:
P1:x=2 P2:x=1 父进程输出: P2:x=0进程终止时,并不会被立即清除,而是等待父进程回收,称为僵死进程。父进程回收终止的子进程时,内核将子进程退出状态传给父进程,然后抛弃该进程。如果回收前父进程已经终止,那么僵死进程由 init 进程回收。
回收子进程可以用 wait 和 waitpid 等函数。
练习题8.3
输出序列为:bacc acbc abcc
内核调用 execve 函数在当前进程的上下文中加载并运行一个新程序。 execve 加载 filename 后,调用启动代码,启动代码准备栈,将控制传给新程序的主函数 int main(int argc, char argv[], char envp[])。
练习题8.4
输出6行
参数数组和环境数组会被传递给程序。按C标准, main 函数只有两个参数,一般环境数组使用全局变量environ 传递。
练习题8.5
操作环境数组可以通过 getenv 函数族。
程序是代码和数据的集合,可以作为目标模块存在于磁盘,或作为段存在于地址空间中。进程是执行中程序的一个实例。程序总是运行在某个进程的上下文中。
ps 命令可以查看系统中当前的进程。
top 命令会打印当前进程资源使用的信息。
练习题8.6
信号
信号是一种更高层软件形式的异常,它允许进程中断其他进程。一个信号即一条信息,通知进程一个某种类型的事件已经在系统中发生了。
每种信号类型都对应某个类型的系统事件。底层硬件异常通常对用户进程不可见,信号提供了一种机制向用户进程通知这些异常的发生。其他信号对应内核或其他用户进程中较高层的软件事件。
发送信号指内核通过更新目的进程上下文中的某个状态,发送一个信号给目的进程。发送信号的原因有:
1、 内核检测到一个系统事件,如除零或子进程终止。
2、进程调用了 kill 函数,显示要求内核发送信号给目的进程。进程可以给自己发送信号。
接收信号指目的进程被内核强迫以某种方式对信号的发送做出反应。进程可以忽略信号,终止,或执行信号处理程序捕获信号。
发出而没有被接收的信号称为待处理信号。一种类型最多有一个待处理信号,重复的信号被丢弃。进程可以阻塞某种信号,这时仍可被发送,但不会被接收。一个待处理信号最多只能被接收一次。
发送信号
发送信号给进程基于进程组的概念。进程组由一个正整数ID标识,每个进程只属于一个进程组。
shell为每个作业创建一个独立的进程组,进程组ID一般为作业中父进程中的一个。
^C 发送 SIGINT 信号到shell,shell捕获信号发送给前台进程组的每个进程,默认终止前台作业。 ^Z 发送SIGTSTP 信号到shell,shell捕获信号发送给前台进程组的每个进程,默认挂起前台作业。
用 kill 命令向其他进程发送任意信号,给定的PID为负值时,表示发送信号给进程组ID为PID绝对值的所有进程。
进程可以用 kill 函数发送信号给任意进程(包括自己)。
接受信号
每个进程都有一个信号屏蔽字,它规定了当前要阻塞递送到该进程的信号集。每个可能的信号都有一位屏蔽字,对应位设置时表明信号当前是被阻塞的。用 sigprocmask 函数检测和更改当前信号屏蔽字。
每个信号类型都有一个预定义的默认行为,为下面中的一种:
1、 进程终止。
2、 进程终止并转储内存。 3、 进程停止(挂起)直到被SIGCONT信号重启。 4、 进程忽略该信号。内核从异常处理程序返回,将控制传递给进程p之前会检查未被阻塞的待处理信号的集合。集合为空则内核传递控制给进程p的逻辑控制流的下一条指令;集合非空则内核选择集合中某个信号k(通常取最小k),强制进程p接收k。信号触发进程的某种行为,进程完成行为后控制传递给p的逻辑控制流的下一条指令。
每种信号都有默认行为,可以用 signal 函数修改和信号关联的默认行为(除 SIGSTOP 和 SIGKILL 外):
#includetypedef void (*sighandler_t)(int);/** 改变和信号signum关联的行为 * @return 返回前次处理程序的指针,出错返回SIG_ERR */sighandler_t signal(int signum, sighandler_t handler);
指向用户定义函数,也就是信号处理程序的指针。或者为:
SIG_IGN :忽略 signum 信号。 SIG_DFL :恢复 signum 信号默认行为。信号处理程序的调用称为捕捉信号,信号处理程序的执行称为处理信号。 signal 函数会将 signum 参数传递给信号处理程序 handler 的参数,这样 handler 可以捕捉不同类型的信号。
signal 的语义和实现有关,最好使用 sigaction 函数代替它。
信号处理程序可以被其他信号处理程序中断。
练习题8.7
#includevoid handler(int sig){ return;}unsigned int snooze(unsigned int secs){ unsigned int rc=sleep(secs); printf("Slept for %d of %d secs.\n",secs-rc,secs); return rc;}int main(int argc,char **argv[]){ if(argc!=2) { fprintf(stderr,"usage:%s \n",argv[0]); exit(0); } if(signal(SIGINT,handler)==SIG_ERR) unix_error("signal error\n"); (void)snooze(atoi(argv[1])); exit(0);}
信号处理
不会有重复的信号排队等待。信号处理有以下特性:
信号处理程序阻塞当前正在处理的类型的待处理信号。
同种类型至多有一个待处理信号。
会潜在阻塞进程的慢速系统调用被信号中断后,在信号处理程序返回时不再继续,而返回一个错误条件,并将 errno 设为 EINTR 。
对于第三点,Linux系统会重启系统调用,而Solaris不会。不同系统之间,信号处理语义存在差异。Posix标准定义了 sigaction 函数,使在Posix兼容的系统上可以设置信号处理语义。
练习题8.8
下面这个程序输出什么?
父进程开始时打印“2”,然后创建子进程,子进程会陷入一个无限循环。然后父进程向子进程发送一个信号,并等待它终止。子进程捕获这个信号(中断这个无限循环),对计数器值(从初始值2)减1,打印“1”,然后终止。在父进程回收子进程之后,它对计数器值(从初始值2)加1,打印“3”,并且终止。
非本地跳转
C提供了一种用户级的异常控制流,称为非本地跳转。它将控制直接从一个函数转移到另一个正在执行的函数,而不需要经过正常的调用-返回序列。
非本地跳转通过setjmp和longjmp函数来提供。
非本地跳转可以用来从一个深层嵌套的函数调用中立即返回,如检测到错误;或者使一个信号处理程序转移到一个特殊的代码位置,而不是返回到信号中断的指令的位置。
在信号处理程序中进行非本地跳转时应使用 sigsetjmp 和 siglongjmp 。如果 savesigs 非0,则 sigsetjmp 在 env 中保存进程的当前信号屏蔽字,调用 siglongjmp 时从 env 恢复保存的信号屏蔽字。同时,应该使用一个 volatile sig_atomic_t 类型的变量来确保 env 未设置时不被中断。
举例:
#include#include #include #include static sigjmp_buf buf;static volatile sig_atomic_t canjmp;void handler(int sig){ if (canjmp == 0) return; /* ... */ canjmp = 0; siglongjmp(buf, 1);}int main(){ signal(SIGINT, handler); if (!sigsetjmp(buf, 1)) printf("starting\n"); else printf("restarting\n"); canjmp = 1; while (1) { sleep(1); printf("processing ...\n"); } exit(0);}
家庭作业
8.9
考虑四个具有如下开始和结束时间的进程
对于每个进程,指明他们是否并发运行
8.10
A. 调用一次,返回两次: fork
B. 调用一次,从不返回: execve, longjmp
C. 调用一次,返回一次或者多次: setjmp
8.11
父进程打印一次,子进程运行两次,打印两行,父进程再运行一次,共4行。
8.12
8行。
8.13
423或243
8.14
主进程只打印一行。
主进程的直接子进程会打印一行,子进程的子进程又打印一行。 所以是3行。8.15
这里的子进程不是exit,而是return,说明两个子进程都要到回到main函数去打印那里的hello。所以是5行。
8.16
输出counter = 2,因为全局变量也是复制的,而不是共享的。
8.17
Hello 1 Bye 0 2 Bye
Hello 1 0 Bye 2 Bye Hello 0 1 Bye 2 Bye8.18
进程如图:
A C E
8.19
2^n行
8.20
int main(int argc, char* args[]){ execve("/bin/ls", args, environ); //没有错误处理,注意环境变量 return 0;}
8.21
abc或bac。
8.22
int mysystem(char *command){ int status; char *argv[4]; char *a0 = "sh"; char *a1 = "-c"; if( fork()==0 ) /*子进程*/ { argv[0] = a0; argv[1] = a1; argv[2] = command; argv[3] = NULL; execve("/bin/sh", args, environ); return -1; //执行异常 } else{ /*父进程*/ if( wait(&status) > 0) { if(WIFEXITED(status) != 0) return WEXITSTATUS(status); else return status; } else return -1; //wait异常 }}
8.23
一个可能的原因是,在第一个信号发给父进程之后,父进程进入handler,并且阻塞了SIGUSR2,第二个信号依然可以发送,然而,之后的3个信号便会被抛弃了。因为是连续发送,所以很可能是没等上下文切换,这5个信号就同时发送了。所以只有2个信号被接收。
8.24
#include "csapp.h"#define N 2int main(){ int status, i; pid_t pid; char errorInfo[128]; /* Parent creates N children */ for(i=0;i0) { if (WIFEXITED(status)) printf("child %d terminated normally with exit status=%d\n", pid, WEXITSTATUS(status)); else if(WIFSIGNALED(status)) { printf("child %d terminated by signal %d: ", pid, WTERMSIG(status) ); psignal(WTERMSIG(status), errorInfo); //psignal会打印sig的信息 } } if (errno != ECHILD) unix_error("waitpid error"); exit(0);}
8.25
#include#include #include #include #include #include #include sigjmp_buf env;void tfgets_handler(int sig){ signal(SIGALRM, SIG_DFL); siglongjmp(env, 1);}char *tfgets(char *buf, int bufsize, FILE *stream){ static const int TimeLimitSecs = 5; signal(SIGALRM, tfgets_handler) alarm(TimeLimitSecs); int rc = sigsetjmp(env, 1); if(rc == 0) return fgets(buf, bufsize, stream); else return NULL; //alarm,time out}
8.26
未完成。
教材学习中的问题和解决过程
- 问题1:
练习题8.5输出的结果与预期结果不一样
- 问题1解决方案:
头文件的使用错误,应使用csapp.h头文件。
代码调试中的问题和解决过程
- 问题1:
带有csapp.h头文件的程序无法被执行
- 问题1解决方案:
linux没有自带csapp.h,所以要自己导入,将csapp.h要放到 /usr/include的文件夹里面。打开后这里面有好多头文件
要在头文件的#end if前面加上一句#include <csapp.c>,因为那个头文件要把csapp.c文件包含进去 这样的话就可以用了。
结对及互评
本周结对学习情况
- [20155214](http://www.cnblogs.com/besti155214/p/8052476.html)- 结对照片
- 结对学习内容
解答同伴问题:
1、在程序中如何异步回收子进程?
解答:
当一个进程fork出子进程后,没有对子进程进行回收,那么子进程运行完之后的状态会变为僵尸进程.
我们可以通过wait和waitpid来回收子进程,防止僵尸进程的出现.
但是wait和waitpid,要么以阻塞方式运行,要么以轮询方式运行,都极大的占用了CPU资源.
在每个子进程运行完成之后,都会向父进程发出SIGCHLD信号.而在默认情况下,父进程会忽略掉该信号.因此,我们只需要对SIGCHLD信号进行捕捉,即可异步对子进程进行回收.
一下是一个示例代码:
#include#include #include #include #include void waitChild1(int sig){ int status = 0; if(waitpid(-1,&status,WNOHANG)>0){ printf("the child exit status is %d\n",WEXITSTATUS(status)); }else{ printf("failure!\n"); }}void waitChild2(int sig){ int status = 0; pid_t pid = 0; while((pid = waitpid(-1,&status,WNOHANG)) >0){ printf("%d's exit status is %d\n",pid,WEXITSTATUS(status)); }}int main(){ signal(SIGCHLD,waitChild2); int pid = fork(); if(pid == 0){ //child printf("i am child,my pid is %d\n",getpid()); exit(1); } pid = fork(); if(pid == 0){ //child printf("i am child,my pid is %d\n",getpid()); exit(1); } pid = fork(); if(pid == 0){ //child printf("i am child,my pid is %d\n",getpid()); exit(1); } pid = fork(); if(pid == 0){ //child printf("i am child,my pid is %d\n",getpid()); exit(1); } while(1){ printf("i am father,my pid is %d\n",getpid()); sleep(5); } return 0;}
2、如何使用非本地跳转来规避正常的调用/返回栈规则。
解答:
非本地跳转通过setjmp和longjmp函数来提供。
#include/** 在env缓冲区中保存当前栈的内容,供longjmp使用,返回0 * @return setjmp返回0,longjmp返回非0 */int setjmp(jmp_buf env);int sigsetjmp(sigjmp_buf env, int savesigs);/** 从env缓冲区中恢复栈的内容,触发一个从最近一次初始化env的setjmp调用的返回,setjmp返回非0的给定val值 */void longjmp(jmp_buf env, int val);void siglongjmp(sigjmp_buf env, int val);
其他(感悟、思考等,可选)
1、深入学习了进程的相关概念。
学习进度条
代码行数(新增/累积) | 博客量(新增/累积) | 学习时间(新增/累积) | 重要成长 | |
---|---|---|---|---|
目标 | 5000行 | 30篇 | 400小时 | |
第三周 | 114/114 | 3/3 | 20/20 | |
第四周 | 136/250 | 2/5 | 18/38 | |
第五周 | 87/337 | 2/7 | 22/60 | |
第六周 | 271/608 | 2/9 | 30/90 | |
第七周 | 185/716 | 2/11 | 30/90 | |
第八周 | 531/1247 | 3/14 | 30/90 | |
第九周 | 439/1686 | 3/17 | 30/90 | |
第十一周 | 153/1839 | 2/19 | 30/90 | |
第十三周 | 628/2467 | 2/21 | 30/90 |
尝试一下记录「计划学习时间」和「实际学习时间」,到期末看看能不能改进自己的计划能力。这个工作学习中很重要,也很有用。
耗时估计的公式 :Y=X+X/N ,Y=X-X/N,训练次数多了,X、Y就接近了。参考:,
计划学习时间:25小时
实际学习时间:20小时
改进情况:
(有空多看看)
参考资料
- ...