操作系统实验报告 进程管理与进程通信

Posted mp-ui

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了操作系统实验报告 进程管理与进程通信相关的知识,希望对你有一定的参考价值。

在这里插入图片描述

一、 实验目的

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

二、 实验环境

Ubuntu 20.10,gcc编译器

三、 实验内容

  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的共享存储区发送和接收的程序,并设计对该共享存储区进行互斥访问及进程同步的措施,必须保证实现正确的通信。

四、 实验原理 实验中用到的系统调用函数(包括实验原理中介绍的和自己采用的),实验步骤

实验原理:

实验步骤:

第一题:

因为题目说创建2个子进程,而根据fork()函数的返回值可以发现,如果返回值==0表示当前是子进程,返回值>0说明当前是父进程,根据这一点就可以通过if-else语句将程序分开成两个不同的进程运行!
在这里插入图片描述

第二题:

一开始采用跟第一题一样的创建进程的方法,只是在子进程中调用 execl()从而替代了后面的内容:
在这里插入图片描述
同时也在父进程中调用wait(),保证父进程不会提前结束:
在这里插入图片描述

第三题:

先创建2个子进程,然后在2个子进程里面都先加锁,通过for循环输出10个数字,执行结束之后再解锁:
在这里插入图片描述

第四题:

先创建2个子进程,然后在进程里面接收信号,后面需要用pause()函数来使程序暂停,直到接收到信号。接收到终止信号之后再向子进程发出信号。
在这里插入图片描述
子进程接收到信号后,终止程序的运行。
在这里插入图片描述
父进程最后调用wait()保证了父进程在最后结束。
在这里插入图片描述

第五题:

首先在main函数中创建2个进程:
父进程中调用server(),接收消息。
子进程中调用client(),发送消息。
在这里插入图片描述
client()函数中发送消息:
在这里插入图片描述
server()函数中发送消息:
先调用wait()函数等client发送完消息并结束进程再接收消息。
在这里插入图片描述

第六题:

main()函数中创建1个子进程执行server,父进程执行client
在这里插入图片描述
client中发送消息
在这里插入图片描述
server中接收消息:
在这里插入图片描述
server一接收到消息都会把addr置为-1,以等待client再次发送消息。

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

1、实验结果与实验程序、实验步骤、实验原理、操作系统原理的对应分析;
2、不同条件下的实验结果反应的问题及原因;
3、实验结果的算法时间、效率、鲁棒性等性能分析。

第一题:

在这里插入图片描述

在CPU多核的情况下,通过实验结果可以发现,多次执行程序,“父进程”“子进程1”“子进程2”输出的顺序都不一样。说明3个进程的执行顺序都是不确定的,这取决于进程的调度时机。
然后把CPU调到1核,重启,
在这里插入图片描述
在这里插入图片描述
可以发现在单核的情况下,执行顺序相对固定,都是先执行的父进程再执行2个子进程。因为单核CPU在处理多线程程序时只能执行一跳指令,每个进程都是轮流执行,所以单核CPU在一定程度上控制了实验结果的随机性。

第二题:

在这里插入图片描述
执行程序,可以发现程序把当前文件夹下的所有文件都输出了。

第三题:

在这里插入图片描述
可以发现,每个进程加锁之后,都不会切换到另外2个进程去执行,只有该进程执行完毕之后解锁之后,才会切换到下一个进程去执行。

第四题:

正在运行程序:
在这里插入图片描述
开启一个新的终端,执行kill -2 54969,向父进程发送中断信号:
在这里插入图片描述
最终的运行结果如下:
在这里插入图片描述

第五题:
server中已成功接收到cilent发送的信息并显示出来。
在这里插入图片描述

第六题:
client每发送一条消息,server都会接收之后,client才会发送下一条消息。
在这里插入图片描述

六、实验总结

每一道题都是自己先在网上找了一些资料之后才开始的,花费了比较长的时间,也遇到了许多困难,比如在linux系统中操作,遇到了许多不适应的地方,很多不懂的地方也是自己花了很多时间搜索资料解决的。但是收获也很大,通过本次的实验,我也对进程、消息和共享区有了更进一步的了解。

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

第一题

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>

int main()
{
	int p;
	p = fork();
	if (p < 0)
	{
		//fork()函数的返回值<0表示进程创建失败!
		printf("子进程创建失败!\\n");
	}
	else if (p == 0)
	{
		//p==0表示这是子进程
		printf("我是第一个子进程,号码是%d\\n", getpid());
		exit(0); //中断进程
	}
	else
	{
		//p>0表示当前是父进程
		//创建第二个子进程
		int q;
		q = fork();
		if (q < 0)
		{
			printf("子进程创建失败!\\n");
		}
		else if (q == 0)
		{
			printf("我是第二个子进程,号码是%d\\n", getpid());
			exit(0);
		}
		else
		{
			printf("我是父进程,号码是%d\\n", getpid());
			exit(0); //中断进程
		}
	}
}

第二题

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/types.h>
int main()
{
	int p;
	p = fork(); //创建子进程
	switch (p)
	{
	case -1:
		printf("创建失败\\n");
		exit(1);
	case 0: //子进程
		execl("/bin/ls", "ls", NULL);
		printf("execfail!\\n");
		exit(1);
	default:		//父进程
		wait(NULL); //先暂停父进程,避免父进程提前结束!
		printf("lscompleted!\\n");
		exit(0);
	}
}

第三题

#include <stdio.h>
#include <unistd.h>
int main()
{
    int p;
    int i;
    p = fork(); //创建子进程
    if(p < 0){
        printf("第一个子进程创建失败\\n");
    }else if(p == 0){
        //当前是子进程
        lockf(1,1,0);   //加锁
        for(i = 0;i <10;++i){
            printf("%d\\n",i);
        }
        printf("第一个子进程执行完成\\n");
        lockf(1,0,0);   //解锁
    }else{
        p = fork();  //创建第二个子进程
        if(p < 0){
            printf("第二个子进程创建失败\\n");
        }else if(p == 0){
            //子进程
            lockf(1,1,0);   //加锁
            for(i = 10;i <20;++i){
                printf("%d\\n",i);
            }
            printf("第二个子进程执行完成\\n");
            lockf(1,0,0);   //解锁
        }else{
            //当前是父进程
            lockf(1,1,0);   //加锁
            for(i = 20;i <30;++i){
                printf("%d\\n",i);
            }
            printf("父进程执行完成\\n");
            lockf(1,0,0);   //解锁
        }
    }
}

第四题

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

void sighandler(int sig){
    //printf("Response\\n");
}

int main(){

    int pid1;
    int pid2;
    pid1 = fork(); //创建第一个子进程
    if(pid1 == 0){
        //第一个子进程
        printf("我是第一个子进程,pid=%d\\n",getpid());
        signal(SIGUSR1,sighandler);   //接受信号
        pause();   //程序暂停,直到信号出现
        printf("Child process1 is killed by parent!\\n");  
        exit(1);
    }else{
        pid2 = fork(); //创建第二个子进程
        if(pid2 == 0){
            // 第二个子进程
            printf("我是第二个子进程,pid=%d\\n",getpid());
            signal(SIGUSR2,sighandler);   //接受信号
            pause();   //程序暂停,直到信号出现
            printf("Child process2 is killed by parent!\\n");
            exit(2);
        }else{
            //父进程
            printf("我是父进程,pid=%d\\n",getpid());
            signal(SIGINT,sighandler);
            pause();
            kill(pid1,SIGUSR1); //向进程pid1发送用户自定义信号1
            kill(pid2,SIGUSR2); //向进程pid2发送用户自定义信号2
            wait(NULL);
            wait(NULL);
            wait(NULL);         //等待两个子进程
            printf("Parent process is killed!\\n");
            exit(0);
        }
    }
}

第五题

#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/msg.h>
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include<string.h>
#include<sys/wait.h>

#define MSGKEY 1024  //消息key

typedef struct msgForm{ //消息结构体
    long  mtype;            /*消息类型*/
    char  mtext[1024];      /*消息的文本*/
}msgForm;

int msgqid;

void client(){
    msgForm msg;
    msg.mtype = 10; //定义消息类型
    strcpy(msg.mtext,"Hello,World!!!");
    msgqid = msgget(MSGKEY,0777); //创建消息队列
    msgsnd(msgqid,&msg,sizeof(msg),0);
    printf("(client)已发送信息!\\n");
    printf("(client)发送的信息为%s\\n",msg.mtext);
    exit(0);
}

void server(){
    msgForm msg;
    msgqid=msgget(MSGKEY,0777|IPC_CREAT); //创建一个所有用户都可以读、写、执行的队列
    wait(0);    //保证cilent发送完消息并结束进程之后才继续执行下面
    msgrcv(msgqid,&msg,sizeof(msg),0,0);    //接受信息
    printf("(server)已接受信息!\\n");
    printf("(server)接受到的信息为%s\\n",msg.mtext);
    msgctl(msgqid, IPC_RMID,0); //消除消息队列的标识符。
    exit(0);
}


int main(){
    int p;
    p = fork(); //创建子进程
    if(p == 0){
        client();
    }else{
        server();
    }
    return 0;
}

第六题

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

#define SHMKEY 1024 // 共享存储区的key
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); //创建一个共享存储区,大小为1024字节
    addr = shmat(shmid, 0, 0);                      //共享存储区所附接到的进程虚地址(首地址)
    do
    {
        *addr = -1;
        while (*addr == -1);//响应客户端
        printf("(server)received\\n");
    } while (*addr);
    shmctl(shmid, IPC_RMID, 0); //撤消共享存储区,归还资源
    exit(0);
}

void main()
{
    int p;
    p = fork(); //创建子进程
    if(p < 0){
        printf("子进程创建失败!\\n");
    }
    else if(p == 0){
        server();
    }else{
        client();
    }

}

八、思考题

1、进程创建与进程并发执行

(1)系统是怎样创建进程的?
答:通过fork()系统调用之后,执行以下操作:
① 申请空白PCB(过程控制块)。
② 为新工序分配资源。
③ 初始化PCB。
④ 将新进程插入就绪队列。

(2)当首次调用新创建进程时,其入口在哪里?
答:进程的进程控制块(PCB)结构中有指向其TTS(任务状态段)的指针,TTS里面存放着进程的入口。

(3)利用strace 和ltrace -f -i -S ./executable-file-name查看程序执行过程,并分析原因,画出进程家族树。
在这里插入图片描述 在这里插入图片描述
执行过程如图所示,父进程先创建的子进程2再创建子进程1,并且在输出内容后被杀死。进程家族树如图所示:
在这里插入图片描述

2、进程的睡眠、同步、撤消等进程控制

(1)可执行文件加载时进行了哪些处理?
答:进程用exec( )装入命令ls ,exec( )后,子进程的代码被ls的代码取代,这时子进程的PC指向ls的第1条语句,开始执行ls的命令代码。

(2)什么是进程同步?wait( )是如何实现进程同步的?
答:进程同步是指:在多道程序环境下,进程是并发执行的,不同进程之间存在着不同的相互制约关系。wait()函数一调用,父进程就会先暂停执行,直到有一个进程结束之后才去控制。

(3)wait( )和exit()是如何控制实验结果的随机性的?
答:子进程在使用了execl()函数之后,列出了当前目录下所有的文件,执行完这个函数之后才去调用exit(),退出当前进程,才去接着继续执行父进程,所以我们可以发现父进程的lscompleted!永远在最后才输出,这样子就控制了实验结果的随机性。

3、多进程通过加锁互斥并发运行

(1)进程加锁和未上锁的输出结果相同吗? 为什么?
答:不相同。因为进程加锁之后,就保证了在执行其中一个进程的时候,不会切换到另外2个进程去执行,只有解锁之后才会切换到另外2个进程。如果进程未上锁,那么程序就会随时切换到另外2个进程执行,输出结果会不相同。

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

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

(2)kill( )和signal( )函数在信号通信中的作用是什么?如果分别注释掉它们,结果会如何?
答:

  • kill()函数的作用是向指定的子进程发送信号,signal()的作用是接收指定的信号,signal()后面需要配合pause()函数一起使用。
  • 如果注释掉kill(),那么子进程将接收不到信号,导致子进程一直在等待信号的出现,父进程也一直在等待子进程结束,最终程序一直阻塞。
  • 如果注释掉signal(),那么父进程也接收不到中断信号,发出中断信号后程序也没反应,程序也是一直阻塞。

5、消息的发送与接收

(1)为了便于操作和观察结果,需要编制几个程序分别用于消息的发送与接收?
答:两个程序,其中client发送消息,server接收消息。

(2)这些程序如何进行编辑、编译和执行?为什么?
答:
① 两个程序分别编辑,执行gcc client.c -o client和gcc server.c -o server
② 执行: ./server和 ./client
③ client和server是两种不同的程序,需要分开来编辑、编译和执行,client发送请求,server用于接收消息。

(3)如何实现消息的发送与接收的同步?
答:发送程序和接收程序都必须一直做好相互通信的准备。

6、进程的共享存储区通信

(1)为了便于操作和观察结果,需要如何合理设计程序来实现子进程间的共享存储区通信?
答:每个进程都需要开辟一个共享存储区,然后附加到自己的一个内存空间中,就可以正常地进行读写操作了。

(2)比较消息通信和共享存储区通信这两种进程通信机制的性能和优缺点。
答:
①消息队列的建立比共享区的设立消耗的资源少。前者只是一个软件上设定的问题,后者需要对硬件的操作,实现内存的映像,当然控制起来比前者复杂。如果每次都重新进行队列或共享的建立,共享区的设立没有什么优势。
②当消息队列和共享区建立好后,共享区的数据传输,受到了系统硬件的支持,不耗费多余的资源;而消息传递,由软件进行控制和实现,需要消耗一定的cpu的资源。从这个意义上讲,共享区更适合频繁和大量的数据传输。
③消息的传递,自身就带有同步的控制。当等到消息的时候,进程进入睡眠状态,不再消耗cpu资源。而共享队列如果不借助其他机制进行同步,接收数据的一方必须进行不断的查询,白白浪费了大量

以上是关于操作系统实验报告 进程管理与进程通信的主要内容,如果未能解决你的问题,请参考以下文章

操作系统实验3共享内存进程间通信实验

操作系统ucore lab5实验报告

操作系统课程设计

操作系统第5次实验报告:内存管理

操作系统第5次实验报告:内存管理

计算机操作系统实验指导 (第3版) 第四篇 操作系统学习指导和习题解析 第18章:进程同步与通信 习题和答案