4个题目:进程创建及创建过程分析、进程族亲关系分析、进程间软中断通信、进程间管道通信(欢迎评论/私信)
笔者用的是“ProcessOn免费在线作图网站”作的图。
报告格式要求:正文{中文:宋体、五号、单倍距;英文:Times New Roman、11号、单倍距}
一级标题{宋体、小三号、加粗} 二级标题{四号} 三级标题{小四}不允许有四级标题
图命名{在图下方居中、宋体、五号、仅允许出现2级图名eg.3-1<空格>名称} 图中文字{宋体、小五}
表命名{在表上方居中、......} 表中文字{......}
页眉{学号<空格>姓名、宋体、小四、居中} 页脚{目录页码用罗马数字、正文页码用阿拉伯数字}
注*参考文献格式要求、缩进要求
题目1 进程创建及创建过程分析 参看书P163~166
灵活运用fork系统调用创建进程,深入分析进程创建过程及fork系统调用返回值的含义,透彻分析父子进程代码共享、执行流程及数据集合的变化轨迹。
设计的程序能体现出父子进程执行轨迹的差别,也能体现出父子进程数据集合的差别。
#include
#include
pid_t fork(void);
int main()
{
printf("parent pid=%d\n",getpid());
pid_t pid=fork();
int x=0;
if(pid==0){
x=x+1;
printf("child pid=%d,x=%d,&x=%p\n",getpid(),x,&x);
}
else{
x=x-1;
printf("parent pid=%d,x=%d,&x=%p\n",getpid(),x,&x);
}
sleep(10);
}
图1-1 父子进程创建的运行结果
图1-2 与进程对应的PCB和数据集示意图
图1-3 fork系统调用如何创建进程
图1-4 fork系统调用返回后父子进程的执行路径
执行步骤:
(1)程序启动时,系统加载程序,为变量分配内存,为父进程创建进程控制块PCB,并在其中填写分配给该进程的PID(此处为3416)、代码地址、数据集地址和其他属性,在进程数据集中x和PID的内容不定。
(2)父进程执行赋值语句“x=0”:程序将整数0写入变量x所在的存储单元。
(3)父进程执行fork系统调用:
①系统首先创建子进程PCB,内容从父进程PCB复制而来,但pid是新分配的唯一整数(此处为3417);
②创建父进程数据集的一个副本,保存于新分配的存储器中,作为子进程数据集,其中变量x的值也是0,以后子进程仅对属于自己的数据集进行操作;
③子进程PCB中的数据集地址指向子进程自己的数据集,有了程序代码、数据集和PCB,一个完整的子进程就创建出来了。这样子进程除pid与父进程不同外,程序代码和数据集都与父进程一样,fork函数完成了对父进程的复制工作;
④由于子进程从父进程复制而来,程序计数器PC中具有相同的地址。当获得CPU后,下一条指令或语句也与父进程一致,父进程接下来从fork系统调用返回,子进程也一样。在父子进程的数据集中都有一个“fork函数返回值”项,父进程fork系统调用的返回值为子进程的pid(此处为3417),子进程的fork返回值为0,并由系统填入父子进程的数据集中。
(4)fork系统调用返回后父子进程的执行路径:
①fork系统调用完成子进程的创建后,父子进程都有相同的程序代码,从函数调用返回开始往下执行,从数据集中读取fork返回值,赋值给pid变量,因此父进程的pid变量被写入3417,子进程的pid变量被写入0;
②接下来父子进程都往下执行,判断if条件,决定是否执行if分支。由于父进程pid>0,子进程pid==0,因此父进程将进入if(pid>0)分支,子进程将进入if(pid==0)分支;
③父进程执行if(pid>0)分支时,遇到x=x-1语句,将x(初值为0)减1后写回变成-1,并输出“父进程的PID、x的值和x的地址”;子进程执行if(pid==0)时,遇到x=x+1语句,将x(初值为0)加1后变成1,并输出“子进程的PID、x的值和x的地址”;
④完成if语句块后,两进程都要执行最后的“sleep(10);”语句,睡眠10秒,让用户有时间查看进程信息。
[1]徐钦桂,徐治根,黄培灿,谢伟鹏. Linux编程[M]. 北京:清华大学出版社,2019:163-166.
通过本次课程设计我学会灵活运用fork系统调用创建进程,深入分析进程创建过程及fork系统调用返回值的含义,透彻分析父子进程代码共享、执行流程及数据集合的变化轨迹。
fork函数时调用一次,返回两次。在父进程和子进程中各调用一次。子进程中返回值为0,父进程中返回值为子进程的PID。可以根据返回值的不同让父进程和子进程执行不同的代码。子进程是父进程的副本,获得了父进程数据空间、堆和栈的副本;父子进程并不共享这些存储空间,即代码段;因此子进程对变量的所做的改变并不会影响父进程。一般来说,fork之后父、子进程执行顺序是不确定的,这取决于内核调度算法。进程之间实现同步需要进行进程通信。
题目2 进程族亲关系分析 参看书P166~169
通过画进程族亲关系图来帮助理解多进程应用程序,从而获得编写多进程并发程序的能力。
仿照教材图5-16分析进程族亲关系。针对教材习题5.10进行调试、运行、分析,并对程序做适当修改后再分析输出结果。
#include
int flag=1;
int main(){
pid_t pid;
pid=fork();
if(pid>0)flag=flag+1;
if(pid==0)flag=flag+2;
pid=fork();
if(pid>0)flag=flag+10;
if(pid==0)flag=flag+20;
pid=fork();
if(pid>0)flag=flag+100;
if(pid==0)flag=flag+200;
pid=fork();
if(pid>0)flag=flag+1000;
if(pid==0)flag=flag+2000;
printf("flag=%d,pid=%d\n",flag,getpid());
}
图2-1 进程族亲关系的运行结果
图2-2 进程族亲关系分析和进程图画法
程序执行和进程产生过程:
(1)程序启动时,系统创建进程p1(flag=1);p1执行程序中的第一个fork函数调用,创建子进程p11(flag=1),两个进程的pid变量值分别为p11的PID和0,即p1.pid=PID(p11),p11.pid=0。
(2)条件pid>0为真的进程p1将flag加1变成2;条件pid==0为真的进程p11将flag加2变成3。
(3)接着p1、p11都执行程序中的第二个fork函数调用,分别创建进程p12(flag=2)和p111(flag=3),四个进程的pid变量值分别为p1.pid=PID(p12)、p11.pid=PID(p111)、p12.pid=0、p111.pid=0。
(4)条件pid>0为真的进程p1和p11将flag加10变成12和13;条件pid==0为真的进程p12和p111将flag加20变成22和23。
(5)第三个fork函数调用和其后的两个if语句,执行步骤同第(3)和(4)步,简写成:
p1:flag=12+100=112 p11:flag=13+100=113 p111:flag=23+100=123
p12:flag=22+100=122
新的子进程p13:flag=12+200=212 p112:flag=13+200=213 p121:flag=22+200=222 p1111:flag=23+200=223
(6)第四个fork函数调用和其后的两个if语句,执行步骤同第(3)和(4)步,简写成:
p1:flag=112+1000=1112 p11:flag=113+1000=1113 p111:flag=123+1000=1123
p12:flag=122+1000=1122 p13:flag=212+1000=1212 p112:flag=213+1000=1213
p121:flag=222+1000=1222 p1111:flag=223+1000=1223
新的子进程p14:flag=112+2000=2112 p131:flag=212+2000=2212
p122:flag=122+2000=2122 p1211:flag=222+2000=2222
p113:flag=113+2000=2113 p1121:flag=213+2000=2213
p1112:flag=123+2000=2123 p11111:flag=223+2000=2223
(7)最后,16个进程均往下执行printf,产生16行输出,并分别执行sleep(10)语句睡眠10秒而终止。
[1]徐钦桂,徐治根,黄培灿,谢伟鹏. Linux编程[M]. 北京:清华大学出版社,2019:166-169.
在本次课程设计中,我学会了通过画进程族亲关系图来帮助理解多进程应用程序,从而获得编写多进程并发程序的能力。
进程族亲关系图让我清楚地明白了进程间错综复杂的父子关系,以及fork系统调用后又是如何“繁衍后代”的。赋予代代子孙们的变量flag同它们固有的PID一样不会互相重复,这种方式能更加唯一标识一个进程。
题目3 进程间软中断通信 参看书P186~193
理解进程间软中断通信机制。
使用系统调用fork()创建两个子进程,再用系统调用signal()让父进程捕捉键盘上发出的中断信号(即按Ctrl+C键),当父进程接收到这两个软中断的其中一个后,父进程用系统调用kill()向两个子进程分别发送整数值为16和17软中断信号,子进程获得对应软中断信号,然后分别输出下列信息后终止:
Child process 1 is killed by parent!!
Child process 2 is killed by parent!!
父进程调用wait()函数等待两个子进程终止后,输出以下信息,结束进程执行:
Parent process is killed!!
多运行几次编写的程序,简略分析出现不同结果的原因。
#include
#include
#include
#include
#include
#include
int flag_wait = 1;
void stop2()
{
flag_wait = 0;
printf("Child interruption\n");
}
void stop1()
{
printf("Parent interruption\n");
}
int main()
{
pid_t pid1, pid2;
//当接受到按下Ctrl+C时产生的SIGINT信号时,调用stop1()
signal(SIGINT, stop1);
//创建子进程1
do
{
pid1 = fork();
} while (pid1 < 0);
if (pid1 > 0)
{
//创建子进程2
do
{
pid2 = fork();
} while (pid2 < 0);
if (pid2 > 0)
{
sleep(5);
//向子进程1发送中断信号16
kill(pid1, 16);
wait(0);
//向子进程2发送中断信号17
kill(pid2, 17);
wait(0);
printf("Parent process is killed!!\n");
exit(0);
}
else
{
//当接受到信号17时,调用stop2()
signal(17, stop2);
//死循环直到接收到中断信号
while (flag_wait);
printf("Child process 2 is killed by parent!!\n");
exit(0);
}
}
else
{
//当接受到信号16时,调用stop2()
signal(16, stop2);
//死循环直到接收到中断信号
while (flag_wait);
printf("Child process 1 is killed by parent!!\n");
exit(0);
}
return 0;
}
程序执行步骤:
(1)创建两个pid_t类型的变量pid1和pid2;
(2)再用系统调用signal()让父进程捕捉键盘上发出的中断信号(即按Ctrl+C键)
①当父进程接收到这个软中断(Ctrl+C)后,输出“Parent interruption”;
②当父进程未收到软中断(Ctrl+C),则继续往下执行;
(3)fork系统调用创建子进程pid1和pid2;
(4)如果当前运行的是子进程pid1和pid2,则在它们得到软中断信号后终止,并输出“Child process 1 is killed by parent!!”、“Child process 2 is killed by parent!!”;
(5)如果当前运行的是父进程,则一起随眠5秒,再分别向子进程1和2发送中断信号16和17,父进程调用wait()函数等待两个子进程终止后,输出“Parent process is killed!!”,结束进程执行。
图3-3 进程间软中断通信的流程图
[1]徐钦桂,徐治根,黄培灿,谢伟鹏. Linux编程[M]. 北京:清华大学出版社,2019:186-193.
通过本次课程设计我理解了进程间软中断通信机制。软中断信号(signal)是一种简单且最基本的进程通信机制,它最大的特点是提供了一种简单的处理异步事件的方法。例如,常见的用户从键盘键入组合键Ctrl+C来中断一个程序的运行,或者在两个进程之间通过某个信号来通知发生了异步事件,或者向系统或进程报告突发的硬件故障,如非法指令、运算溢出等。更重要的是,用户进程还可以向自己发送信号以中断进程的执行,并自动转入指定的软中断处理函数去执行用户自行安排的处理内容,处理完毕后再返回用户进程继续执行,从而为应用程序提供了由用户自行处理随机事件的通信机制。
软中断信号实现 (signal implementation) 是操作系统用来通知进程有事情发生的一种机制。由于这种信号总是在进程处于运行状态时才会去响应,故称之为软中断信号。软中断信号的使用者是操作系统和源程序,操作系统事先将系统的中可以使用的软件中断信号进行集中编码并定义相应含义后,提交用户使用。用户可以通过相应的软件中断序号或软中断名称来使用软中断,二者在使用上是等效的。用户只能在操作系统提供的软件中断序号范围内使用软件中断信号,不能自己创建新的软件中断信号。如果用户的应用程序之间有信号需要发送,则可以使用操作系统预留给用户使用的用户信号 SIGUSR1 或用户信号 SIGUSR2。
题目4 进程间的管道通信 参看书P278~281
理解进程间管道通信机制。
使用系统调用pipe()建立一条管道线,两个子进程分别向管道各写一句话:
Child process1 is sending a message!
Child process2 is sending a message!
而父进程则从管道中读出来自两个子进程的信息,显示在屏幕上。
要求:父进程先接受子进程P1发来的消息,然后再接受子进程P2发来的消息。
#include
#include
#include
#include
#include
int main()
{
pid_t pid1,pid2; //定义两个进程变量
int fd[2];
char OutPipe[100],InPipe[100]; //定义两个字符数组
pipe(fd); //创建管道
while((pid1=fork())==-1); //如果进程1创建不成功,则空循环
if(pid1==0) //如果子进程1创建成功,pid1为进程号
{
lockf(fd[1],1,0); //锁定管道
sprintf(OutPipe,"Child process 1 is sending message!\n"); //给OutPipe赋值
write(fd[1],OutPipe,50); //向管道写入数据
sleep(1); //等待读进程读出数据
lockf(fd[1],0,0); //解除管道的锁定
exit(0); //结束进程1
}
else
{
while((pid2=fork())==-1); //若进程2创建不成功,则空循环
if(pid2==0)
{
lockf(fd[1],1,0);
sprintf(OutPipe,"Child process 2 is sending message!\n");
write(fd[1],OutPipe,50);
sleep(1);
lockf(fd[1],0,0);
exit(0);
}
else
{
wait(0); //等待子进程1结束
read(fd[0],InPipe,50); //从管道中读出数据
printf("read:%s\n",InPipe); //显示读出的数据
wait(0); //等待子进程2结束
read(fd[0],InPipe,50);
printf("read:%s\n",InPipe); //父进程结束
exit(0);
}
}
return 0;
}
图4-1 进程间管道通信的运行结果
[1]徐钦桂,徐治根,黄培灿,谢伟鹏. Linux编程[M]. 北京:清华大学出版社,2019:278-281.
通过本次课程设计我理解了进程间软管道通信机制。
pipe管道,又称pipe文件,它用于连接一个读进程和一个写进程,以实现它们之间的信息共享。向管道(共享文件)提供输入的发送进程(即写进程),以字符流形式将大量的数据送入管道;而接受管道输出的接收进程(即读进程),可以从管道中接收(读)数据。
为了协调双方的通信,管道机制必须提供以下三方面的协调能力:
(1)互斥:当一个进程正在对pipe执行读/写操作时,其它(另一)进程必须等待,程序中使用lockf(fd[1],1,0)函数实现对管道的加锁操作,用lockf(fd[1],0,0)解除管道的锁定。
(2)同步:当写(输入)进程把一定数量(如4KB)的数据写入pipe后,便去睡眠等待,直到读(输出)进程取走数据后,再把它唤醒。当读进程试图从一空pipe中读取数据时,也应睡眠等待,直至写进程将数据写入管道后,才将之唤醒。
(3)确定对方是否存在:只有确定了写进程和读进程都存在的情况下,方能通过管道进行通信。