生产者消费者问题
1. 实验目的
- 学习掌握操作系统中进程之间的通信
- 理解并掌握使用信号量基址进行多进程之间互斥访问共享内存区域的控制
- 学习进程的创建和控制,共享内存区域的创建、使用和删除,信号量的创建使用和删除
2. 实验内容
- 一个大小为3的缓冲区,初始为空
- 2个生产者
- 随机等待一段时间,往缓冲区添加数据
- 若缓冲区已满,等待消费者取走数据后再添加
- 重复6次
- 3个消费者
- 随机等待一段时间,从缓冲区读取数据
- 若缓冲区为空,等待生产者添加数据后再读取
- 重复4次
说明:
- 显示每次添加和读取数据的时间和缓冲区里的数据
- 生产者和消费者都用进程模拟
3. 实验环境
本实验基于本机macOS系统和Windows虚拟机完成,具体实验环境如下:
3.1 Linux环境
Linux环境配置如下:
- 操作系统:macOS
- 内存容量:8GB
- 处理器:2.9GHz Intel Core i5
- 硬盘容量:500GB
3.2 虚拟机环境
Windows虚拟机环境配置如下:
- 虚拟机软件:VMware Fusion 11
- 虚拟机操作系统:Windows 7 旗舰版
- 虚拟机内存:4GB
- 虚拟机硬盘容量:60GB
4. 程序设计和实现
4.1 Windows实现
Windows下没有直接创建共享内存区域的方式,于是实验中使用文件映射的方式实现共享内存。
4.1.1 数据结构设计
以下数据结构均定义在头文件ProducerConsumer.hpp
中
缓冲区
定义缓冲区结构如下:
typedef struct buffer
{
int buff[BUFFER_LEN];
int head;
int tail;
int empty;
} buffer;
说明
- 该数据结构用于表示缓冲区相关数据
buff
为缓冲区内容,是一个长度为BUFFER_LEN
的数组head
和tail
为缓冲区指针,用于指示缓冲区读写位置empty
为标志位,标识缓冲区是否为空
共享内存
定义共享内存结构如下:
typedef struct shareMemory
{
buffer buffer_data;
HANDLE sem_full;
HANDLE sem_empty;
HANDLE sem_mutex;
} shm;
说明
- 该数据结构用于表示共享内存相关数据
buffer_data
为缓冲区,为自定义数据结构sem_full
,sem_empty
,sam_mutex
为三个相关信号量,用于缓冲区同步与互斥使用
4.1.2 API介绍
创建文件映射
CreateFileMapping()
是用于创建一个文件映射内核对象的函数,声明如下:
HANDLE WINAPI CreateFileMapping(
_In_HANDLE hFile,
_In_opt_LPSECURITY_ATTRIBUTES lpAttributes,
_In_DWORD flProtect,
_In_DWORD dwMaximumSizeHigh,
_In_DWORD dwMaximumSizeLow,
_In_opt_LPCTSTR lpName
);
实验中使用如下:
HANDLE handleFileMapping=
CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE,0,sizeof(shm),SHM_NAME);
说明
- 参数
hFile
使用INVALID_HANDLE_VALUE
,表示在页面文件中创建一个可共享的文件映射,在本实验中用于作为共享内存 - 参数
flProtect
使用PAGE_READWRITE
,表示以可读、写的方式打开映射 - 参数
dwMaximumSizeLow
使用sizeof(shm)
,该数据为文件映射最大长度的低32位,表示该文件大小只有在4.1.1定义的数据结构sharememory
一样大。 - 参数
lpName
使用SHM_NAME
该值为宏定义,表示共享内存区名字
映射文件对象
MapViewOfFile()
是用于将一个文件映射对象映射到当前程序地址空间的函数,声明如下:
LPVOID WINAPI MapViewOfFile(
_In_HANDLE hFileMappingObject,
_In_DWORD dwDesiredAccess,
_In_DWORD dwFileOffsetHigh,
_In_DWORD dwFileOffsetLow,
_In_SIZE_T dwNumberOfBytesToMap
);
实验使用如下:
LPVOID shmaddr=MapViewOfFile(handleFileMapping,FILE_MAP_ALL_ACCESS,0,0,0);
说明
- 参数
hFileMappingObject
使用CreateFileMapping
的返回句柄,表示将创建的对应的文件映射对象映射到程序地址空间 - 参数
dwDesiredAccess
使用FILE_MAP_ALL_ACCESS
,表示可以使用文件所有权限,是与创建文件映射对象相对应的权限
打开文件映射
OpenFileMapping()
是用于打开一个已经存在的文件映射对象的函数,返回相应打开的句柄,声明如下:
HANDLE OpenFileMapping(
_In_DWORD dwDesiredAccess,
_In_BOOL bInheritHandle,
_In_LPCSTR lpName
);
实验使用如下:
HANDLE hFileMapping=OpenFileMapping(FILE_MAP_ALL_ACCESS,FALSE,SHM_NAME);
说明
- 参数
dwDesireAccess
使用FILE_MAP_ALL_ACCESS
,表示打开该映射对象时具有全部权限,和创建文件对象对应 - 参数
bInheritHandle
使用FALSE
,表示由该进程启动的新进程不允许继承该句柄,防止错误发生 - 参数
lpName
使用SHM_NAME
,表示打开创建的名为SHM_NAME
的文件映射对象
创建信号量
CreateSemaphore()
是用于创建一个信号量的函数,返回对应信号量的句柄,声明如下:
HANDLE CreateSemaphore(
LPSECURITY_ATTRIBUTES lpSemaphoreAttributes,
LONG lInitialCount,
LONG lMaximumCount,
LPCTSTR lpName
);
实验使用如下:
shmaddr->sem_empty=CreateSemaphore(NULL,BUFFER_LEN,BUFFER_LEN,"SEM_EMPTY");
说明
- 参数
lInitialCount
和lMaximumCount
分别表示该信号量初始值和最大可以到达的值,实验中设置如下:sem_empty
初始值:BUFFER_LEN
最大值:BUFFER_LEN
sem_full
初始值:0
最大值:BUFFER_LEN
sem_mutex
初始值:1
最大值:1
- 参数
lpName
是信号量的名字
创建进程
CreateProcess()
是用于创建进程并为进程指定运行程序的函数,声明如下
BOOL CreateProcess(LPCTSTR lpApplicationName, LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes,
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles, DWORD dwCreationFlag,
LPVOID lpEnvironment, LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMTION lpProcessInformation);
实验使用如下:
CreateProcess(szFilename, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
说明
lpApplication
:该参数指定新进程将使用的可执行文件lpCommandLine
:该参数指定里传递给新进程的命令行字符串,该函数将按照一定的顺序搜索该可执行文件位置,并执行lpProcessInformation
:该参数是只想包含返回的进程和线程的句柄、进程和线程标识符的指针。在等待同步函数中需要从该结构中调取句柄信息
等待同步
WaitForSingleObject()
是用于等待对象信号状态的函数,声明如下:
DWORD WaitForSingleObject(
HANLDE hHandle,
DWORD dwMillisecondes
);
实验使用如下:
WaitForSingleObject(shmaddr->sem_full, INFINITE);
说明
- 参数
hHandle
指示要等待信号状态的对象句柄 - 参数
INFINITE
表示等待时间无限 - 实验中使用该函数由如下两个用途
- 等待信号量,该操作作为信号量操作中的_P操作_
- 等待子进程结束,防止造成孤儿进程
释放信号量
ReleaseSemaphore()
是用于给指定信号量增加指定值的函数,声明如下:
BOOL ReleaseSemaphore(
HANDLE hSemaphore,
LONG lReleaseCount,
LPLONG lpPreviousCount
);
实验使用如下:
ReleaseSemaphore(shmaddr->sem_mutex, 1, NULL);
说明
- 参数
hSemaphore
是指定信号量的句柄 - 参数
lReleaseCount
是给信号量增加的值 - 该操作作为信号量操作中的_V操作_
关闭内存映射
UnmapViewOfFile()
是用于停止当前程序的一个内存映射的函数,声明如下:
BOOL WINAPI UnmapViewOfFile(
_In_LPCVOID lpBaseAddress
);
实验使用如下:
UnmapViewOfFile(pFile);
说明
- 参数
pFile
是函数MapViewOfFile()
函数返回的文件映射对象句柄 - 该函数用于解除当前进程地址空间对一个文件映射对象的映射
关闭句柄
CloseHandle()
是用于关闭现有已打开句柄的函数,声明如下:
BOOL CloseHandle(
HANDLE hObject
);
实验使用如下:
CloseHandle(hFileMapping);
说明
- 参数
hFileMapping
是函数OpenFileMapping()
的返回值,是一个已经打开的文件映射对象句柄 - 该函数解除了对该进程对文件映射对象句柄的使用,防止内核泄漏
4.1.3 程序代码
本实验程序源代码分为四个文件,分别是
- 用于全局声明的头文件
ProducerConsumer.hpp
- 用于作为父进程的执行文件
ProducerConsumer.cpp
- 用于作为生产者进程的执行文件
Producer.cpp
- 用于作为消费者进程的执行文件
Consumer.cpp
//ProducerConsumer.hpp
#include <stdio.h>
#include <stdlib.h>
#include <iostream>
#include <time.h>
#include <windows.h>
#include <unistd.h>
#define NUM_PRODUCER 2
#define TIME_PRODUCER 6
#define NUM_CONSUMER 3
#define TIME_CONSUMER 4
#define BUFFER_LEN 3
#define SHM_NAME "Buffer"
static HANDLE handleFileMapping;
typedef struct buffer
{
int buff[BUFFER_LEN];
int head;
int tail;
int empty;
} buffer;
typedef struct shareMemory
{
buffer buffer_data;
HANDLE sem_full;
HANDLE sem_empty;
HANDLE sem_mutex;
} shm;
//ProducerConsumer.cpp
#include "ProducerConsumer.hpp"
//Window下共享内存是由FileMapping实现的
HANDLE shareMemory(){
//创建临时文件映射对象,使用INVALID_HANDLE_VALUE代替文件句柄
HANDLE handleFileMapping=CreateFileMapping(INVALID_HANDLE_VALUE,NULL,PAGE_READWRITE,0,sizeof(shm),SHM_NAME);
if (handleFileMapping==NULL||handleFileMapping==INVALID_HANDLE_VALUE) {
printf("Create File Mapping Failed!\n");
exit(1);
}
//把文件映射对象的一个View映射到进程地址空间,返回文件映射其实地址
LPVOID shmaddr=MapViewOfFile(handleFileMapping,FILE_MAP_ALL_ACCESS,0,0,0);
if (shmaddr==NULL) {
printf("Map View of File Failed!\n");
exit(1);
}
ZeroMemory(shmaddr,sizeof(shm));
//解除当前地址空间的映射
UnmapViewOfFile(shmaddr);
return handleFileMapping;
}
int main(int argc, char const *argv[])
{
// SYSTEMTIME time;
//创建共享内存
handleFileMapping=shareMemory();
HANDLE hFileMapping=OpenFileMapping(FILE_MAP_ALL_ACCESS,FALSE,SHM_NAME);
if (hFileMapping==NULL) {
printf("Open File Mapping Failed!\n");
exit(1);
}
LPVOID pFile=MapViewOfFile(hFileMapping,FILE_MAP_ALL_ACCESS,0,0,0);
if (pFile==NULL) {
printf("Map View of File Failed!\n");
exit(1);
}
shm *shmaddr=(shm*)(pFile);
shmaddr->buffer_data.head=0;
shmaddr->buffer_data.tail=0;
shmaddr->buffer_data.empty = 1;
shmaddr->sem_empty=CreateSemaphore(NULL,BUFFER_LEN,BUFFER_LEN,"SEM_EMPTY");
shmaddr->sem_full=CreateSemaphore(NULL,0,BUFFER_LEN,"SEM_FULL");
shmaddr->sem_mutex=CreateSemaphore(NULL,1,1,"SEM_MUTEX");
UnmapViewOfFile(pFile);
pFile=NULL;
CloseHandle(hFileMapping);
//创建子进程
// char szCmdLine[MAX_PATH];
char szFilename[MAX_PATH];
HANDLE lphandles[NUM_CONSUMER+NUM_PRODUCER];
//创建生产者
for(int i = 0; i < NUM_PRODUCER; i++)
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
sprintf(szFilename,"./Producer.exe");
// sprintf(szCmdLine,"\"%s\"","./Producer.exe");
ZeroMemory(&si,sizeof(STARTUPINFO));
si.cb=sizeof(STARTUPINFO);
if (!CreateProcess(szFilename,NULL,NULL,NULL,FALSE,0,NULL,NULL,&si,&pi)) {
printf("Create Producer Process Failed\n");
exit(1);
}
//把进程句柄存下来
lphandles[i]=pi.hProcess;
}
for(int i = 0; i < NUM_CONSUMER; i++)
{
STARTUPINFO si;
PROCESS_INFORMATION pi;
sprintf(szFilename,"./Consumer.exe");
// sprintf(szCmdLine,"\"%s\"","./Producer.exe");
ZeroMemory(&si, sizeof(STARTUPINFO));
si.cb = sizeof(STARTUPINFO);
if (!CreateProcess(szFilename, NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi))
{
printf("Create Consumer Process Failed\n");
exit(1);
}
//把进程句柄存下来
lphandles[NUM_PRODUCER+i] = pi.hProcess;
}
// WaitForMultipleObjects(NUM_CONSUMER+NUM_PRODUCER,lphandles,TRUE,INFINITE);
for(int i = 0; i < NUM_CONSUMER+NUM_PRODUCER; i++)
{
WaitForSingleObject(lphandles[i],INFINITE);
CloseHandle(lphandles[i]);
}
CloseHandle(handleFileMapping);
handleFileMapping=INVALID_HANDLE_VALUE;
// Sleep(2000);
return 0;
}
//Producer.cpp
#include "ProducerConsumer.hpp"
int main(int argc, char const *argv[])
{
SYSTEMTIME time;
HANDLE hFileMapping=OpenFileMapping(FILE_MAP_ALL_ACCESS,FALSE,SHM_NAME);
if (hFileMapping==NULL) {
printf("In Producer Open File Mapping Failed!\n");
exit(1);
}
LPVOID pFile=MapViewOfFile(hFileMapping,FILE_MAP_ALL_ACCESS,0,0,0);
if (pFile==NULL) {
printf("Map View of File Failed!\n");
exit(1);
}
shm *shmaddr=(shm*)(pFile);
shmaddr->sem_empty=OpenSemaphore(SEMAPHORE_ALL_ACCESS,FALSE,"SEM_EMPTY");
shmaddr->sem_full=OpenSemaphore(SEMAPHORE_ALL_ACCESS,FALSE,"SEM_FULL");
shmaddr->sem_mutex=OpenSemaphore(SEMAPHORE_ALL_ACCESS,FALSE,"SEM_MUTEX");
int pid=GetCurrentProcessId();
for(int i = 0; i < TIME_PRODUCER; i++)
{
srand(pid+i);
int input=rand()%3000;
Sleep(input);
WaitForSingleObject(shmaddr->sem_empty,INFINITE);
WaitForSingleObject(shmaddr->sem_mutex,INFINITE);
shmaddr->buffer_data.buff[shmaddr->buffer_data.tail]=input;
shmaddr->buffer_data.tail=(shmaddr->buffer_data.tail+1)%BUFFER_LEN;
shmaddr->buffer_data.empty=0;
GetSystemTime(&time);
printf("Now,the time is %04d:%02d:%02d-%02d:%02d:%02d\n",time.wYear,time.wMonth,time.wDay,time.wHour+8,time.wMinute,time.wSecond);
fflush(stdout);
printf("Producer %d put %d into buffer\n",pid,input);
fflush(stdout);
printf("The buffer contains: ");
fflush(stdout);
//由于是生产者,所以缓冲区不会为空
for (int i = shmaddr->buffer_data.head;;)
{
printf("%d ", shmaddr->buffer_data.buff[i]);
fflush(stdout);
i = (i + 1) % BUFFER_LEN;
if (i == shmaddr->buffer_data.tail)
{
printf("\n\n");
fflush(stdout);
break;
}
}
ReleaseSemaphore(shmaddr->sem_mutex, 1, NULL);
ReleaseSemaphore(shmaddr->sem_full, 1, NULL);
}
UnmapViewOfFile(pFile);
pFile=NULL;
CloseHandle(hFileMapping);
return 0;
}
//Consumer.cpp
#include "ProducerConsumer.hpp"
int main(int argc, char const *argv[])
{
SYSTEMTIME time;
HANDLE hFileMapping = OpenFileMapping(FILE_MAP_ALL_ACCESS, FALSE, SHM_NAME);
if (hFileMapping == NULL)
{
printf("In Consumer Open File Mapping Failed!\n");
exit(1);
}
LPVOID pFile = MapViewOfFile(hFileMapping, FILE_MAP_ALL_ACCESS, 0, 0, 0);
if (pFile == NULL)
{
printf("Map View of File Failed!\n");
exit(1);
}
shm *shmaddr = (shm *)(pFile);
shmaddr->sem_empty = OpenSemaphore(SEMAPHORE_ALL_ACCESS, FALSE, "SEM_EMPTY");
shmaddr->sem_full = OpenSemaphore(SEMAPHORE_ALL_ACCESS, FALSE, "SEM_FULL");
shmaddr->sem_mutex = OpenSemaphore(SEMAPHORE_ALL_ACCESS, FALSE, "SEM_MUTEX");
int pid = GetCurrentProcessId();
for (int i = 0; i < TIME_CONSUMER; i++)
{
srand(pid);
int sleep_time = rand() % 3000;
Sleep(sleep_time);
WaitForSingleObject(shmaddr->sem_full, INFINITE);
WaitForSingleObject(shmaddr->sem_mutex, INFINITE);
int output=shmaddr->buffer_data.buff[shmaddr->buffer_data.head];
shmaddr->buffer_data.head=(shmaddr->buffer_data.head+1)%BUFFER_LEN;
GetSystemTime(&time);
printf("Now,the time is %04d:%02d:%02d-%02d:%02d:%02d\n", time.wYear, time.wMonth, time.wDay, time.wHour + 8, time.wMinute, time.wSecond);
fflush(stdout);
printf("Consumer %d read %d out buffer\n", pid, output);
fflush(stdout);
if (shmaddr->buffer_data.head==shmaddr->buffer_data.tail) {
shmaddr->buffer_data.empty=1;
printf("Now The buffer is Empty!!!\n\n");
}
else{
printf("Now The Buffer Contains: ");
fflush(stdout);
for (int i = shmaddr->buffer_data.head;;)
{
printf("%d ", shmaddr->buffer_data.buff[i]);
fflush(stdout);
i = (i + 1) % BUFFER_LEN;
if (i == shmaddr->buffer_data.tail)
{
printf("\n\n");
fflush(stdout);
break;
}
}
}
ReleaseSemaphore(shmaddr->sem_mutex, 1, NULL);
ReleaseSemaphore(shmaddr->sem_empty, 1, NULL);
}
UnmapViewOfFile(pFile);
pFile = NULL;
CloseHandle(hFileMapping);
return 0;
}
程序运行方式如下:
> g++ ProducerConsumer.cpp -o ProduceConsumer.exe
> g++ Producer.cpp -o Producer.exe
> g++ Consumer.cpp -o Consumer.exe
> ProducerConsumer.exe
4.1.4 运行结果
两个生产者进程和三个消费者进程并行执行,所以结果每次都有所不同,以下只展示某次结果的部分内容
4.2 macOS实现
4.2.1 数据结构
缓冲区
定义缓冲区结构体如下:
//定义缓冲区结构体
typedef struct buff
{
int buff[BUFFER_LEN];
int head;
int tail;
int empty;
} buffer;
说明
buff
:表示缓冲区内容head
:表示缓冲区头指针,用于生产者写入tail
:表示缓冲区尾指针,用于消费者读出empty
:标志位,判断buff是否为空,因为仅仅使用头尾指针无法判断缓冲区为满或为空
4.2.2 API介绍
创建信号量集
semget()
用于获取与某个建关联的信号量集标识,声明如下:
int semget(
key_t key,
int nsems,
int semflg
);
实验使用如下:
int sem_id=semget(SEM_ID,NUM_SEM,IPC_CREAT|0600);
说明
- 参数
SEM_ID
:宏定义,表示的是信号量集的键值 - 参数
NUM_SEM
:宏定义,表示信号量个数 - 参数
IPC_CREAT
:由于键值不为IPC_PRIVATE
,且键对应的信号量集不存在,在标志中指定IPC_CREAT
可以创建新的信号量集
信号量操作
semctl()
是用于执行在信号量集上的控制操作的函数,声明如下:
int semctl(
int semid,
int semnum,
int cmd,
union semun arg
);
实验使用该函数用于信号量初始化和信号量集删除,使用如下:
semctl(sem_id,SEM_EMPTY,SETVAL,sem_val);
semctl(sem_id,IPC_RMID,0);
说明
sem_id
:函数semget
的返回值,标识一个信号量集SEM_EMPTY
:宏定义,标识出信号量集中的第几个信号量SETVAL
:控制操作命令,表示信号量初始化置值sem_val
:一个union semun
的变量,对其中val
进行赋值,用于对信号量进行初始化IPC_RMID
:控制操作命令,表示删除信号量集
P、V操作
semop()
是用于信号量的值与相应资源使用情况相关的操作的函数,声明如下:
int semop(
int semid,
struct sembuf *sops,
size_t nsops
);
实验使用将在4.2.3中提到
说明
semid
:信号集标识符,函数semget()
返回值sops
:指向存储信号操作结构的数组指针,信号操作结构将在4.2.3提到nsops
:信号操作结构的数量,大于等于1
创建共享内存对象
shmget()
是用于创建共享内存对象的函数,声明如下:
int shmget(
key_t key,
size_t size,
int shmflg
);
实验使用如下:
int shm_id=shmget(SHM_KEY,sizeof(buffer),SHM_MODE|IPC_CREAT);
说明
SHM_KEY
:宏定义,作为共享内存的键值,当该值为0或IPC_PRIVATE时会建立新的共享内存对象SHM_MODE
:宏定义,表示对该共享内存区域的访问模式及权限IPC_CREAT
:用于shmflg
作为标志,当内存中不存在与键匹配的共享内存对象时创建一个共享内存
共享内存区映射
shmat()
是用于把共享内存区对象映射到调用进程的地址空间的函数,声明如下:
void *shmat(
int shmid,
const void *shmaddr,
int shmflg
);
实验使用如下:
buffer* shmaddr=shmat(shm_id,0,0);
说明
shm_id
:共享内存标识符,为函数shmget()
函数的返回值shmaddr
:该函数返回的一个附加好的共享内存地址
断开连接
shmdt()
是用于断开共享内存连接的函数,声明如下:
int shmdt(
const void *shmaddr
);
实验使用如下:
shmdt(shmaddr);
说明
shmaddr
:连接共享内存的起始地址,函数shmat()
函数返回值- 该函数断开了现进程与共享内存区的连接,为后面删除共享内存区准备
共享内存删除
shmctl()
是用于完成对共享内存控制的函数,声明如下:
int shmctl(
int shmid,
int cmd,
struct shmid_ds *buf
);
实验使用如下:
shmctl(shm_id,IPC_RMID,0);
说明
shm_id
:共享内存标识符,为函数shmget()
函数返回值IPC_RMID
:操作命令,表示删除这片共享内存
4.2.3 自定义子函数
信号操作结构体
对信号量集定义P、V操作时,需要使用到sembuf
结构体,声明如下:
struct sembuf{
unsigned short int sem_num;
short int sem_op;
short int sem_flg;
};
说明
sem_num
:标识信号量集中的第几个信号量,从_0_开始sem_op
:标识对信号量所进行的操作,有如下几种:- 大于0:对该信号量执行挂出操作,即_V操作_,增加对值由
sem_op
决定 - 小于0:对该信号量执行等待操作,即_P操作_
- 等于0:表示调用者希望设置值
semval
为0,若为0则返回,否则信号量的semzcnt
加1,阻塞等待
- 大于0:对该信号量执行挂出操作,即_V操作_,增加对值由
sem_flag
:信号量操作属性标志,为0表示正常操作
P操作子函数
void P_op(int sem_id,int sem_num){
struct sembuf buf;
buf.sem_num=sem_num;
buf.sem_op=-1;
buf.sem_flg=0;
semop(sem_id,&buf,1);
}
V操作子函数
void V_op(int sem_id,int sem_num){
struct sembuf buf;
buf.sem_num=sem_num;
buf.sem_op=1;
buf.sem_flg=0;
semop(sem_id,&buf,1);
}
4.2.4 程序代码
本程序代码以附件的形式给出,包含于ProducerConsumer.c
文件中
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <stdlib.h>
#define NUM_PRODUCER 2
#define TIME_PRODUCER 6
#define NUM_CONSUMER 3
#define TIME_CONSUMER 4
#define BUFFER_LEN 3
#define NUM_SEM 3
#define SEM_ID 233
#define SEM_EMPTY 0
#define SEM_FULL 1
#define SEM_MUTEX 2
#define SHM_KEY 666
#define SHM_MODE 0777
//定义缓冲区结构体
typedef struct buff
{
int buff[BUFFER_LEN]; //缓冲区内容
int head; //缓冲区头指针
int tail; //缓冲区尾指针
int empty; //设置标志位,判断是否buff为空
} buffer;
//定义P操作
void P_op(int sem_id,int sem_num){
struct sembuf buf;
buf.sem_num=sem_num;
buf.sem_op=-1;
buf.sem_flg=0;
semop(sem_id,&buf,1);
}
//定义V操作
void V_op(int sem_id,int sem_num){
struct sembuf buf;
buf.sem_num=sem_num;
buf.sem_op=1;
buf.sem_flg=0;
semop(sem_id,&buf,1);
}
void Producer_op(int sem_id,buffer* shmaddr){
srand((unsigned)(time(NULL))+getpid());
int input = rand();
int sleep_time = input % 10; //随机等待时间
sleep(sleep_time);
P_op(sem_id,SEM_EMPTY); //申请emtpy
P_op(sem_id, SEM_MUTEX); //加锁
shmaddr->buff[shmaddr->tail]=input;//取一个随机数,从尾写入
shmaddr->tail=(shmaddr->tail+1)%BUFFER_LEN;
shmaddr->empty=0;
time_t now;
time(&now);
struct tm *localnow = localtime(&now);
printf("现在时间为:%s", asctime(localnow));
fflush(stdout);
pid_t pid=getpid();
printf("生产者%d将%d放入缓冲区中\n",pid,input);
fflush(stdout);
printf("现在缓冲区中有:");
fflush(stdout);
//由于是生产者,所以缓冲区不会为空
for(int i = shmaddr->head;;)
{
printf("%d ",shmaddr->buff[i]);
fflush(stdout);
i = (i + 1) % BUFFER_LEN;
if (i==shmaddr->tail) {
printf("\n\n");
fflush(stdout);
break;
}
}
// fflush(stdout);
V_op(sem_id, SEM_MUTEX); //解锁
V_op(sem_id,SEM_FULL); //生成full
}
void Consumer_op(int sem_id, buffer *shmaddr){
srand((unsigned)(time(NULL)));
sleep(rand()%10);
P_op(sem_id,SEM_FULL); //申请消费
P_op(sem_id, SEM_MUTEX); //加锁
int output=shmaddr->buff[shmaddr->head];//读取数据
shmaddr->head=(shmaddr->head+1)%BUFFER_LEN;
if (shmaddr->head==shmaddr->tail) {//头尾相等,缓冲为空
shmaddr->empty=1;
}
time_t now;
time(&now);
struct tm *localnow = localtime(&now);
printf("现在时间为:%s", asctime(localnow));
fflush(stdout);
pid_t pid = getpid();
printf("消费者%d从缓冲区中读出%d\n", pid, output);
fflush(stdout);
if (shmaddr->empty) {
printf("此时缓冲区为空\n\n");
fflush(stdout);
}
else
{
printf("现在缓冲区中有:");
fflush(stdout);
for (int i = shmaddr->head;;)
{
printf("%d ", shmaddr->buff[i]);
fflush(stdout);
i = (i + 1) % BUFFER_LEN;
if (i == shmaddr->tail)
{
printf("\n\n");
fflush(stdout);
break;
}
}
}
// fflush(stdout);
V_op(sem_id, SEM_MUTEX); //解锁
V_op(sem_id,SEM_EMPTY); //释放empty
}
int main(int argc, char const *argv[])
{
//创建信号量
int sem_id=semget(SEM_ID,NUM_SEM,IPC_CREAT|0600); //信号量集
if (sem_id==-1) {
printf("Semget Failed!\n");
_Exit(1);
}
union semun sem_val;
sem_val.val=BUFFER_LEN;
semctl(sem_id,SEM_EMPTY,SETVAL,sem_val); //信号量empty初始化为缓冲区长度
sem_val.val=0;
semctl(sem_id,SEM_FULL,SETVAL,sem_val); //信号量full初始化为0
sem_val.val=1;
semctl(sem_id,SEM_MUTEX,SETVAL,sem_val); //信号量mutex初始化为1
//申请共享内存
int shm_id=shmget(SHM_KEY,sizeof(buffer),SHM_MODE|IPC_CREAT);
if (shm_id==-1) {
printf("Shmget Failed!\n");
_Exit(1);
}
//将共享内存附加到进程空间返回虚地址
buffer* shmaddr=shmat(shm_id,0,0);
if (shmaddr==(void*)-1) {
printf("Shmat Failed!\n");
_Exit(1);
}
else {
//为缓冲区结构头尾指针赋值
shmaddr->head=0;
shmaddr->tail=0;
shmaddr->empty=1;
}
for(int pn = 0; pn < NUM_PRODUCER; pn++)
{
pid_t pid=fork();
if (pid==-1) {
printf("Create Producer Failed!\n");
_Exit(1);
}
else if (pid==0){
//将共享区附加到子进程地址空间
shmaddr=shmat(shm_id,0,0);
if (shmaddr==(void*)-1) {
printf("Shmat to Producer Failed!\n");
}
for(int pt = 0; pt < TIME_PRODUCER; pt++)
{
Producer_op(sem_id,shmaddr);
}
//解除共享内存连接
shmdt(shmaddr);
_Exit(0);
}
}
for(int cn = 0; cn < NUM_CONSUMER; cn++)
{
pid_t pid=fork();
if (pid==-1) {
printf("Create Consumer Failed!\n");
_Exit(1);
}
if (pid==0) {
//将共享内存连接到进程空间
shmaddr=shmat(shm_id,0,0);
if (shmaddr==(void*)-1) {
printf("Shmat to Comsumer Failed!\n");
_Exit(1);
}
for(int ct = 0; ct < TIME_CONSUMER; ct++)
{
Consumer_op(sem_id,shmaddr);
}
shmdt(shmaddr);
_Exit(0);
}
}
//等待所有子进程完成
while(wait(0)!=-1);
shmdt(shmaddr);
shmctl(shm_id,IPC_RMID,0);
semctl(sem_id,IPC_RMID,0);
printf("That's end!\n");
return 0;
}
程序运行方式如下:
$ gcc ProducerConsumer.c -o ProducerConsumer
$ ./ProducerConsumer
4.2.5 运行结果
两个生产者进程和三个消费者进程并行执行,所以结果每次都有所不同,以下只展示某次结果的部分内容
使用命令ipcs
可以查看内存中共享区域使用情况,运行时内存共享如下:
可以看出,由于程序中使用共享内存和信号量,所以内存中存在对应的数据
在程序执行之后,必须要把信号量和共享内存删除,否则会导致遗留,运行结束后内存共享应该如下:
5. 实验收获与体会
- 本实验让我进一步熟悉了Windows、macOS中进程控制的创建与管理的相关内容,熟练掌握了Windows和macOS下进程的IPC通信
- Windows和macOS进程之间通信方式不同,Windows利用文件映射创建共享缓冲区进行共享内存,而macOS使用信号量机制和直接申请共享内存方式通信
- 进一步了解了生产者消费者问题的解决方式以及P、V操作的控制过程
- 需要注意的是,信号量创建、打开后必须要进行删除操作,否则将会遗留共享内存区域和共享信号量。