2017-2018-1 20155310 《信息安全系统设计基础》第十三周学习总结
教材学习内容总结
8.2进程
进程是计算机科学中最深刻最成功的概念之一。系统中的每个程序都是运行在某个进程的上下文中的。(上下文是由程序正确运行所需的状态组成的)
•一个独立的逻辑控制流
•一个逻辑流的执行在时间上与另一个流重叠,称为并发流。
•多个流并发地执行的一般现象称为冰法。
•多任务也叫作时间分片:一个进程和其他进程轮流运行的概念称为多任务。
•并行流是并发流的一个真子集。
•私有地址空间
•上下文切换
•上下文切换是较高层形式的异常控制流。
•内核使用上下文切换来实现多任务。
•调度:在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占的进程。
•当内核代表用户执行系统调用时,可能会发生上下文切换。一般,即使系统调用没有阻塞,内核也可以决定执行上下文切换,而非将控制返回给调用进程。
•中断也可能引发上下文切换。
一个独立的逻辑控制流:他提供一个假象,好像我们的额程序独占的使用处理器。
一个私有的地址空间:他提供一个假象,好像我们独占的使用存储器系统。
多个流一起执行被称为并发。
一个进程和其他进程轮流进行的概念被称为多任务。
一个进程执行执行他的控制流的一部分的每一段时间叫做时间片。
并发流:一个逻辑流的执行在时间上与另一个流重叠。
并发:多个流并发地执行的一般现象。
多任务:一个进程和其他进程轮流运行的概念。
时间片:一个进程执行它的控制流的一部分的每一时间段。
多任务也叫时间分片。
1、模式位:用某个控制寄存器中的一个位模式,限制一个应用可以执行的指令以及它可以访问的地址空间范围。
2、当设置了位模式,进程就运行在内核模式中,一个运行在内核模式中的进程可以中兴指令集中的任何指令,而且可以访问系统中任何存储器位置。
3、没有设置位模式时,进程就运行在用户模式中,不允许执行特权指令,例如停止处理器、改变位模式,或者发起一个I/O操作。
4、用户程序必须通过系统调用接口间接的当问内核代码和数据。
5、进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障、或者陷入系统调用这样的异常。
上下文切换机制:
(1)保存当前进程的上下文
(2)恢复某个先前被抢占的进程被保存的上下文
(3)将控制传递给这个新恢复的进程
引起上下文切换的情况:
(1)当内核代表用户执行系统调用时
(2)中断时
8.3 系统调用错误处理
•错误报告函数
•
错误包装函数
8.4 进程控制
进程控制
•获取进程ID
•创建和终止进程
•回收子进程
•
等待回收子进程
•
引入:一个进程可以通过调用waitpid函数来等待它的子进程终止或者停止
#include<sys/types.h>
#incldue<sys/wait.h>
pidt waitpid(pidt pid,int *status,int options);//如果成功,返回子进程的PID,如果为WNOHANG,则为0,其他错误则为-1
说明:默认地,即当options=0的时候,waitpid挂起调用进程的执行,直到它的等待集合中的一个子进程终止。如果等待集合中的一个进程在刚调用的时候就已经终止了,那么waitpid就立即返回。以上两种情况都会使得waitpid函数返回已经终止的子进程的PID,并且去除该进程。
•
判断等待集合的成员
如果pid>0,那么等待的集合就是一个单独的子进程,它的进程ID等于pid; 如果pid<-1,那么等待集合就是由父进程的所有子进程组成的
•
修改默认行为 可以通过将options设置为常量WNOHANG和WUNTRACED的各种组合,修改默认行为: WNOHANG:如果等待集合中的任何子进程都还没有都还没有终止,那么就立即返回0; WUNTRACED:挂起调用进程的执行,直到等待集合中的一个变成已经终止的或者被停止,然后返回导致返回的子进程的PID; WNOHANG|WUNTRACED:立即返回。
•检查已回收子进程的退出状态 非空的status参数会被放上status参数的关于返回的子进程的状态信息(wait.h定义了status参数的几个宏) WIFEXITED:如果子进程通过调用exit函数或者一个返回即return政策终止,就返回真; WEXITSTATUS:返回一个正常终止的子进程的退出状态(在WIFEXITED返回为真的时候才定义这个状态) WIFSINGALED:如果子进程是因为一个未被捕获的信号终止的,那么就返回真; WTERMSIG:返回导致子进程终止的信号的编号。只有在WIFSINGALED为真的时候,才定义这个状态。
•错误条件 如果调用进程没有子进程,那么waitpid函数返回-1,并且设置errno为ECHLD; 如果waitpid函数被一个信号中断,那么它返回-1,并设置errno为EINTR
•应用举例
#include "csapp.h"
#define N 2
int main() {
int status,i;
pidt pid;
for(i =0;i<N;i++)
if((pid = fork())==0)
exit(100+i);
while((pid = waitpid(-1,&status,0))>0) {
if(WIFEXITED(status))
printf("child %d terminated normally with exit status = %d
",pid,WEXITSTATUS(status));
else
printf("child %d terminated abnormally
",pid);
}
if(errno != ECHILD) unixerror("waitpid error");
exit(0);
}
(1). 父进程创建N个子进程,然后子进程以一个唯一的退出状态退出;
(2).waitpid函数被阻塞直到某个子进程终止,然后进入while循环测试是否是正常终止的;是正常的话就输出;
(3).当回收了所有的子进程之后,再调用waitpid就返回-1,并且设置errno为ECHILD;如果不是正常终止的,就输出一个错误消息
•函数使用•sleep函数将一个进程挂起一段指定的时间
#include <unistd.h>
unsigned int sleep(unsigned int secs);
•pause函数让调用函数休眠,直到该进程收到一个信号
#include <unistd.h>
int pause(void);
•execve函数在当前进程的上下文中加载并运行一个新程序
#include <unistd.h>
int execve(const char *filename,const char *argv[],const char *envp[]);
•getenv、setenv、unsetenv函数(P501)
•关于fork和execve的区别: fork函数在新的子进程中运行相同的程序,新的子进程是父进程的一个复制品; 而execve函数在当前进程的上下文中加载并运行一个新的程序,会覆盖当前进程的地址空间,但并没有创建一个新进程。
8.4.1 获取进程ID
•每个进程都有一个唯一的正数(非零)进程ID(PID)
•getpid函数返回调用进程的PID
•
getppid返回它父进程的PID(创建调用进程的进程)
8.4.2 创建和终止进程
•进程的三种状态——运行、停止和终止
•进程会因为三种原因终止进程:收到信号,该信号默认终止进程;从主程序返回;调用exit函数
•父进程通过调用fork创建一个新的运行子进程:父进程与子进程有相同(但是独立的)地址空间,有相同的文件藐视符集合
•
fork调用一次返回两次、并发执行、相同的但是独立的地址空间、共享文件
8.4.3 回收子进程
•回收:当一个进程终止时,内核并不立即把它从系统中清除。相反,进程被保持在一种已终止的状态中,直到被它的父进程回收
•僵死进程:一个终止了但是还未被回收的进程称为僵死进程,仍然消耗系统的存储器资源
•回收子进程的两种方法:1,内核的init进程 2,父进程waitpid函数
•如果父进程没有回收它的僵死子进程就终止了,那么内核就会安排init进城来回收它们。init进程的PID为1,并且是在系统初始化时创建的
•一个进程可以通过调用waitpid函数来等待它的子进程终止或停止
•
wait(&status)函数,等价于调用wait(-1,&status,0)
8.4.4 让进程休眠
•sleep函数将一个进程挂起一段指定的时间
•
pause函数让调用函数休眠,直到该进程收到一个信号
8.4.5 加载并运行程序
•execve函数在当前进程的上下文中加载并运行一个新程序
•execve调用一次且不返回
•
getenv函数在环境数组中搜索字符串“name=value”,找到返回指向value的指针,否则返回NULL
8.4.6 利用fork和execve运行程序
8.5 信号
Unix信号是一种更高层的软件形式的异常
•
信号术语
•
发送信号
(1). 用/bin/kill程序发送信号
unix> /bin/kill -9 15213
(2).从键盘发送信号
(3).用kill函数发送信号
#include "csapp.h"
int main(){
pid_t pid;
if ((pid = Fork()) == 0){
Pause();
printf("control should never reach here!
");
exit(0);
}
Kill(pid,SIGKILL);
exit(0);
}
(4).用alarm函数发送信号
#include "csapp.h"
void handler(int sig)
{
static int beeps = 0;
printf("BEEP
");
if (++beeps<5)
Alarm(1);
else{
printf("BOOM!
");
exit(0);
}
}
int main()
{
Signal(SIGALRM,handler);
Alarm(1);
while(1){
;
}
exit(0);
}
•
接收信号
(1)signal函数可以改变和信号signum相关联的行为
•处理信号
•
可移植的信号处理问题
(1)sigaction函数
•非本地跳转 将控制直接从一个函数转移到另一个当前正在执行的函数•通过setjmp和longjmp提供
•操作进程的工具•STRACE:打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹
•PS:列出当前系统中的进程(包括僵死进程)
•TOP:打印出关于当前进程资源使用的信息
•PMAP:显示进程的存储器映射
•unix信号,它允许进程中断其他进程
•
低层的硬件异常是由内核异常处理程序处理的,正常情况下,对用户进程而言是不可见的。信号提供了一种机制,通知用户进程发生了这些异常
8.5.1 信号术语
•传送一个信号的步骤:发送信号、接收信号
•发送信号:内核通过更新目的进程中上下文中的某个状态,发送一个信号给目的进程。发送信号有两个原因:1)内核检测到一个系统事件; 2)一个进程调用kill函数,显式地要求内核发送一个信号给目的进程
•一个进程可以给自己发送信号
•接收信号:目的进程就接收了信号。进程可以忽略这个信号,终止或者通过执行信号处理程序捕获这个信号
•
一种类型的信号只能有一种待处理信号
8.5.2 发送信号
•进程组:每个进程都只属于一个进程组,进程组是由一个进程组ID来标识的
•进程可以通过setpgid函数返回当前进程的进程组ID
•默认的,一个子进程和它的父进程同属于一个进程组
•/bin/kill程序可以向另外的进程发送任意的信号
•在任何时刻,至多只有一个前台作业和0个或多个后台作业
•外壳为每个作业创建一个独立的进程组,一个作业对应一个进程组
•进程通过调用kill函数发送信号给其他进程(包括自己)
•进程通过调用alarm函数向他自己发送SIGALRM信号
•
alarm函数安排内核在secs秒内发送一个SIGALRM信号给调用进程
8.5.3 接收信号
•进程可以通过使用signal函数来修改和信号相关的默认行为
•
SIGSTOP和SIGKILL,它们的默认行为不能被修改
8.5.4 信号处理问题
•
当一个程序捕获多个信号时,有一些问题:待处理信号不会排队等待、待处理信号被阻塞、系统调用被中断
8.5.5 可移植的信号处理
•目的是为了统一同一信号在不通系统中的语义
•
sigaction函数,或者是它的包装函数Signal函数
8.5.6 显式地阻塞和取消阻塞
•应用程序可以使用sigprocmask函数显式地阻塞和取消阻塞选择的信号
•
sigprocmask函数改变当前已阻塞信号的集合
8.5.7 同步流以避免讨厌的并发错误
8.6 非本地跳转
•c语言提供了一种用户级异常控制流形式,称为本地跳转
•通过setjmp和longjmp函数来提供
•setjmp函数只被调用一次,但返回多次:一次是当第一次调用setjmp,而调用环境保存在缓冲区env中时,一次是为每个相应的longjmp调用
•longjmp只调用一次,但从不返回
•非本地跳转的一个重要应用就是允许从一个深层嵌套的函数调用中立即返回,通常是由检测到某个错误情况引起的
•
非本地跳转的另一个重要应用是使一个信号处理程序分支到一个特殊的代码位置,而不是返回到达中断了的指令位置
8.7 操作进程的工具
•
Linux系统提供监控和操作进程的工具:打印一个正在运行的程序和它的子进程调用的每个系统调用的轨迹(STRACE)、列出当前系统中包括僵死进程的进程(PS)、打印出关于当前进程资源使用的信息(TOP)、显示进程的存储器映射(PMAP)、虚拟文件系统(/proc)
数组指针、指针数组、函数指针、指针函数的区别
•指针数组(char *a[4]):定义了一个数组,而它的每个元素的类型是一个指向字符/字符串的指针
•数组指针:(char (*a)[4]):表示一个指向“一个有4个字符类型元素的数组”的指针
•函数指针(int (*ptr)(int x, int y)):函数指针是指向函数的指针变量。
•
指针函数(int *p(int a,float b)): 返回值为指针的函数,p为函数名,int *为函数类型
管道和I/O重定向
管道pipe函数
•无名管道为建立管道的进程及其子孙提供一条以比特流方式传送消息的通信管道
•该管道在逻辑上被看作管道文件,在物理上则由文件系统的高速缓冲区构成,而很少启动外设
•发送进程利用文件系统的系统调用write(fd[1],buf,size),把buf种的长度为size字符的消息送入管道入口fd[1],接收进程则使用系统调用read(fd[0],buf,size)从管道出口fd[0]出口读出size字符的消息置入buf中
•这里,管道按FIFO(先进先出)方式传送消息,且只能单向传送消息
•
利用UNIX提供的系统调用pipe,可建立一条同步通信管道。其格式为:int fd[2];pipe(fd);这里,fd[1]为写入端,fd[0]为读出端
I/O重定向dup,dup2
•dup和dup2也是两个非常有用的调用,它们的作用都是用来复制一个文件的描述符
•它们经常用来重定向进程的stdin、stdout和stderr
•利用函数dup,可以复制一个描述符,这两个描述符共享同一个数据结构
•dup2函数跟dup函数相似,但dup2函数允许调用者规定一个有效描述符和目标描述符的id。dup2函数成功返回时,目标描述符(dup2函数的第二个参数)将变成源描述符(dup2函数的第一个参数)的复制品,换句话说,两个文件描述符现在都指向同一个文件,并且是函数第一个参数指向的文件
课后作业
解答:
进程A和B是互相并发的,就像B和C一样,因为它们各自的执行是重叠的,也就是一个进程在另一个进程结束前开始。进程A和C不是并发的,因为它们的执行没有重叠;A在C开始之前就结束了。
解答:
A.这里的关键点是子进程执行了两个prin七f语句。在fork返回之后,它执行了第8行的prin七fe然后它从if语句中出来,执行了第9行的printf语句。下面是子进程产生的输出:printfl:x=2 printf2: x=1
B.父进程只执行了第9行的printf:printf2: x=0
解答:
父进程打印b,然后是c。子进程打印a,然后是c。意识到你不能对父进程和子进程是如何交错执行的做任何假设是非常重要的。因此,任何满足b}c和a -- c的拓扑排序都是可能的输出序列。有四个这样的序列:acbc, bcac, abcc和bacco
解答:
A.每次我们运行这个程序,就会产生6个输出行。 B.输出行的顺序根据系统不同而不同,取决于内核如何交替执行父子进程的指令。一般而言,满足下图的任意拓扑排序都是合法的顺序:
比如unix>Hello01Bye2Bye当我们在系统上运行这个程序时,会得到下面的输出:
unix> ./waitprob1
Hello
0
1
Bye
2
Bye
在这种情况下,父进程首先运行,在第6行打印''Hello'',在第8行打印“0”。对wait的调用会阻塞,因为子进程还没有终止,所以内核执行一个上下文切换,并将控制传递给子进程,子进程在第8行打印''1'',在第15行打印''Bye'',然后在第16行终止,退出状态为2。在子进程终止后,父进程继续,在第12行打印子进程的退出状态,在第15行打印''Bye''。
解答:
解答:
解答:
只要休眠进程收到一个未被忽略的信号,sleep函数就会提前返回。但是,因为收到一个SIGINT信号的默认行为就是终止进程(见图8-25 ),我们必须设置一个SIGINT处理程序来允许sleep函数返回。处理程序简单地捕获SIGNAL,并将控制返回给sleep函数,该函数会立即返回。
解答:
这个程序打印字符串“213",这是卡内基梅隆大学CS:APP课程的缩写名。父进程开始时打印''2'',然后创建子进程,子进程会陷入一个无限循环。然后父进程向子进程发送一个信号,并等待它终止。子进程捕获这个信号(中断这个无限循环),对计数器值(从初始值2)减一,打印''1",然后终止。在父进程回收子进程之后,它对计数器值(从初始值2)加一,打印,''3'',并且终止。