有些实验截图因为太长了并未给出,但是应该不影响阅读。
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的共享存储区发送和接收的程序,并设计对该共享存储区进行互斥访问及进程同步的措施,必须保证实现正确的通信。
1、编写一段程序,使用系统调用fork( )创建两个子进程。当此程序运行时,在系统中有一个父进程和两个子进程并发执行。
实验思路:
(1)如果fork( )调用成功,进程创建完成,此时OS在内存中建立一个新进程,所建的新进程是调用fork( )父进程(parent process)的副本,称为子进程(child process)。
(2)子进程继承了父进程的许多特性,并具有与父进程完全相同的用户级上下文。父进程与子进程并发执行,我们通过返回值来确认当前进程是父进程还是子进程。
① 在子进程中,pid变量保存的fork( )返回值为0,表示当前进程是子进程。
② 在父进程中,pid变量保存的fork( )返回值为子进程的id值(进程唯一标识符)。
也就是说,如果我们要调用fork()创建两个子进程,那么我们得调用两次fork()函数来创建两个子进程,通过返回值来判断哪个是父进程和哪个是子进程。
(3)为了确认系统是如何调用进程的,入口在哪里,程序需要打印出当前进程的父进程与子进程以便观察。
(4)进行单CPU运行和多CPU运行对照实验,有无同步控制对照试验,父进程和两个子进程输出一次/输出两次验证实验。
① 无同步控制:当多个进程并发执行,但对执行顺序又没有进行同步控制时,由于多进程无序地抢占系统资源,运行结果就会出现不确定性,各种情况都有可能。导致实验结果中各个进程的输出次序带有随机性。
② 有同步控制:使用临界区实现多线程同步。当有线程进入临界区段时,其他线程或是进程必须等待,以确保这些共用资源是被互斥获得使用。
(5)多次进行实验观察输出次序。
实验过程:
(1)建立fork-1.c文档,编写父进程和两个子进程输出一次的无同步控制代码。
(2)建立fork-2.c文档,编写父进程和两个子进程输出两次的无同步控制代码。
(3)建立fork-3.c文档,编写父进程和两个子进程输出两次的有同步控制代码。
(4)对fork-1.c、fork-2.c文档编译,在单CPU和多CPU的虚拟机上运行。对fork-3.c文档编译,在多CPU的虚拟机上运行。
(5)多次输出观察实验结果。
2、用fork( )创建一个进程,再调用exec( ),用新的程序替换该子进程的内容,利用wait( )来控制进程执行顺序,掌握进程的睡眠、同步、撤消等进程控制方法。
实验思路:
(1)该问题需要实现进程的睡眠、同步、撤销等进程控制方法,把问题逐个分解。
(2)睡眠:父进程在调用fork( )建立一个子进程后,马上调用wait( ),使父进程在子进程结束之前,一直处于睡眠状态。子进程用exec( )装入命令ls ,exec( )后,子进程的代码被ls的代码取代,这时子进程的PC指向ls的第1条语句,开始执行ls的命令代码。
(3)同步:使用wait()函数使父进程和子进程运行时同步。父进程调用wait(NULL)语句等待子进程运行结束。如果子进程没有完成,父进程一直等待。wait( )将调用进程挂起,直至其子进程因暂停或终止而发来软中断信号为止。如果在wait( )前已有子进程暂停或终止,则调用进程做适当处理后便返回。
(4)撤销:为了及时回收进程所占用的资源并减少父进程的干预,UNIX/LINUX利用exit( )来实现进程的自我终止,通常父进程在创建子进程时,应在进程的末尾安排一条exit( ),使子进程自我终止。exit(0)表示进程正常终止,exit(1)表示进程运行有错,异常终止。
实验过程:
(1)建立wait.c文档,编写题目要求程序。
① 调用fork()函数建立子进程。
② 调用excel()函数,用新的程序替换该子进程的内容。
③ 在父进程运行初期使用wait()函数,使父进程和子进程运行时同步。
(2)编译wait.c文档,运行,查看实验结果。
3、编写一段多进程并发运行的程序,用lockf( )来给每一个进程加锁,以实现进程之间的互斥。
实验思路:
(1)多进程并发运行:使用forc()函数建立两个子进程,如此,两个子进程和父进程加起来一共有三个进程,构成多进程。
(2)加锁:在每个进程运行内容输出前加上lockf(1,1,0)进行加锁操作。
(3)解锁:在每个进程运行内容输出后加上lockf(1,0,0)进行解锁操作。
实验过程:
(1)建立mutex.c文档,编写题目要求程序。
① 调用fork()函数建立两个子进程。
② 在每个子进程输出内容前面加锁。
③ 在每个子进程输出内容后面解锁。
(2)编译mutex.c文档,多次运行,查看实验结果。
4、编写程序:用fork( )创建两个子进程,再用系统调用signal( )让父进程捕捉键盘上来的中断信号(即按^c键);捕捉到中断信号后,父进程用系统调用kill( )向两个子进程发出信号,子进程捕捉到信号后分别输出下列信息后终止:
Child process1 is killed by parent!
Child process2 is killed by parent!
父进程等待两个子进程终止后,输出如下的信息后终止:
Parent process is killed!
实验思路:
实验过程:
(1)建立signal.c文档,编写题目要求程序。
① 调用fork()函数建立两个子进程。
② 在父进程建立软中断信号,在接受到^c之后杀死两个子进程。
(2)编译signal.c文档,运行,查看实验结果。
5、使用系统调用msgget( ),msgsnd( ),msgrev( ),及msgctl( )编制一长度为1k的消息发送和接收的程序,并分析消息的创建、发送和接收机制及控制原理。
实验思路:
(1)为了便于操作和观察结果,编写两个程序 client.c 和 server.c,分别用于消息的发送与接收。
(2)server 建立一个 Key 为 75 的消息队列,等待其它进程发来的消息。当遇到类型为1的消息,则作为结束信号,取消该队列,并退出 server。 server 每接收到一个消息后显示一句 “(server)received。”
(3)client 使用 key 为 75 的消息队列,先后发送类型从 10 到 1 的消息,然后退出。最后一个消息,即是 server 端需要的结束信号。 client 每发送一条消息后显示一句 “(client)sent”。
实验过程:
(1)建立client.c、server.c文档,编写题目要求程序。
① server 建立一个 Key 为 75 的消息队列,等待其它进程发来的消息。
② client 使用 key 为 75 的消息队列,先后发送类型从 10 到 1 的消息,然后退出。
(2)编译client.c、server.c文档,运行,查看实验结果。
6、编制一长度为1k的共享存储区发送和接收的程序,并设计对该共享存储区进行互斥访问及进程同步的措施,必须保证实现正确的通信。
实验思路:
实验过程:
(1)建立share.c文档,编写题目要求程序。
① server 建立一个flag 为 0777 的消息队列,等待其它进程发来的消息。
② client 使用 flag为 0777 的消息队列,不断给server发送信息。
(2)编译share.c文档,运行,查看实验结果。
1、实验代码:
fock-1.c
#include
#include
void main()
{
int p1,p2;
while((p1=fork())==-1);//如果返回值等于-1,说明fork()函数执行失败
if(p1==0) //如果返回值等于0,说明当前执行的是子进程
printf("Child_1 PID = %d,Ret = %d\n",getpid(),p1);
else
{
while((p2=fork())==-1);
if(p2==0)
printf("Child_2 PID = %d,Ret = %d\n",getpid(),p2);
else
printf("Parent PID = %d,Ret = %d and %d\n",getpid(),p1,p2);
}
}
fork-2.c
#include
#include
void main()
{
int p1,p2;
for(int i =0;i<2;i++)
{
while((p1=fork())==-1);//如果返回值等于-1,说明fork()函数执行失败
if(p1==0) //如果返回值等于0,说明当前执行的是子进程
printf("id = %d,Child_1 PID = %d,Ret = %d\n",i,getpid(),p1);
else
{
while((p2=fork())==-1);
if(p2==0)
printf("id = %d,Child_2 PID = %d,Ret = %d\n",i,getpid(),p2);
else
printf("id = %d,Parent PID = %d,Ret = %d and %d\n",i,getpid(),p1,p2);
}
}
}
fork-3.c
#include
#include
#include
#include
#include
// add mutex
pthread_mutex_t mutex = PTHREAD_MUTEX_INITIALIZER;
void main()
{
int p1,p2;
printf("ID\tson/pa\tPPID\tPID\tFID1\tFID2\n");
// ID表示当前的循环次数,PPID表示当前进程的父进程PID,PID表示当前进程的PID
// FID表示fork()返回给当前进程的值
for(int i =0;i<2;i++)
{
while((p1=fork())==-1);//如果返回值等于-1,说明fork()函数执行失败
if(p1==0) //如果返回值等于0,说明当前执行的是子进程
{
if(pthread_mutex_lock(&mutex) != 0)
{
perror("pthread_mutex_lock");
exit(EXIT_FAILURE);
}
printf("%d\tson\t%d\t%d\t%d\n",i,getppid(),getpid(),p1);
if(pthread_mutex_unlock(&mutex) != 0)
{
perror("pthread_mutex_unlock");
exit(EXIT_FAILURE);
}
}
else
{
while((p2=fork())==-1);
if(p2==0)
{
if(pthread_mutex_lock(&mutex) != 0)
{
perror("pthread_mutex_lock");
exit(EXIT_FAILURE);
}
printf("%d\tson\t%d\t%d\t%d\n",i,getppid(),getpid(),p2);
if(pthread_mutex_unlock(&mutex) != 0)
{
perror("pthread_mutex_unlock");
exit(EXIT_FAILURE);
}
}
else
{
if(pthread_mutex_lock(&mutex) != 0)
{
perror("pthread_mutex_lock");
exit(EXIT_FAILURE);
}
printf("%d\tparent\t%d\t%d\t%d\t%d\n",i,getppid(),getpid(),p1,p2);
if(pthread_mutex_unlock(&mutex) != 0)
{
perror("pthread_mutex_unlock");
exit(EXIT_FAILURE);
}
}
}
}
}
2、实验代码:
wait.c
#include
#include
#include
#include
#include
int main()
{
int pid;
pid = fork();//创建子进程
switch(pid)
{
case -1: //创建失败
printf("fork fail!\n");
exit(1);
case 0: //子进程
execl("/bin/ls","ls","-1","-color",NULL);
exit(1);
default: //父进程
wait(NULL); //同步
printf("ls comleted!\n");
exit(0);
}
}
3、实验代码:
mutex.c
#include
#include
int main( )
{
int p1,p2,i;
while((p1=fork( ))== -1); // 创建子进程 p1
if (p1==0)
{
/* lockf( )
第一个参数是文件描述符,第二个参数是锁定和解锁:1表示锁定,0表示解锁。
第三个参数为0表示从文件的当前位置到文件尾。
*/
lockf(1,1,0); // 加锁
for(i=0;i<10;i++)
printf("Child_1 %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("Child_1 %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); // 解锁
}
}
}
思考题代码:
Unlock.c
#include
#include
int main( )
{
int p1,p2,i;
while((p1=fork( ))== -1); /* 创建子进程 p1*/
if (p1==0)
{
for(i=0;i<10;i++)
printf("Child_1 %d\n",i);
}
else
{
while((p2=fork( ))==-1); /* 创建子进程 p2*/
if (p2==0)
{
for(i=0;i<10;i++)
printf("Child_2 %d\n",i);
}
else
{
for(i=0;i<10;i++)
printf("Parent %d\n",i);
}
}
}
4、实验代码:
signal.c
#include
#include
#include
#include
#include
#include
int wait_mark;
void waiting( )
{
//等待。直到wait_mark=0时结束等待
while(wait_mark!=0);
signal(SIGINT,SIG_IGN);
}
void mark_to_zero( )
{
//signal()调用函数
wait_mark=0;
}
int 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,mark_to_zero); // 接收到 ^c 信号
waiting(); // 确保wait_mark置0之后执行kill操作,否则等待
signal(SIGALRM,mark_to_zero);// 接受SIGALRM信号
kill(p1,SIGUSR1); // 向 p1 发软中断信号SIGUSR1
kill(p2,SIGUSR2); // 向 p2 发软中断信号SIGUSR2
wait(0); // 等待两个进程中止
wait(0);
printf("Parent process is killed!\n");
exit(0); // 进程正常中止
}
else
{
wait_mark=1;
signal(SIGUSR2,mark_to_zero); // 接收到软中断信号SIGUSR2
signal(SIGINT,SIG_IGN);// 忽略^c信号
waiting(); // 确保wait_mark置0之后执行kill操作,否则等待
lockf(stdout,1,0); // 加锁
printf("Child process2 is killed by parent!\n");
lockf(stdout,0,0); // 解锁
exit(0); // 进程正常中止
}
}
else
{
wait_mark=1;
signal(SIGUSR1,mark_to_zero); // 接收到软中断信号 16 SIGUSR1
signal(SIGINT,SIG_IGN);// 忽略^c信号
waiting();
lockf(stdout,1,0); // 加锁
printf("Child process1 is killed by parent!\n");
lockf(stdout,0,0); // 解锁
exit(0); // 进程正常中止
}
}
思考题代码:
signal-1.c
#include
#include
#include
#include
#include
#include
int wait_mark;
void waiting( )
{
//等待。直到wait_mark=0时结束等待
while(wait_mark!=0);
signal(SIGINT,SIG_IGN);
}
void mark_to_zero( )
{
//signal()调用函数
wait_mark=0;
}
int 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,mark_to_zero); // 接收到 ^c 信号
waiting(); // 确保wait_mark置0之后执行kill操作,否则等待
signal(SIGALRM,mark_to_zero);// 接受SIGALRM信号
//kill(p1,SIGUSR1); // 向 p1 发软中断信号SIGUSR1
//kill(p2,SIGUSR2); // 向 p2 发软中断信号SIGUSR2
wait(0); // 等待两个进程中止
wait(0);
printf("Parent process is killed!\n");
exit(0); // 进程正常中止
}
else
{
wait_mark=1;
signal(SIGUSR2,mark_to_zero); // 接收到软中断信号SIGUSR2
signal(SIGINT,SIG_IGN);// 忽略^c信号
waiting(); // 确保wait_mark置0之后执行kill操作,否则等待
lockf(stdout,1,0); // 加锁
printf("Child process2 is killed by parent!\n");
lockf(stdout,0,0); // 解锁
exit(0); // 进程正常中止
}
}
else
{
wait_mark=1;
signal(SIGUSR1,mark_to_zero); // 接收到软中断信号 16 SIGUSR1
signal(SIGINT,SIG_IGN);// 忽略^c信号
waiting();
lockf(stdout,1,0); // 加锁
printf("Child process1 is killed by parent!\n");
lockf(stdout,0,0); // 解锁
exit(0); // 进程正常中止
}
}
signal-2.c
#include
#include
#include
#include
#include
#include
int wait_mark;
void waiting( )
{
//等待。直到wait_mark=0时结束等待
while(wait_mark!=0);
signal(SIGINT,SIG_IGN);
}
void mark_to_zero( )
{
//signal()调用函数
wait_mark=0;
}
int 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,mark_to_zero); // 接收到 ^c 信号
waiting(); // 确保wait_mark置0之后执行kill操作,否则等待
//signal(SIGALRM,mark_to_zero);// 接受SIGALRM信号
kill(p1,SIGUSR1); // 向 p1 发软中断信号SIGUSR1
kill(p2,SIGUSR2); // 向 p2 发软中断信号SIGUSR2
wait(0); // 等待两个进程中止
wait(0);
printf("Parent process is killed!\n");
exit(0); // 进程正常中止
}
else
{
wait_mark=1;
//signal(SIGUSR2,mark_to_zero); // 接收到软中断信号SIGUSR2
//signal(SIGINT,SIG_IGN);// 忽略^c信号
waiting(); // 确保wait_mark置0之后执行kill操作,否则等待
lockf(stdout,1,0); // 加锁
printf("Child process2 is killed by parent!\n");
lockf(stdout,0,0); // 解锁
exit(0); // 进程正常中止
}
}
else
{
wait_mark=1;
//signal(SIGUSR1,mark_to_zero); // 接收到软中断信号 16 SIGUSR1
//signal(SIGINT,SIG_IGN);// 忽略^c信号
waiting();
lockf(stdout,1,0); // 加锁
printf("Child process1 is killed by parent!\n");
lockf(stdout,0,0); // 解锁
exit(0); // 进程正常中止
}
}
5、实验代码:
client.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#define MSGKEY 75
struct msgform
{
long mtype;
char mtext[1000];
}msg;
int msgqid;
void client()
{
msgqid = msgget(MSGKEY,0777);//打开 75#消息队列
for(int i = 10;i>=1;i--)
{
msg.mtype = i;
printf("(client)sent\n");
msgsnd(msgqid,&msg,1024,0);//发送消息
}
exit(0);
}
int main()
{
client();
}
server.c
#include
#include
#include
#include
#include
#include
#include
#include
#include
#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);
}
int main()
{
server();
}
6、实验代码:
#include
#include
#include
#include
#include
#include
#include
#define SHMKEY 75
int shmid,p;
int *addr;
void client()
{
shmid = shmget(SHMKEY,1024,0777);//打开共享存储区,SHMKEY是共享存储区的名字,1024是其大小(以字节计),0777是用户设置的标志
/*shmid是共享存储区的标识符.
第一个0是用户给定的,将共享存储区附接到进程的虚地址空间规定共享存储区的读、写权限,以及系统是否应对用户规定的地址做舍入操作。
第二个0表示可读、可写*/
addr = shmat(shmid,0,0);//打开共享存储区
for(int 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);//创建共享存储区,SHMKEY是共享存储区的名字,1024是其大小(以字节计),0777|IPC_CREAT是用户设置的标志
addr = shmat(shmid,0,0);//获取首地址
do
{
*addr = -1;//指针指向的空间赋值为-1,表示已经接受了此条信息
while(*addr == -1);
printf("(server)received\n");
}while(*addr);
shmctl(shmid,IPC_RMID,0);//写入共享存储区,shmid是共享存储区的标识符,IPC_RMID是操作命令,0是用户缓冲区地址
exit(0);
}
int main()
{
while((p=fork())==-1);//建立子进程1
if(!p)server();//启动服务器
system("ipcs -m");//操作命令
while((p=fork())==-1);//建立子进程2
if(!p)client();//启动客户端
wait(0);//同步
wait(0);
}
1、实验结果与实验程序、实验步骤、实验原理、操作系统原理的对应分析;
2、不同条件下的实验结果反应的问题及原因;
3、实验结果的算法时间、效率、鲁棒性等性能分析。
1、编写一段程序,使用系统调用fork( )创建两个子进程。当此程序运行时,在系统中有一个父进程和两个子进程并发执行。
(1)分析图7-1-1和图7-1-2,在单CPU运行时parent、child_1、child_2的输出顺序相对固定,在多CPU运行时,parent、child_1、child_2的输出顺序是随机的。分析图1-3和图1-4,单CPU时总是父进程先输出,子进程后输出。多CPU时,父进程和子进程的输出顺序是随机的。单CPU运行时只有一个CPU在运行,CPU会轮流给每个线程分配时间片,时间片分配到哪个线程头上,哪个线程里的代码就执行。多核CPU同时执行多个线程里的代码,CPU之间争抢输出机会,每次运行程序时各CPU争抢到的输出的时间片不同,输出结果各不相同。
(2)无论是单CPU模式还是多CPU模式,程序总是先建立父进程,然后再创建子进程。由进程的PID号可以推断出,程序建立子进程时,先建立child_1,再建立child_2。这是由fork()函数创建规则所决定的。
(3)拿图7-1-5最后一个程序运行结果对输出次数为两次的情况进行分析,会发现有些子进程的父进程是PID1042,从parent的FID1和FID2判断,子进程PID2413和子进程PID2415的父进程应该是PID2409,子进程PID2412的父进程应该是PID2407。这是因为PID2407和PID2409执行完第二个循环后,main函数就该退出了,也即进程该死亡了,因为它已经做完所有事情了。PID2407和PID2409死亡后,PID2413,PID2415就没有父进程了,这在操作系统是不被允许的,所以PID2413,PID2415的父进程就被置为PID1042了。
2、用fork( )创建一个进程,再调用exec( ),用新的程序替换该子进程的内容,利用wait( )来控制进程执行顺序,掌握进程的睡眠、同步、撤消等进程控制方法。
实验结果:
图7-2-1 wait.out输出结果
结果分析:
(1)父进程创建子进程之后进入睡眠状态,把进程的使用权交给子进程。
(2)子进程的内容被execl()函数修改,用新的程序替换该子进程的内容,输出了当前文件夹文件。
(3)当子进程输出结束之后,子进程自我中止,父进程拿到进程的使用权。
3、编写一段多进程并发运行的程序,用lockf( )来给每一个进程加锁,以实现进程之间的互斥,观察并分析出现的现象及原因。
结果分析:
(1)从实验结果图中可看出,每个进程输出完自己的输出内容之后,别的进程才能输出自己的内容。
(2)当一个进程在执行时,由于通过加锁实现了进程之间的互斥,因此没有进程可以抢占它的输出。
(3)由于每次运行时不同进程抢占进程情况不同,所以每次输出顺序不一致。
4、编写程序:用fork( )创建两个子进程,再用系统调用signal( )让父进程捕捉键盘上来的中断信号(即按^c键);捕捉到中断信号后,父进程用系统调用kill( )向两个子进程发出信号,子进程捕捉到信号后分别输出下列信息后终止:
Child process1 is killed by parent!
Child process2 is killed by parent!
父进程等待两个子进程终止后,输出如下的信息后终止:
Parent process is killed!
实验结果:
图7-4-1 signal.out输出结果
结果分析:
(1)从实验结果图中可看出,按下^c键之后,子进程被杀死之后父进程被杀死。
(2)父进程在接收到信号之后执行杀死子进程操作,同时调用两个wait()函数等待子进程被杀死之后再执行“Parent process is killed!”这句话。
5、使用系统调用msgget( ),msgsnd( ),msgrev( ),及msgctl( )编制一长度为1k的消息发送和接收的程序,并分析消息的创建、发送和接收机制及控制原理。
实验结果:
图8-5-1 client.out输出结果
图8-5-2 server.out输出结果
结果分析:
从理想的结果来说,应当是每当 client 发送一个消息后, server 接收该消息,client 再发送下一条。也就是说“ (client)sent ”和 “(server)received 的字样应该在屏幕上交替出现。实际的结果大多是,先由client发送了两条消息,然后 server接收一条消息。此后client、server交替发送和接收消息。 最后server一次接收两条消息。client 和 server 分别发送和接收了10条消息,与预期设想一致。
6、编制一长度为1k的共享存储区发送和接收的程序,并设计对该共享存储区进行互斥访问及进程同步的措施,必须保证实现正确的通信。
实验结果:
图8-6-1 share.out输出结果
结果分析:
(1)和预想的完全一样。但在运行过程中,发现每当 client 发送一次数据后, server 要等待大约 0.1 秒才有响应。同样,之后 client 又需要等待大约 0.1 秒才发送下一个数据。
(2)出现上述应答延迟的现象是程序设计的问题。当client端发送了数据后,并没有任何措施通知server端数据已经发出,需要由client的查询才能感知。此时,client端并没有放弃系统的控制权,仍然占用 CPU的时间片。只有当系统进行调度时,切换到了server进程,再进行应答。这个问题,也同样存在于server端到client的应答过程中。
(1)系统是怎样创建进程的?
① 申请空白PCB(过程控制块)。
② 为新工序分配资源。
③ 初始化PCB。
④ 将新进程插入就绪队列。
(2)当首次调用新创建进程时,其入口在哪里?
进程的进程控制块(PCB)结构中有指向其TTS(任务状态段)的指针,TTS里面存放着进程的入口。
如图1-3,ID=0时,指向parent进程的父进程的PID=2981,它的PCB结构中有指向parent进程的指针PID=3003。Parent进程的PCB结构中有指向child_1进程的指针PID=3004和指向child_2进程的指针PID=3005。其中3003、3004、3005都是进程的入口。
(3)利用strace 和ltrace -f -i -S ./executable-file-name查看程序执行过程,并分析原因,画出进程家族树。
程序执行过程如图所示(文中未给出),父进程的被创建之后,它先复制子进程child_1,再复制子进程child_2,父进程在输出内容后被杀死,接着子进程也在输出内容后被杀死。
原因:当进程做完自己的事情之后就要结束进程了,子进程虽然是父进程复制出来的,但它单独的占有内存空间,所以不因为父进程的结束而结束。
进程家族树:
(4)程序的多次运行结果为什么不同?如何控制实验结果的随机性?
① 当使用的虚拟机是多处理器的多核操作系统时(如图8-1-3(a)),程序运行结果每次差异较大,详情可见图7-1-1和图7-1-2,图7-1-3和图7-1-4的对比。原因是多核CPU同时执行多个线程里的代码,CPU之间争抢输出机会,每次运行程序时各CPU争抢到的输出的时间片不同,输出结果各不相同。
② 方案一:使用单CPU
1)可以通过控制CPU的数量来控制实验结果的随机性。如图8-1-3(b)所示。单核的CPU在处理多线程时每次只能执行一跳指令,也就是说无论程序有多少个线程,每一时刻执行的也只是一个线程里的代码,CPU会轮流给每个线程分配时间片,时间片分配到哪个线程头上,哪个线程里的代码就执行。
2)单核CPU一定程度上控制了实验结果的随机性,如图7-1-1和图7-1-3所示,总是先输出父进程的结果再输出对应的子进程的结果。
③ 方案二:使用临界区实现多线程同步
1)在每个进程输出前加一道同步锁来确保不被别的进程抢占资源。
2)结果如图7-1-5所示,可见输出时按ID顺序输出,父进程在子进程之前输出。
(1)可执行文件加载时进行了哪些处理?
①创建一个新进程的 fork 系统调用
②用于实现进程自我终止的 exit 系统调用
③改变进程原有代码的 exec 系统调用
④用于将调用进程挂起并等待子进程终止的 wait 系统调用;
(2)什么是进程同步?wait( )是如何实现进程同步的?
① 进程同步是指对多个相关进程在执行次序上进行协调,以使并发执行的主进程之间有效地共享资源和相互合作,从而使程序的执行具有可在现行。
② wait()通过让父进程进入睡眠状态实现进程同步。父进程调用wait(NULL)语句等待子进程运行结束。如果子进程没有完成,父进程一直等待。wait( )将调用进程挂起,直至其子进程因暂停或终止而发来软中断信号为止。如果在wait( )前已有子进程暂停或终止,则调用进程做适当处理后便返回。
(3)wait( )和exit()是如何控制实验结果的随机性的?
wait()锁住父进程使它不与别的进程抢占输出,exit()撤销已经完成的子进程。对wait()函数而言,它由返回结果判断自己下一步操作。
③ 如果其所有子进程都还在运行,则阻塞;
④ 如果一个子进程已经终止,正等待父进程获取其终止状态,则取得该子进程的终止状态立即返回;
⑤ 如果它没有任何子进程,则立即出错返回;
(1)进程加锁和未上锁的输出结果相同吗? 为什么?
可能相同,可能不相同。进程加锁能够保证让该进程的输出内容输出完解锁之后,再把进程让给别的进程。未上锁时进程在输出过程中可能会被别的进程抢占输出,导致不同进程输出内容交叉出现,但也有可能在输出时一直被同一个进程抢占输出。
(1)为了得到实验内容要求的结果,需要用到哪些系统调用函数来实现及进程间的通信控制和同步?
signal() fork() kill() wait() exit() lockf()
(2)kill( )和signal( )函数在信号通信中的作用是什么?如果分别注释掉它们,结果会如何?
① kill()函数负责杀死另外的函数。
② signal()函数预置对信号的处理方式,允许调用进程控制软中断信号。
③ 如图8-4-1所示,注释掉kill()函数之后,程序不会对^c产生响应从而去杀死两个子进程,若不杀死两个子进程,父进程就会一直维持等待状态。
④ 如图8-4-2所示,注释掉signal()函数之后,程序不会杀死子进程和父进程。
(1)为了便于操作和观察结果,需要编制几个程序分别用于消息的发送与接收?
两个,一个发送消息,一个接收消息。
(2)这些程序如何进行编辑、编译和执行?为什么?
① 两个程序分别编辑、编译为 client.out 与 server.out。
② 执行: ./server.out ./client.out
③ Client和server是两种不同的属性,需要分开来编辑、编译和执行,客户端发送请求,服务器接收消息。
(3)如何实现消息的发送与接收的同步?
① 发送程序和接收程序都必须一直做好相互通信的准备。
② 运行时先启动server,再启动client。
(1)为了便于操作和观察结果,需要如何合理设计程序来实现子进程间的共享存储区通信?
① 先启动服务器创建共享存储区,再启动客户端对共享存储区进行写入,避免产生进程写入共享存储区但存储区并没有建立的错误,并且每次读写进行判断是否有内容。
② 一个程序就能进行读写操作,方便观察客户端写入和服务器接收的顺序。
(2)比较消息通信和共享存储区通信这两种进程通信机制的性能和优缺点。
① 消息队列的建立比共享区的设立消耗的资源少。前者只是一个软件上设定的问题,后者需要对硬件的操作,实现内存的映像,当然控制起来比前者复杂。如果每次都重新进行队列或共享的建立,共享区的设立没有什么优势。
② 当消息队列和共享区建立好后,共享区的数据传输,受到了系统硬件的支持,不耗费多余的资源;而消息传递,由软件进行控制和实现,需要消耗一定的cpu 的资源。从这个意义上讲,共享区更适合频繁和大量的数据传输。
③ 消息的传递,自身就带有同步的控制。当等到消息的时候,进程进入睡眠状态,不再消耗 cpu 资源。而共享队列如果不借助其他机制进行同步,接收数据的一方必须进行不断的查询,白白浪费了大量的 cpu 资源。