广州大学2020操作系统实验一:进程管理与进程通信

在这里插入图片描述

五份实验报告下载链接:click~

课程设计下载链接:click~

一、实验目的

1、掌握进程的概念,明确进程的含义。
2、认识并了解进程并发执行的实质,进程的阻塞与唤醒,终止与退出的过程。
3、熟悉进程的睡眠、同步、撤消等进程控制方法。
4、分析进程竞争资源的现象,学习解决进程互斥的方法 。
5、了解什么是信号,利用信号量机制熟悉进程间软中断通信的基本原理,
6、熟悉消息传送的机理 ,共享存储机制。

二、实验内容

1、编写一段程序,使用系统调用fork( )创建两个子进程。当此程序运行时,在系统中有一个父进程和两个子进程并发执行,观察实验结果并分析原因。
2、用fork( )创建一个进程,再调用exec( ),用新的程序替换该子进程的内容,利用wait( )来控制进程执行顺序,掌握进程的睡眠、同步、撤消等进程控制方法,并根据实验结果分析原因。
3、编写一段多进程并发运行的程序,用lockf( )来给每一个进程加锁,以实现进程之间的互斥,观察并分析出现的现象及原因。
4、编写程序:用fork( )创建两个子进程,再用系统调用signal( )让父进程捕捉键盘上来的中断信号(即按^c键);捕捉到中断信号后,父进程用系统调用kill( )向两个子进程发出信号,子进程捕捉到信号后分别输出下列信息后终止:
Child process1 is killed by parent!
Child process2 is killed by parent!
父进程等待两个子进程终止后,输出如下的信息后终止:
Parent process is killed!
分析利用信号量机制中的软中断通信实现进程同步的机理。
5、使用系统调用msgget( ),msgsnd( ),msgrev( ),及msgctl( )编制一长度为1k的消息发送和接收的程序,并分析消息的创建、发送和接收机制及控制原理。
6、编制一长度为1k的共享存储区发送和接收的程序,并设计对该共享存储区进行互斥访问及进程同步的措施,必须保证实现正确的通信。

三、实验原理

四、实验中用到的系统调用函数

(包括实验原理中介绍的和自己采用的),自己采用的系统调用函数要按照指导书中的格式说明进行介绍。
fork, exec, wait, exit, getpid, sleep, lockf, kill, signal, msgget, msgsnd, msgrcv, msgctl,shmget, shmat, shmdt, shmctl。

五、实验步骤

以下六个实验的实验步骤大致统一为:
① 梳理问题
② 整理思路
③ 画出程序框图
④ 转化为代码实现
⑤ 分析实验结果
⑥ 总结

1、实验一:进程创建与进程并发执行

使用系统调用fork()函数依次创建两个子进程,简单实现如下图程序框图。
在这里插入图片描述

2、实验二:进程的睡眠、同步、撤消等进程控制

父进程使用睡眠等待子进程执行完毕后继续运行,实现进程间的同步,简单实现如下图程序框图。
在这里插入图片描述

3、实验三:多进程通过加锁互斥并发运行

这个在实验一的基础上,给父进程和子进程都加上锁,保证其中的一个在占用资源时不会被别的进程干扰。简单实现如下图程序框图。在这里插入图片描述

4、实验四:进程间通过信号机制实现软中断通信

通过父进程向子进程发送软中断信号,子进程才能继续运行,这个信号机制对于多进程间的同步和互斥也有相当大的作用。简单实现如下图程序框图。在这里插入图片描述

5、实验五:消息的发送与接收

一、当进程要发送消息给另一个进程时,先要向系统申请一个缓冲区,然后把消息写进去,接着把该缓冲区连到接收进程的一个消息队列中,并通知接收者,接收进程可以在适当的时候从消息队列中取出消息,并释放该缓冲区。
二、所以经过以上分析,我们应该建立一个服务端负责接收消息,并且要先创建一个消息队列;还要建立一个客户端,负责发送消息。在这里插入图片描述

6、实验六:进程的共享存储区通信

一、共享存储通信是指在内存中分配一片空间作为共享存储区。需要进行通信的各个进程把共享存储区附加到自己的地址空间中,然后就像进行正常的操作一样对共享区中的数据进程进行读或写。
二、经过以上分析,我们首先要创建一个共享存储区,然后两个进程按序依次往里面读或写,这里就需要用到一些互斥手段,信号、互斥锁等等。
这里使用的是服务端总是设置该存储区取值为-1,只有取值为-1时客户端才可以对存储区进行修改,服务端在不是-1的情况下可以去接收信息,并且在接收信息后要重新赋值-1,这里其实用到了信号量对多个进程互斥的思想。在这里插入图片描述

六、实验数据及源代码(学生必须提交自己设计的程序源代码,并有注释,源代码电子版也一并提交),包括思考题的程序。

目录:
实验一:
——experiment1_0.c
——experiment1_1.c
——experiment1_2.c
实验二:
——experiment2_0.c
实验三:
——experiment3_0.c
——experiment3_1.c
实验四:
——experiment4_0.c
——experiment4_1.c
——experiment4_2.c
实验五:
——server.c
——client.c
实验六:
——experiment6_0.c

实验一代码

experiment1_0.c

#include<stdio.h>
#include<unistd.h>
void main()
{
int p1,p2;
while((p1=fork())==-1); //创建子进程p1
if(p1==0)
{
putchar('b');
}
else
{
while((p2=fork())==-1); //创建子进程p2
if(p2==0)
{
putchar('c');
}
else
{
putchar('a');
}
}
}

experiment1_1.c

#include<stdio.h>
#include<unistd.h>
void main()
{
int p1,p2;
printf("hello world!\n");
while((p1=fork())==-1); //创建子进程p1
//父进程在这里继续运行,子进程从这里开始。
if(p1==0)
{
putchar('b');
}
else
{
while((p2=fork())==-1); //创建子进程p2
if(p2==0)
{
putchar('c');
}
else
{
putchar('a');
}
}
}

experiment1_2.c

#include<stdio.h>
#include<unistd.h>
void main()
{
int p1,p2,i;
while((p1=fork())==-1); //创建子进程p1
//父进程在这里继续运行,子进程从这里开始。
if(p1==0)
{ for(i=0;i<10;i++)
printf("son1");
}
else
{
while((p2=fork())==-1); //创建子进程p2
if(p2==0)
{
for(i=0;i<10;i++)
printf("son2");
}
else
{
for(i=0;i<10;i++)
printf("parent");
}
}
}
实验二代码

experiment2_0.c

#include<stdio.h>
#include<unistd.h>
#include<stdlib.h>
#include<sys/wait.h>
void main()
{ 
int pid; 
pid=fork(); /*创建子进程*/
switch(pid) 
{
case -1: /*创建失败*/
printf("fork fail!\n");
exit(1); /*程序异常终止*/
case 0: /*子进程*/
execl("/bin/ls","ls","-1","-color",NULL); 
printf("exec fail!\n");
exit(1); /*程序异常终止*/
default: /*父进程*/
wait(NULL); /*同步*/
printf("ls completed !\n");
exit(0); /*程序正常终止*/
}
}



实验三代码

experiment3_0.c

#include <stdio.h>
#include <unistd.h>
void main()
{
int p1,p2,i;
while((p1=fork()) == -1); /*创建子进程p1*/
if (p1==0)
{
lockf(1,1,0); /*加锁,重点关注第二个参数,1表示锁定,0表示解锁*/
for(i=0;i<10;i++)
printf("left_son %d\n",i);
lockf(1,0,0); /*解锁*/
}
else
{
while((p2=fork()) == -1); /*创建子进程p2*/
if (p2 == 0)
{
lockf(1,1,0); /*加锁*/
for(i=0;i<10;i++)
printf("right_son %d\n",i);
lockf(1,0,0); /*解锁*/
}
else
{
lockf(1,1,0); /*加锁*/
for(i=0;i<10;i++)
printf("parent %d\n",i);
lockf(1,0,0); /*解锁*/
}
}
}

experiment3_1.c

#include <stdio.h>
#include <unistd.h>
void main()
{
int p1,p2,i;
while((p1=fork()) == -1); /*创建子进程p1*/
if (p1==0)
{
//lockf(1,1,0); /*加锁,重点关注第二个参数,1表示锁定,0表示解锁*/
for(i=0;i<10;i++)
printf("left_son %d\n",i);
//lockf(1,0,0); /*解锁*/
}
else
{
while((p2=fork()) == -1); /*创建子进程p2*/
if (p2 == 0)
{
//lockf(1,1,0); /*加锁*/
for(i=0;i<10;i++)
printf("right_son %d\n",i);
//lockf(1,0,0); /*解锁*/
}
else
{
//lockf(1,1,0); /*加锁*/
for(i=0;i<10;i++)
printf("parent %d\n",i);
//lockf(1,0,0); /*解锁*/
}
}
}



实验四代码

experiment4_0.c

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
void waiting(),next(),keep_alive();
int wait_mark;
void main()
{
int p1,p2,stdout;
while((p1=fork())==-1); /*创建子进程p1*/
if (p1>0)
{
while((p2=fork())==-1); /*创建子进程p2*/
if(p2>0)
{
wait_mark=1;
signal(SIGINT,next); /*接收到中断信号,next*/
waiting();
kill(p1,16); /*向p1发软中断信号16*/
kill(p2,17); /*向p2发软中断信号17*/
wait(NULL); /*同步*/
wait(NULL);
printf("Parent process is killed!\n");
exit(0);
}
else
{
wait_mark=1;
signal(SIGINT, keep_alive); /*接收到中断信号,保持静止*/
signal(17,next); /*接收到软中断信号17,next*/
waiting();
printf("Child process2 is killed by parent!\n");
exit(0);
}
}
else
{
wait_mark=1;
signal(SIGINT, keep_alive);/*接收到中断信号,保持静止*/
signal(16,next); /*接收到软中断信号16,next*/
waiting();
printf("Child process1 is killed by parent!\n");
exit(0);
}
}

void waiting( )
{
while(wait_mark!=0);
}

void next( )
{
wait_mark=0;
}

void keep_alive() 
{
}


experiment4_1.c

experiment4_1.c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
void waiting(),next(),keep_alive();
int wait_mark;
void main()
{
int p1,p2,stdout;
while((p1=fork())==-1); /*创建子进程p1*/
if (p1>0)
{
while((p2=fork())==-1); /*创建子进程p2*/
if(p2>0)
{
wait_mark=1;
signal(SIGINT,next); /*接收到中断信号,next*/
waiting();
//kill(p1,16); /*向p1发软中断信号16*/
//kill(p2,17); /*向p2发软中断信号17*/
wait(NULL); /*同步*/
wait(NULL);
printf("Parent process is killed!\n");
exit(0);
}
else
{
wait_mark=1;
signal(SIGINT, keep_alive); /*接收到中断信号,保持静止*/
signal(17,next); /*接收到软中断信号17,next*/
waiting();
printf("Child process2 is killed by parent!\n");
exit(0);
}
}
else
{
wait_mark=1;
signal(SIGINT, keep_alive);/*接收到中断信号,保持静止*/
signal(16,next); /*接收到软中断信号16,next*/
waiting();
printf("Child process1 is killed by parent!\n");
exit(0);
}
}

void waiting( )
{
while(wait_mark!=0);
}

void next( )
{
wait_mark=0;
}

void keep_alive() 
{
}

experiment4_2.c

#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <sys/wait.h>
#include <stdlib.h>
void waiting(),next(),keep_alive();
int wait_mark;
void main()
{
int p1,p2,stdout;
while((p1=fork())==-1); /*创建子进程p1*/
if (p1>0)
{
while((p2=fork())==-1); /*创建子进程p2*/
if(p2>0)
{
wait_mark=1;
//signal(SIGINT,next); /*接收到中断信号,next*/
waiting();
kill(p1,16); /*向p1发软中断信号16*/
kill(p2,17); /*向p2发软中断信号17*/
wait(NULL); /*同步*/
wait(NULL);
printf("Parent process is killed!\n");
exit(0);
}
else
{
wait_mark=1;
//signal(SIGINT, keep_alive); /*接收到中断信号,保持静止*/
//signal(17,next); /*接收到软中断信号17,next*/
waiting();
printf("Child process2 is killed by parent!\n");
exit(0);
}
}
else
{
wait_mark=1;
//signal(SIGINT, keep_alive);/*接收到中断信号,保持静止*/
//signal(16,next); /*接收到软中断信号16,next*/
waiting();
printf("Child process1 is killed by parent!\n");
exit(0);
}
}

void waiting( )
{
while(wait_mark!=0);
}

void next( )
{
wait_mark=0;
}

void keep_alive() 
{
}



实验五代码

server.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#define MSGKEY 75
struct msgform
{ 
long mtype;
char mtext[1000];
}msg;
int msgqid;

void server()
{
msgqid=msgget(MSGKEY,0777|IPC_CREAT); /*创建75#消息队列*/
do
{
msgrcv(msgqid,&msg,1030,0,0); /*接收消息*/
printf("(server)received\n");
}
while(msg.mtype!=1);
msgctl(msgqid,IPC_RMID,0); /*删除消息队列,归还资源*/
exit(0);
}

void main()
{ 
server();
}

client.c

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/msg.h>
#include <sys/ipc.h>
#define MSGKEY 75
struct msgform
{
long mtype;
char mtex[1000];
}msg;
int msgqid;

void client()
{
int i;
msgqid=msgget(MSGKEY,0777); /*打开75#消息队列*/
for(i=10;i>=1;i--)
{
msg.mtype=i;
printf("(client)sent\n");
msgsnd(msgqid,&msg,1024,0); /*发送消息*/
}
exit(0);
}

void main()
{ 
client();
}



实验六代码

experiment6_0.c

#include <sys/shm.h>
#include <sys/ipc.h>
#include <sys/wait.h>
#include <unistd.h>
#include <stdlib.h>
#include <stdio.h>

#define SHMKEY 75 /*共享存储区名字*/
int shmid, i;
int *addr;

void client()
{
int i;
shmid = shmget(SHMKEY, 1024, 0777| IPC_CREAT); /*创建一个共享存储区,名字为75,大小为1024字节,不重复创建*/
addr = shmat(shmid, 0, 0); /*共享存储区所附接到的进程虚地址(首地址)*/
for(i = 9; i>=0; i--)
{
while(*addr != -1); /*确保服务端收到了一条信息,再发下一条*/
printf("(client)sent\n");
*addr = i;
}
exit(0);
}

void server()
{
shmid = shmget(SHMKEY, 1024, 0777 | IPC_CREAT); /*创建一个共享存储区,名字为75,大小为1024字节,不重复创建*/
addr = shmat(shmid, 0, 0);/*共享存储区所附接到的进程虚地址(首地址)*/
do
{
*addr = -1;
while(*addr == -1);/*响应客户端,addr到0退出循环*/
printf("(server)received\n");
}while(*addr);
shmctl(shmid, IPC_RMID, 0); /*撤消共享存储区,归还资源*/
exit(0);
}

void main()
{
while((i = fork()) == -1);/*创建子进程*/
if(!i) server();/*子进程运行server*/
system("ipcs -m");/*共享内存资源*/
while((i = fork()) == -1);/*创建子进程*/
if(!i) client();/*子进程运行client*/
wait(0);
wait(0);
}

七、实验结果分析(截屏的实验结果,与实验结果对应的实验分析)

1、实验一:进程创建与进程并发执行

见附录代码:experiment1_0.c

在这里插入图片描述

以上实验是经过了很多次尝试才能得到的结论,然后发现大部分都是acb,极少概率出现abc,但从课本和网上收集到的理论来说,调用fork()后,我们并不能确定父、子进程谁会率先访问CPU,但从该输出来说,这并不能够反映这个输出的不确定性。
由于先后顺序跟系统的这个优先级调度有关,最后我在《Linux/UNIX系统编程手册》24.4 fork()之后的竞争条件(Race Condition)中找到了准确的答案。
在这里插入图片描述
可以得出结论:这个先后顺序是由Linux系统内的一些设置有关,哪个进程先执行要看系统的进程调度策略。

以上结论都是基于单核CPU资源的情况下的结论,我们尝试着把内核调至4个,这样就可以支持多线程并行了。
在这里插入图片描述
我们在对内核进行添加至四个后,重新执行该代码,可以发现所有排列组合都是有可能发生的,因此可以得出结论:在CPU内核资源充足的情况下,多进程执行结果是带有不确定性的,有abc、bac、bca等等,从进程并发执行来看,各种情况都有可能,上面的三个进程没有同步措施,所以父进程与子进程的输出内容会叠加在一起。输出次序会带有随机性。

思考题
(1)系统是怎样创建进程的?
创建进程时要执行创建进程的系统调用函数(如第一个实验中的fork函数),其主要操作应该分为以下四步:
① 申请一个空闲的PCB。
② 为新进程分配资源。
③ 初始化新进程的PCB。
④ 将新进程加到就绪队列中。
不同的操作系统会采用不同的实现方式来创建进程。我们主要针对该实验的Linux系统进行分析:父进程利用fork系统调用来创建新进程;父进程创建子进程时,会把自己的地址空间制作成一个副本;两个进程都可以继续执行fork系统调用之后的指令,但有一个地方需要注意,fork成功后,会返回两次,子进程返回的是0,而父进程返回的是子进程的PID。

(2)当首次调用新创建进程时,其入口在哪里?
在这里插入图片描述
如图,父子进程之间的一个执行流程,首次调用新创建进程时,其入口在fork()的下一句程序。子进程和父进程运行相同的代码,但是有各自的数据空间。

我们用下图的程序作为结论证明:
见附录代码:experiment1_1.c
在这里插入图片描述
我们额外地补充了一句输出。
从下图的运行结果来看,只输出了一句“hello world!”,可以看到除了父进程执行该语句,两个子进程都不会执行,因此证明了我们的结论。
在这里插入图片描述

(3)利用strace 和ltrace -f -i -S ./executable-file-name查看程序执行过程,并分析原因,画出进程家族树。
strace ./executable-file-name 打印程序运行执行过程
在这里插入图片描述
ltrace -S ./executable-file-name 和上面的命令相似,把程序运行时所做的系统调用都打印出来了。
在这里插入图片描述
ltrace -i ./executable-file-name 跟踪当前进程
在这里插入图片描述
ltrace -f ./executable-file-name 除了跟踪当前进程外,还跟踪其子进程,非常直观、简洁。
在这里插入图片描述
由于只输出一个字母太快了,我们可能得尝试很多次才能看到程序执行的多种情况,因此我们对代码进行如下调整:
见附录代码:experiment1_2.c
在这里插入图片描述
使用ltrace -f ./executable-file-name 观察进程并发执行的过程
单核CPU资源Linux环境下输出:
在这里插入图片描述
单核下只能通过循环多打印的条件下才能快速地看清多进程之间对于CPU资源的争夺。
四核CPU资源Linux环境下输出:
在这里插入图片描述
可以看到多进程在四核CPU资源下竞争更为激烈。

分析原因:当多个进程并发执行,但对执行顺序又没有进行同步控制时,由于多进程无序地抢占系统资源,运行结果就会出现不确定性,各种情况都有可能。导致实验结果中各个进程的输出次序带有随机性。

画出进程家族树:
在这里插入图片描述
注:通过以上实验得出结论,由于Linux对于单核CPU的进程调度有一些调整,不能很好地反映输出的不确定性,以下的实验均使用四核CPU的Linux环境下执行。

2、实验二:进程的睡眠、同步、撤消等进程控制

见附录代码:experiment2_0.c
在这里插入图片描述
子进程被创建后,使用execlp系统调用,用一个程序(如可执行文件)取代原来内存空间的内容,子进程用exec( )装入命令ls ,exec( )后,子进程的代码被ls的代码取代,这时子进程的PC指向ls的第1条语句,开始执行ls的命令代码。
父进程执行wait系统调用,把自己插入阻塞(睡眠)队列中,等待子进程的终止。

思考题
(1)可执行文件加载时进行了哪些处理?
可执行文件的加载:
(1)UNIX/Linux系统中,在shell命令行提示符后输入命令:$./exp[enter](注:exp是我人为指定的gcc编译后的文件名)。
(2)shell命令行解释器构造argv和envp。
(3)调用fork()函数,创建一个子进程,与父进程shell完全相同(只读/共享),包括只读代码段、可读写数据段、堆以及用户栈等。
(4)调用exec()函数执行另一个程序。当进程调用exec函数时,该进程用户空间资源(正文、数据、堆和栈)完全由新程序替代,新程序则从main函数开始执行。因为调用exec函数并没有创建新的进程,所以前后的进程ID并没有改变,也即内核信息基本不做修改。

可执行文件的生成应经过以下步骤:C源程序一>编译预处理一>编译一>优化程序一>汇编程序一>链接程序一>可执行文件

exec系列函数有好几种,我们重点介绍实验2中使用的execl函数:
int execl(const char *pathname, const char arg0, … / (char *)0 */ );
execl()函数用来执行参数path字符串所指向的程序,第二个及以后的参数代表执行文件时传递的参数列表,最后一个参数必须是空指针以标志参数列表为空。

(2)什么是进程同步?wait( )是如何实现进程同步的?
进程同步:在多道程序环境下,进程是并发执行的,不同进程之间存在着不同的相互制约关系。
我们把异步环境下的一组并发进程因直接制约而互相发送消息、进行互相合作、互相等待,使得各进程按一定的速度执行的过程称为进程间的同步。具有同步关系的一组并发进程称为合作进程,合作进程间互相发送的信号称为消息或事件。 如果我们对一个消息或事件赋以唯一的消息名,则我们可用过程wait (消息名),表示进程等待合作进程发来的消息,从而实现进程同步。
我们对该实验2进行进一步分析:wait的函数原型为pid_t wait(int *status)  
;进程一旦调用了wait,就立即阻塞自己,由wait等待任何要僵死的子进程,(参数staus表示进程退出状态),若成功等到则返回终止进程的PID;否则返回-1,wait就会一直阻塞在这里,直到有一个出现为止。如果我们不需要记录这个子进程的退出状态,只想把这个僵尸进程消灭掉,我们就可以设定这个参数为NULL,即wait(NULL)。

(3)wait( )和exit()是如何控制实验结果的随机性的?
wait( )保证了父进程会等待子进程结束后才继续运行;exit()保证了在创建失败或者exec失败的情况下能够异常退出,保证wait(NULL)能够无论接收到正常还是异常等终止指令,都能使父进程正常运行。因此这两个函数通过等待和终止控制了实验结果的随机性。

3、实验三:多进程通过加锁互斥并发运行

见附录代码:experiment3_0.c
加锁后能保证各个进程能有序地执行,不会由于多进程并发,导致中途被插入。
在这里插入图片描述

思考题
(1)进程加锁和未上锁的输出结果相同吗? 为什么?
见附录代码:experiment3_1.c
在experiment3_0.c 加锁的基础上把锁给去掉,以下为输出结果。
在这里插入图片描述
可以得到结论:
进程加锁和未上锁的输出结果不相同。(但如果执行内容减少为输出一句,则有可能输出结果相同,因为在CPU一个时间片内就快速地完成了一个进程内的多个线程任务)
因为CPU属于临界资源,多进程并发执行时,就会形成竞争,各个进程争夺CPU资源,单CPU进行进程调度的时候,需要读取上下文+执行程序+保存上下文,即进程切换。通过lockf函数可以保证在当前指令完成之前,其他进程不能访问该单元,以实现进程之间的互斥。

4、实验四:进程间通过信号机制实现软中断通信

见附录代码:experiment4_0.c
在这里插入图片描述

思考题
(1)为了得到实验内容要求的结果,需要用到哪些系统调用函数来实现及进程间的通信控制和同步?
使用signal( )和kill( )函数进行通信控制,使用wait( )和exit( )函数进行同步。

(2)kill( )和signal( )函数在信号通信中的作用是什么?如果分别注释掉它们,结果会如何?
进程用kill( )函数向一个进程或一组进程发送一个信号。即发送信号;
signal( )函数收到软中断后,然后处理软中断信号,即处理信号。

注释掉kill()函数,见附录代码:experiment4_1.c
在这里插入图片描述
当注释掉kill()函数,两个子进程就无法接收到软中断信号,就会一直保持等待,因为无法终止子进程,父进程的wait()函数也无法等到僵死的子进程,一直阻塞在这里。因此,结果如上图所示,程序进入了阻塞状态。

注释掉signal()函数,见附录代码:experiment4_2.c 

在这里插入图片描述
当注释掉siganl()函数,父进程和两个子进程接收到中断信号,因没有signal()指定其他函数操作,因此均提前中断程序,得到的结果就如上图所示,全部被中断信号杀死。

5、实验五:消息的发送与接收

见附录代码:server.c和client.c
在这里插入图片描述
客户端和服务端两个程序分别在两个终端运行。首先运行服务端,会创建一个消息队列并且不断循环检查队列里是否有新的消息,客户端运行后,会向服务端的消息队列发送消息,客户端接收后作出回应。
思考题
(1)为了便于操作和观察结果,需要编制几个程序分别用于消息的发送与接收?
可以编制两个程序,server.c和client.c,server.c负责接收,client.c负责发送。

(2)这些程序如何进行编辑、编译和执行?为什么?
文本编辑:server.c client.c分别编辑两个文件的代码
gcc编译: gcc client.c -o cli;gcc server.c -o ser
使用两个终端分别执行:先执行./ser,再执行./cli

(3)如何实现消息的发送与接收的同步?
sever.c先创建一个消息队列,然后通过循环开始等待消息队列内是否有新的消息。接着client.c开始往消息队列发送消息,这时候sever.c就会从消息队列中读取进行响应,接收完毕之后,sever.c需要删除消息队列,归还资源。

6、实验六:进程的共享存储区通信

在这里插入图片描述
一个子进程首先开辟一个共享的存储区,另一个子进程添加该存储区到自己的地址空间中,两个进程可以正常往里面读或写,然后设置信号来达到进程互斥的目的,保证一个进程在对该存储区进行操作的同时,另一个进程不会操作,达到发送消息和接收信息一来一回的目的。

思考题
(1)为了便于操作和观察结果,需要如何合理设计程序来实现子进程间的共享存储区通信?
首先开辟一个共享的存储区,然后各个进程需要把这个共享存储区附加到自己的地址空间中,然后就可以进行正常操作一样对共享区中的数据进程进行读或写操作。
然后就是考虑进程的同步问题,针对该问题,可以使用信号,信号量,互斥锁来进行同步,但是信号和信号量需要两个进程都实现一套自己的逻辑(访问临界区前,先检查冲突标志,如果没有冲突则访问,并向其它的所有进程依次发送信号,告诉它们我要开始访问了;如果有冲突则阻塞等待。

(2)比较消息通信和共享存储区通信这两种进程通信机制的性能和优缺点。
消息通信的性能优于管道通信,但次于共享存储区通信;其优点是扩大了信息传递能力;缺点是系统要开设多个缓冲区,占用内存空间,增加对缓冲区的管理秩序,对于缓冲区和消息队列的管理要涉及复杂的同步关系,给系统增加了复杂性和调度负担。

共享存储区通信因为数据不需要在客户进程和服务器进程之间复制,因此
是最快的 IPC 方式
,它是针对其他进程间通信方式运行效率低而专门设计的。它往往与其他通信机制,如信号量,配合使用,来实现进程间的同步和通信。其有点总结以下四点:
(1)与常用的集中式多处理机使用的通信机制兼容。
(2)当处理器通信方式复杂或程序执行动态变化时易于编程,同时在简化编译器设计方面也占有优势。
(3)当通信数据较小时,通信开销较低,带宽利用较好。
(4)通过硬件控制的Cache减少了远程通信的频度,减少了通信延迟以及对共享数据的访问冲突。
其缺点是没有像消息通信的一些消息传递机制,需要配合信号量等一些同步、互斥机制使用,不便于编程实现。

猜你喜欢

转载自blog.csdn.net/weixin_43999137/article/details/107565099
今日推荐