操作系统实验7-生产者消费者实验(二):共享存储区的同步互斥

实验时间: 2023.5.9                 

【实验目的】

  1. 了解和熟悉共享存储机制
  2. 应用PV操作解决进程间的同步互斥问题。

【实验内容】

  1. 编写程序,实现多进程对共享存储区的同步访问。
  2. 编写程序,利用信号量机制实现多进程对共享存储区的同步互斥访问。

 【实验指导】

1、参照参考程序1编程. 为了便于操作和观察结果,在主程序中首先建立一个key为75的共享区,先后fork()两个子进程:server和client,两子进程间通过共享存储区进行通信。要求client当共享存储区 空时放数据,server当共享存储区 满时取数据。

(1) 通过循环判断共享存储区状态实现进程间同步

server端首先获取key为75的共享区使用权。并将共享区第一个字节置为-1,作为数据空的标志。接着等待其他进程发来的消息。当该字节的值不为-1时,表示共享存储区中有数据了,于是取出进行处理。

server每接收到一次数据后显示接收到的数据。然后又第一个字节设为-1,表示取空。继续等待下一次的数据。如果遇到的值为0,则视为结束信号,取消该队列,并退出server。(?)

client端获取key为75的共享区使用权。每当共享区第一个字节为-1时,表示第共享存储区空闲,于是client端可向共享存储区放一个数据。client每发送一次数据后显示所放数据值。client端放完了9-0数据之后退出。

父进程在server和client均退出后结束。

在运行过程中,发现每当client发送一次数据后,server要等待大约0.1秒才有响应。同样,之后client又需要等待大约0.1秒才发送下一个数据。

(2)编写程序,利用上次实验中实现的sem_p,sem_v来实现server和client两子进程对共享存储区的同步访问。注意在sem_init中,对信号量初值的设置。

2、(1)修改程序,利用sem_p,sem_v来实现server和client两子进程对共享存储区的同步互斥访问。注意在sem_init中,对信号量初值的设置。

参考程序

参考程序1编写程序,实现多进程对共享存储区的同步访问。

#include <sys/sem.h>

#include <sys/shm.h>

#include <sys/ipc.h>

#include <stdio.h>

#include<stdlib.h>

#include<unistd.h>

#include<sys/wait.h>

#define  SHMKEY  75

int  shmid;   

int  *addr;

在函数内部,程序使用 perror 函数输出 mes 指向的字符串,并将系统错误信息输出到标准错误流中。如果没有错误发生,perror 函数不会输出任何内容。

然后,程序调用 exit 函数终止程序的执行,并将状态码设为 1。exit 函数用于正常或异常终止程序,可以通过在参数中指定状态码来向系统报告当前程序的状态。通常情况下,状态码为 0 表示程序正常结束,非零状态码表示程序异常终止或发生错误。

因此,当程序执行到 fatal 函数时,如果发生了错误,会自动输出错误信息并终止程序执行;如果没有错误发生,则不会有任何动作。该函数通常用于在程序出错时自动输出错误信息并终止程序执行,避免用户对程序出错的原因产生疑惑。

int fatal(char *mes)

{ perror(mes);

exit(1);

}

函数内部首先调用 shmat 函数获得共享内存区的首地址,shmid 是一个定义在全局范围内的参数。接下来,程序循环 10 次,每次向共享内存中写入一个整数数据。在写入数据之前,程序通过 while 循环判断共享内存区是否可以被写入。这里的判断条件 *addr != -1,表示只有当共享内存区的值为 -1 时,才能进行写操作。这种方式可以防止多个进程同时对共享内存进行写操作。在写入数据后,程序会通过 printf 函数输出写入的整数数据,然后进行一次延时操作,等待一段时间后再进行下一次写操作。最后,程序执行 exit 函数退出进程。

client函数的作用是通过共享内存实现进程间通信,并在整数数据写入共享内存区后输出写入的数据。

void  client( )

{  int i;

addr=shmat(shmid,0,0);           /*获得共享存储区首地址*/

for (i=9;i>=0;i--)

  {  

     while (*addr!=-1);

     printf("(client) sent *addr= %d\n",i);

     *addr=i;

 }

sleep(1);

exit(0);

}

函数内部首先也是通过 shmat 函数获得共享内存区的首地址,然后通过 do-while 循环不断读取共享内存中的数据。在循环开始时,程序将共享内存区的值设置为 -1,表示共享内存已准备好接收数据。

程序通过 while 循环判断共享内存区是否已被写入数据。当共享内存中的值不为 -1 时,表示有新的数据写入共享内存区,程序会将该数据读取出来,并通过 printf 函数输出读取的整数数据。

当读取的数据为 0 时,程序退出循环,并通过 shmctl 函数撤消共享存储区并归还资源。最后,程序通过 exit 函数退出进程。

该server 函数的作用是通过共享内存实现进程间通信,并在从共享内存区读取到数据时输出读取的数据。当读取的数据为 0 时,程序结束。

void  server( )

{

 int tmp;

addr=shmat(shmid,0,0);        /*获取首地址*/

do  

 {    

     *addr=-1; 先调用了该函数

     while (*addr==-1);

     tmp=*addr;

     printf("(server) received *addr=%d\n",tmp);

 }while (tmp);  

shmctl(shmid,IPC_RMID,0);     /*撤消共享存储区,归还资源*/

exit(0);

}

这段代码是一个主函数,主要作用是创建共享内存区并执行 server 和 client 两个函数。

1. 创建共享内存区。程序通过调用 shmget 函数创建一个大小为 1024 字节的共享内存区,并将其标识符存储在 shmid 变量中。

2. 创建子进程并执行 server 函数。程序通过 fork 函数创建子进程,并判断子进程是否创建成功。如果创建成功,则子进程调用 server 函数执行从共享内存区中读取整数数据的操作。

3. 显示当前系统的 IPC 信息。程序通过 system 函数调用 ipcs 命令显示当前系统所有的 IPC(进程间通信)信息,包括共享内存区、消息队列等。

4. 创建另外一个子进程并执行 client 函数。程序通过 fork 函数创建另外一个子进程,并判断子进程是否创建成功。如果创建成功,则子进程调用 client 函数执行向共享内存区中写入整数数据的操作。

5. 再次调用 system 和 wait 函数。程序再次通过 system 函数调用 ipcs 命令显示当前系统的 IPC 信息。接下来,程序通过 wait 函数等待子进程的结束,以确保进程执行的正确性。wait 函数会挂起当前进程的执行,直到子进程结束才会继续执行后续操作。

6. 调用 system 和 wait 函数。程序再次调用 system 函数调用 ipcs 命令显示当前系统的 IPC 信息。接下来,程序再次通过 wait 函数等待子进程的结束。

7. 最后一次调用 system 和 wait 函数。程序最后通过 system 函数调用 ipcs 命令显示当前系统的 IPC 信息。接下来,程序最后一次通过 wait 函数等待子进程的结束。

整个程序的作用是创建大小为 1024 字节的共享内存区,并通过 server 和 client 两个函数向共享内存中写入和读取整数数据。程序通过 ipcs 命令显示当前系统所有的 IPC 信息,以便调试和查看系统状态。

 int main()

{

    int pid;

    shmid=shmget(SHMKEY,1024,0660|IPC_CREAT); /*创建1024字节大小的共享存储区*/     

   while ((pid=fork( ))==-1);

   if (!pid) server( );

   system("ipcs  -m");

   while ((pid=fork( ))==-1);

   if (!pid) client( );

   system("ipcs  -m");

   wait(0);

   system("ipcs  -m");

   wait(0);

   system("ipcs  -m");

}

参考程序2编写程序,利用信号量机制实现多进程对共享存储区的同步互斥访问。

#include <sys/sem.h>

#include <sys/shm.h>

#include <sys/ipc.h>

#include <stdio.h>

#include<stdlib.h>

#include<unistd.h>

#include<sys/wait.h>

#define  SHMKEY  75

这段代码定义了一个共享内存标识符shmid和两个信号量操作sembuf结构p、v。

1. shmid是共享内存的标识符,用来标识共享内存段。共享内存是指多个进程可以同时访问同一段内存空间,用于进程间通信和数据共享。

2. sembuf是信号量操作结构体,由三个成员组成,分别是信号量编号(sem_num)、信号量操作(sem_op)、信号量标志(sem_flg)。

3. p={0,-1,0}表示对编号为0的信号量的操作为p操作,即减少1。其中,sem_num为0,表示对第一个信号量进行操作。sem_op为-1,表示将信号量的值减1,即对共享资源进行占用。sem_flg为0,表示操作方式为阻塞式。

4. v={0,1,0}表示对编号为0的信号量的操作为v操作,即增加1。其中,sem_num、sem_op和sem_flg的含义与p相同。sem_op为1,表示将信号量的值加1,即释放共享资源。

通过对共享内存的读写及信号量的上锁、解锁等操作,可以达到进程之间的同步和互斥,保证共享资源的访问正确性。

p和v是两个struct sembuf类型的操作数组元素,具体定义如下:

struct sembuf {

    short sem_num;   // 信号量编号

    short sem_op;    // 操作类型:减1(P操作)或加1(V操作)

    short sem_flg;   // 操作标志:0或IPC_NOWAIT

};

int  shmid;   

struct sembuf p={0,-1,0},

v={0,1,0};

int fatal(char *mes)

{ perror(mes);

exit(1);

}

 /******************/

这段代码定义了两个消息队列标识符senid和recid,以及一个名为semun的共用体类型。

1. senid和recid是消息队列的标识符,用于唯一标识消息队列。消息队列是一种IPC(进程间通信)方式,可以实现不同进程之间的数据交换和传递。

2. semun是一个共用体类型,用于在消息队列中进行信号量设置。它由三个成员组成:

   - val表示信号量的值,类型为int。可以用于设置信号量的初始值、读取信号量的当前值等。

   - buf指向semid_ds结构类型的指针,用于读取或设置信号量的相关信息,如信号量集的创建者、最近一次使用时间、最后一次修改时间、信号量集的大小等等。

   - array指向unsigned short类型的指针,表示信号量集的值列表,用于读取或设置信号量集中所有信号量的值。

共用体是一种内存空间的重叠区域,不同成员共用同一段内存空间,共用体类型的大小为其中最大成员的大小。在本段代码中,semun类型主要用于对消息队列中的信号量进行设置和读取。

int  senid,recid;   

typedef union semun{

    int val;

    struct semid_ds *buf;

    unsigned short *array;

} semun;

这段代码定义了一个名为sem_init()的函数,用于初始化信号量。

此函数通过调用系统调用semget()和semctl()来创建新的信号量或访问现有的信号量集合,并清零信号量并设置其初始值。

具体而言,该函数的实现如下:

1. 首先定义一个semid和semun类型的参数arg。

2. 调用系统调用semget()创建一个新的信号量或访问现有的信号量集合。

semget()函数的三个参数分别为key_t类型的key、int类型的nsems和int类型的semflg。

- key是信号量关键字,与进程共享某个资源的标识相同,用于在不同进程之间定位和访问一个信号量集合。可以使用ftok()函数生成一个唯一的key。

- nsems是信号量集合中的信号量数量,这里为1。

- semflg是一个权限掩码,用于指定该信号量集合的权限。这里的0660表示读写权限为拥有者和群体。

3. 如果调用semget()失败,则调用预定义函数fatal()输出异常信息并退出程序。否则,继续操作。

4. 将arg.val设置为参数i,即信号量的初始值。

5. 调用系统调用semctl()对该信号量设置初始值。

semctl()函数的四个参数分别为int类型的semid、int类型的semnum、int类型的cmd和union semun类型的arg。

- semid是信号量集合的标识符,由semget()函数返回。

- semnum是要访问的信号量编号,这里设为0,表示第一个信号量。

- cmd是要执行的操作,这里使用SETVAL表示设置值。其他常见的操作还有GETVAL(读取值)、SETALL(同时初始化整个信号量集合)、IPC_RMID(删除信号量集合)等。

- arg是一个共用体,用于读取或设置信号量的取值或控制信息。这里将arg的val字段设置为参数i。

6. 如果调用semctl()失败,则调用预定义函数fatal()输出异常信息并退出程序。否则,返回信号量集合的标识符semid。

int sem_init(int key,int i)

{

int semid;

semun arg;

if((semid=semget(key,1,0660|IPC_CREAT))<0)

 fatal("sem_init:semget");

arg.val=i;

if(semctl(semid,0,SETVAL,arg)<0)

 fatal("sem_init:semctl");

return semid;

}

这是一个客户端进程的代码片段,实现了向共享内存区写入数据的功能。

函数名:client

参数:无

返回值:无

功能:向共享内存区写入数据

函数实现步骤:

1. 利用shmat函数获取共享内存区的首地址,将其赋值给指针变量addr

2. 采用循环方式向共享内存区写入多个数据,循环变量i的初始值为9,每次循环将其减1

3. 在每次向共享内存区写入数据前,需使用semop函数对接收进程的信号量进行P操作,等待其接收数据

4. 将需要写入的数据i赋值给共享内存区的内容(通过指针变量addr进行间接寻址)

5. 在每次向共享内存区写入数据后,需使用semop函数对发送进程的信号量进行V操作,通知其可以读取数据

6. 延时1秒,等待接收进程处理完毕后,释放共享内存区

7. 退出客户端进程

其中,semop函数用于对信号量进行操作,其参数包括信号量标识符、操作数组和操作数组元素个数。

在本代码片段中,p和v分别表示对信号量进行P操作和V操作,由于操作的信号量只有一个,所以对应的sem_num参数都为0,sem_op参数表示对信号量进行减1或加1操作,sem_flg参数设为0表示阻塞等待操作完成。因此,semop(senid,&p,1)对应的是对发送进程的信号量进行P操作,semop(recid,&v,1)对应的是对接收进程的信号量进行V操作。

void  client( )

{  int i,*addr;

addr=shmat(shmid,0,0);           /*获得共享存储区首地址*/

for (i=9;i>=0;i--)

  {  

     semop(senid,&p,1);

     printf("(client) sent *addr= %d\n",i);

     *addr=i;

     semop(recid,&v,1);         

 }

sleep(1);

exit(0);

}

这是一个服务器进程的代码片段

函数名:server

参数:无

返回值:无

功能:从共享内存区读取数据

函数实现步骤:

1. 利用shmat函数获取共享内存区的首地址,将其赋值给指针变量addr

2. 使用do-while语句对共享内存区中的数据进行循环读取,直到读取到的数据为0为止

3. 在每次读取数据前,需使用semop函数对发送进程的信号量进行P操作,等待其向共享内存区写入数据

4. 通过指针变量addr进行间接寻址,获取共享内存区中的数据,并将其赋值给变量tmp

5. 在每次读取数据后,需使用semop函数对接收进程的信号量进行V操作,通知其可以继续向共享内存区写入数据

6. 将读取到的数据输出到屏幕上

7. 当读取到的数据为0时退出循环,释放共享内存区

8. 退出服务器进程

与客户端进程中的代码类似,semop函数在本代码片段中也被用于对信号量进行操作,其参数和操作方式与客户端进程中相同。p和v数组元素的作用也与客户端进程中相反,semop(recid,&p,1)对应的是对接收进程的信号量进行P操作,semop(senid,&v,1)对应的是对发送进程的信号量进行V操作。

void  server( )

{

 int tmp,*addr;

addr=shmat(shmid,0,0);        /*获取首地址*/

do  

 {    

     semop(recid,&p,1);

     tmp=*addr;

     printf("(server) received *addr=%d\n",tmp);

     semop(senid,&v,1);

 }while (tmp);  

exit(0);

主要负责创建进程、初始化信号量以及清除共享内存和信号量等资源。

函数名:main

参数:无

返回值:无

功能:创建进程、初始化信号量、清除资源等

函数实现步骤:

1. 调用shmget函数创建一个长度为1024字节的共享内存块,将返回的共享内存ID保存在变量shmid中

2. 初始化三个信号量,分别为发送进程的信号量、接收进程的信号量以及互斥信号量,在本代码片段中,互斥信号量未被用到

3. 通过while循环fork两个子进程,分别作为发送进程和接收进程,并分别调用client和server函数

4. 在每次fork之后,使用system函数和ipcs命令查看共享内存及信号量的状态,方便调试和程序运行情况的跟踪

5. 在两个子进程结束后,调用wait函数以等待其结束,并清除共享内存块和信号量,将其归还给系统

其中,sem_init函数用于创建一个新的信号量,其参数包括信号量的键值、信号量的初始值、以及一些操作标志。在本代码片段中,初始化发送进程的信号量的初始值为1(表示可以写入数据),接收进程的信号量的初始值为0(表示需要等待接收数据)。semctl函数用于对指定的信号量进行操作,包括读取、设置和删除等,其参数主要包括信号量标识符、要进行的操作以及一些其他参数。在本代码片段中,调用semctl函数对发送进程和接收进程的信号量进行了删除操作,以释放相关资源。

此外,除了进程相关的函数之外,还使用了一些系统调用和标准库函数,如system、wait等,实现了对系统资源的管理和进程的控制。

int main()

{

    int pid;

    shmid=shmget(SHMKEY,1024,0660|IPC_CREAT); /*创建1024字节大小的共享存储区*/

    /****************/

 //   mutex=78;

   sen=73;

   rec=74;    

   senid=sem_init(sen,1);

   recid=sem_init(rec,0);

    /****************/

    

   while ((pid=fork( ))==-1);

   if (!pid) server( );

   system("ipcs  -m");

   system("ipcs  -s");

   while ((pid=fork( ))==-1);

   if (!pid) client( );

   system("ipcs  -m");

   wait(0);

   system("ipcs  -m");

   wait(0);

   system("ipcs  -m");

   semctl(senid,0,IPC_RMID);

   semctl(recid,0,IPC_RMID);

   shmctl(shmid,IPC_RMID,0);     /*撤消共享存储区,归还资源*/

}

思考题

1、解释实验指导(1)中, 通过循环判断共享存储区状态实现进程间同步时,出现应答延迟的现象(提示:linux是基于时间片轮转的调度策略)。

2、评价以上两种实现进程间同步的方法。

【实验步骤和结果】

1.(1)

 

 

(2)

6-2.c
#include<stdio.h>
#include<stdlib.h>
#include<string.h>
#include<unistd.h>
#include<sys/types.h>
#include<linux/sem.h>
#include<linux/shm.h>
//利用信号量实现进程同步
//定义信号量内部标识
int emptyid;
int fullid;
main()
{
	int child, shmid;
	struct sembuf P,V; //定义信号量数据结构
	union semun arg; //定义信号量数据结构
	char *viraddr; //定义共享内存
	char buffer[BUFSIZ];
	//创建信号量并初始化
	emptyid=semget(IPC_PRIVATE, 1, IPC_CREAT|0666);
	fullid=semget(IPC_PRIVATE, 1, IPC_CREAT|0666);
	arg.val=1;
	if(semctl(emptyid, 0, SETVAL, arg)==-1)
		perror("semctl setval error");
	arg.val=0;
	if(semctl(fullid, 0, SETVAL, arg)==-1)
	perror("semctl setval error");
	//定义PV操作
	P.sem_num=0;
	P.sem_op=-1;
	P.sem_flg=SEM_UNDO;
	V.sem_num=0;
	V.sem_op=1;
	V.sem_flg=SEM_UNDO;
	//创建并附接共享内存
	shmid=shmget(IPC_PRIVATE, BUFSIZ, 0666|IPC_CREAT);
	viraddr=(char*)shmat(shmid, 0, 0);
	//创建子进程
	while((child=fork())==-1);
	//子进程返回,写信息到共享内存
	if(child==0)
	{
		while(1)
		{
			//对emptyid执行P操作
			semop(emptyid, &P, 1);
			puts("Enter your text:");
			//键盘输入信息
			fgets(buffer, BUFSIZ, stdin);
			//将信息写到共享内存中(覆盖方式)
			strcpy(viraddr, buffer);
			//对fullid执行V操作
			semop(fullid, &V, 1);
			if(strncmp(viraddr,"end",3)==0){//"end"为结束标志
				//睡眠1秒,等待父进程将end取走
				sleep(1);	
			break;
			}	
		printf("child exited\n");
		exit(0);
		}
	}
	//返回父进程,读信息并输出
	else{
		while(1)	
		{
			//对fullid执行P操作		
			semop(fullid, &P, 1);
			printf("Your message is:%s\n",viraddr);
			if(strncmp(viraddr, "end", 3)==0)
				break;
		}
		//等待子进程终止
		wait(0);
		//断开附接的共享内存
		shmdt(viraddr);
		//撤销共享内存和信号量集
		semctl(emptyid, IPC_RMID, 0);
		semctl(fullid, IPC_RMID, 0);
		printf("Parent exit\n");
		exit(0);
	}	
}

 

2. 

 

 

 

【实验总结和体会】

  1. 通过循环判断共享存储区状态实现进程间同步时,出现应答延迟的现象?

linux是基于时间片轮转的调度策略。client模拟生产者,当共享存储区空时放数据,server模拟消费者当共享存储区满时取数据。在运行过程中,发现每当client发送一次数据后,server要等待大约0.1秒才有响应。同样,之后client又需要等待大约0.1秒才发送下一个数据。

出现上述应答延迟的现象是程序设计的问题。当client端发送了数据后,并没有任何措施通知server端数据已经发出,需要由client的查询才能感知。此时,client端并没有放弃系统的控制权,仍然占用CPU的时间片。只有当系统进行调度时,切换到了server进程,再进行应答。这个问题,也同样存在于server端到client的应答过程中。

  1. 评价以上两种实现进程间同步的方法。

(1)是建立一个key为75的共享区,先后fork()两个子进程:server和client,两子进程间通过共享存储区进行通信。通过循环判断共享存储区状态实现进程间同步。

(2)是增加了两个同步信号量集、sem-p、sem-v函数和存储信号操作的数组来实现server和client两子进程对共享存储区的同步访问。

实验六 进程间通信(下)_徐海鑫的博客-CSDN博客(父子进程利用信号量实现对共享内存的同步访问)

 实验六 共享存储区通信_Non_Recursive的博客-CSDN博客(思考题)

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
生产者消费者问题是一个经典的同步问题,涉及到多个线程之间的协作和共享资源的访问。在该问题中,生产者线程生产数据并将其存储在共享缓冲中,而消费者线程从该缓冲中取出数据并进行处理。在实现过程中,需要保证生产者消费者之间的同步互斥,避免出现数据竞争和死锁等问题。 对于该问题实验分析,可以从以下几个方面进行考虑: 1. 同步机制的选择:生产者消费者之间需要进行同步,以确保生产者不会向缓冲中写入数据,而消费者还没有取走数据。常见的同步机制包括互斥量、信号量和条件变量等。需要根据实际情况选择合适的同步机制。 2. 缓冲的实现:缓冲生产者消费者之间的共享资源,需要保证线程安全。可以使用数组、队列等数据结构来实现缓冲。需要考虑缓冲的大小、数据类型等因素。 3. 线程的创建和销毁:需要创建生产者消费者线程,并在适当的时候销毁它们。可以使用线程池来管理线程的创建和销毁。 4. 错误处理:需要考虑错误处理机制,以应对可能出现的异常情况。例如,如果缓冲已满,生产者线程应该等待消费者线程取走数据之后再进行写入,否则会导致阻塞。需要在程序中添加相应的错误处理代码。 综上所述,生产者消费者问题是一个经典的同步问题,需要注意同步机制的选择、缓冲的实现、线程的创建和销毁以及错误处理等方面。在实现过程中,需要保证线程安全,并避免出现数据竞争和死锁等问题

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值