实验三 生产者-消费者问题

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

实验三生产者-消费者问题的实现
实验类型
验证性实验
实验目的
通过生产者-消费者问题的模拟,加深对进程同步、共享存储器通信的理解。

实验要求
实现生产者消费者问题模拟,显示每次添加和读取数据时缓冲区的状态,生产者和消费者用进程模拟,缓冲区用共享内存来实现。

一个大小为3的缓冲区,初始为空;
2个生产者:随机等待一段时间,往缓冲区添加数据,若缓冲区已满,等待消费者取走数据后再添加,重复6次。

3个消费者:随机等待一段时间,从缓冲区读取数据,若缓冲区为空,等待生产者添加数据后再读取,重复4次。

实验指导
一、共享存储区通信
1、共享存储区机制的概念
共享存储区(Share Memory)是UNIX系统中通信速度最高的一种通信机制。

该机制可使若干进程共享主存中的某一个区域,且使该区域出现(映射)在多个进程的虚地址空间中。

另一方面,一个进程的虚地址空间中又可连接多个共享存储区,每个共享存储区都有自己的名字。

当进程间欲利用共享存储区进行通信时,必须先在主存中建立一共享存储区,然后将它附接到自己的虚地址空间上。

此后,进程对该区的访问操作,与对其虚地址空间的其它部分的操作完全相同。

进程之间便可通过对共享存储区中数据的读、写来进行直接通信。

应当指出,共享存储区机制只为进程提供了用于实现通信的共享存储区和对共享存储区
进行操作的手段,然而并未提供对该区进行互斥访问及进程同步的措施。

因而当用户需要使用该机制时,必须自己设置同步和互斥措施才能保证实现正确的通信。

2、涉及的系统调用
(1)shmget( )
创建、获得一个共享存储区。

系统调用格式:
shmid=shmget(key,size,flag)
该函数使用头文件如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
参数定义
int shmget(key,size,flag);
key_t key;
int size,flag;
其中:key是共享存储区的名字;(唯一标识共享内存的键值。

两个进程在调用shmget 时,该参数必须一样,以使两个进程使用的是同一块内存)
size是其大小(以字节计);
flag是用户设置的标志,如IPC_CREAT。

IPC_CREAT表示若系统中尚无指名的共享存储区,则由核心建立一个共享存储区;若系统中已有共享存储区,便忽略IPC_CREAT。

返回值:成功返回共享内存标识码(将用于shmat的参数),该标识码能唯一标识一块共享内存,失败返回-1。

附:
操作允许权八进制数
用户可读00400
用户可写00200
小组可读00040
小组可写00020
其它可读00004
其它可写00002
例:shmid=shmget(key,size,(IPC_CREAT|0400))
创建一个关键字为key,长度为size的共享存储区
IPC(包括消息队列,共享内存,信号量)的xxxget()创建操作,可以指定IPC_CREAT和IPC_EXCL选项。

以共享内存为例:
当只有IPC_CREAT选项打开时,不管是否已存在该块共享内存,则都返回该共享内存的ID,若不存在则创建共享内存。

当只有IPC_EXCL选项打开时,不管有没有该块共享内存,shmget()都返回-1;
所以当IPC_CREAT|IPC_EXCL时,如果没有该块共享内存,则创建,并返回共享内存的ID,若已有该块共享内存,则返回-1。

(2)shmat( )
共享存储区的附接。

从逻辑上将一个共享存储区附接到进程的虚拟地址空间上。

系统调用格式:
virtaddr=shmat(shmid,addr,flag)
该函数使用头文件如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
参数定义
char *shmat(shmid,addr,flag);
int shmid,flag;
char * addr;
其中,shmid是共享存储区的标识符;addr是共享内存在本进程内的虚拟地址的起始地址,若addr为0,系统选择一个适当的地址来附接该共享区(在绝大多数情况下,由于程序员不可能知道当程序运行时,进程地址空间中哪些地址尚未使用,因此不可能给出一个准确的地址,所以通常情况下,该值都填0,表示由系统选定一个尚未使用的合适的地址。

被系统选定的该地址将作为函数的返回值返回)。

flag规定共享存储区的读、写权限。

其值为SHM_RDONLY时,表示只能读;其值为0时,表示可读、可写。

该系统调用的返回值是共享存储区所附接到的进程虚地址viraddr。

(3)shmdt( )
把一个共享存储区从指定进程的虚地址空间断开。

系统调用格式:
shmdt(addr)
该函数使用头文件如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
参数定义
int shmdt(addr);
char addr;
其中,addr是要断开连接的虚地址,亦即以前由连接的系统调用shmat( )所返回的虚地址。

调用成功时,返回0值,调用不成功,返回-1。

(4)shmctl( )
共享存储区的控制,对其状态信息进行读取和修改。

(删除共享内存,使用shmctl)系统调用格式:
shmctl(shmid,cmd,buf)
该函数使用头文件如下:
#include<sys/types.h>
#include<sys/ipc.h>
#include<sys/shm.h>
参数定义
int shmctl(shmid,cmd,buf);
int shmid,cmd;
struct shmid_ds *buf;
其中,buf是用户缓冲区地址,cmd是操作命令(最常用的包括:IPC_RMID删除共享内存段)。

命令可分为多种类型:
(1)用于查询有关共享存储区的情况。

如其长度、当前连接的进程数、共享区的创建者标识符等;
(2)用于设置或改变共享存储区的属性。

如共享存储区的许可权、当前连接的进程计数等;
(3)对共享存储区的加锁和解锁命令;
(4)删除共享存储区标识符等。

共享存储区示例,以下为部分代码:
#define BUF_LENGTH (sizeof(struct mybuffer)) //BUF_LENGTH为共享存储区的大小//定义循环缓冲,包含3个char类型的缓冲区
struct mybuffer
{
char letter[3];
int head; //指向满缓冲区的指针
int tail; //指向空缓冲区的指针
int is_empty; //循环缓冲是否为空
};
struct mybuffer * shmptr; //定义指向共享存储区的指针
int shmid = shmget(IPC_PRIVATE, BUF_LENGTH, IPC_CREAT|0600); //创建大小为BUF_LENGTH的共享存储区
shmptr = shmat(shmid, 0, 0);//附接到进程的虚地址空间,shmptr为指向共享区的指针//访问共享存储区举例(投放产品)
shmptr->letter[shmptr->tail] = get_letter(); // get_letter为随机获取A-Z字符的函数shmptr->tail = (shmptr->tail + 1) % 3;
shmdt(shmptr); //断开与进程虚地址空间的连接
shmctl(shmid, IPC_RMID, 0);//删除共享存储区
二、Linux中的信号量集机制
在Linux系统中,一个或多个信号量构成一个信号量集合。

使用信号量机制可以实现进程之间的同步和互斥,允许并发进程一次对一组信号量进行相同或不同的操作。

每个P、V 操作不限于减1或加1,而是可以加减任何整数。

在进程终止时,系统可根据需要自动消除所有被进程操作过的信号量的影响。

1、信号量集结构体
内核为每个信号量集维护一个信号量结构体,可在<sys/sem.h>找到该定义:
struct semid_ds {
struct ipc_perm sem_perm; /* 信号量集的操作许可权限*/
struct sem *sem_base; /* 某个信号量sem结构数组的指针,当前信号量集中的每个信号量对应其中一个数组元素*/
ushort sem_nsems; /* sem_base 数组的个数*/
time_t sem_otime; /* 最后一次成功修改信号量数组的时间*/
time_t sem_ctime; /* 成功创建时间*/
};
struct sem {
ushort semval; /* 信号量的当前值*/
short sempid; /* 最后一次返回该信号量的进程ID号*/
ushort semncnt; /* 等待semval大于当前值的进程个数*/
ushort semzcnt; /* 等待semval变成0的进程个数*/
}
2、信号量集函数
头文件:
#include <sys/ipc.h>
#include <sys/sem.h>
(1)信号量集的创建与打开semget
原型:int semget(key_t key,int nsems,int semflg);
参数:1)key表示所创建或打开信号量集的键(是用于唯一标识信号量的key)。

使用键IPC_PRIVATE来新建一个键。

2)nsems>0:创建一个新的信号量集,指定集合中信号量的数量,一旦创建就不能更改。

nsems=0:访问一个已存在的集合。

(nsems是信号量的个数,一般为1.之所以出现这个参数,是因为在SYSV中,将若干信号量组合在一起,形成信号量数组,nsems其实就是指数组的元素个数,semget用来生成一个信号量组,而不仅仅是生成一个信号量。

但一般情况下,只生成和使用一个信号量,因此大多数情况下调用semget时,都将nsems 的值指定为1。


3)semflg表示调用函数的操作类型,也可用于设置信号量集的访问权限,两者通过or表示。

IPC_CREAT单独使用:如果信号量集在系统内核中不存在,则创建信号量集。

semget()要么返回新创建的信号量集的标识符,要么返回系统中已经存在的同样的关键字值的信号量的标识符。

IPC(包括消息队列,共享内存,信号量)
IPC_EXCL和IPC_CREAT一同使用时:如果信号量集已经存在,则调用失败。

要么返回新创建的信号量集的标识符,要么返回-1
IPC_EXCL单独使用:没有意义
信号量集的访问权限:
用户读:0400
用户写:0200
组读:0040
组写:0020
其他读:0004
其他写:0002
4)返回值:如果成功,则返回一个称为信号量集标识符的整数,semop和semctl函数将使用它。

如果失败,则返回-1。

例如:创建一个包含2个信号量的信号量集,权限为所有用户均可读写。

int semid = semget(IPC_PRIVATE, 2, IPC_CREAT | 0666);(IPC_PRIVATE:创建一个新的IPC对象)
semget函数执行成功后,就产生了一个由内核维持的类型为semid_ds结构体的信号量集,返回的semid就是指向该信号量集的引索。

semget函数不可以对创建的信号量集中的信号量进行初始化,对信号量的初始化是通过另外的一个函数semctl进行的(用参数SET_VAL,SETALL进行初始化)。

(2)信号量的操作semop (PV操作)
调用原型:int semop(int semid,struct sembuf *opsptr, size_t nops);
其中1)semid为信号量集引用ID,即semget返回的semid。

2)opsptr:指向信号量操作结构数组sembuf。

(结构体指针,结构体中包含了具体操作(P操作或者V操作))
3)nops:opsptr所指向的数组中的sembuf结构体的个数(通常情况下等于1)。

struct sembuf {
short sem_num; // 要操作的信号量在信号量集里的编号,第一个信号的编号是0 short sem_op; // 信号量操作
short sem_flg; // 操作表示符
};
1)若sem_op 是负数,那么调用者希望等待semval(是指semid_ds中的信号量集中的某个信号量的值)变为大于或等于sem_op的绝对值,即申请|sem_op|个资源。

如果资源不可用,那么调用进程将进入睡眠状态,直到信号量控制的资源可以使用为止。

(若sem_op的值为负数,其绝对值又大于信号的现值,操作将会阻塞,直到信号值大于或等于sem_op的绝对值,通常用于获取资源的使用权)
若sem_op 是正数,其值就加到semval上,即释放sem_op个资源。

(若sem_op是正数,该值会加到现有的信号量内含值中,通常用于释放所控资源的使用权)如果sem_op是0,那么调用者希望等到semval变为0,如果semval是0就返回否则将调用sleep()睡眠。

(对于0-1信号量而言,执行P操作则将sem_op设为-1,执行V 操作则将sem_op设为+1)
2)sem_flg:信号操作标志,可能的选择有两种:
SEM_UNDO 由进程自动释放信号量(程序结束时(不论正常或不正常),保证信号值会被重设为semop()调用前的值。

这样做的目的在于避免程序异常情况结束时未将锁定的资源解锁,造成该资源永远被锁定)
IPC_NOWAIT 对信号的操作不能满足时,semop()不会阻塞,并立即返回,同时设定错误信息
//实现P操作
void P(int semid, int semnum)
{
struct sembuf sem_buff;
sem_buff.sem_num = semnum;
sem_buff.sem_op = -1;
sem_buff.sem_flg = 0;
semop(semid, &sem_buff, 1);
}
例如:P(semid,EMPTY); //对semid信号量集中的EMPTY信号量执行P操作
//实现V操作
void V(int semid, int semnum)
{
struct sembuf sem_buff;
sem_buff.sem_num = semnum;
sem_buff.sem_op = 1;
sem_buff.sem_flg = 0;
semop(semid, &sem_buff, 1);
}
例如:V(semid,EMPTY); //对semid信号量集中的EMPTY信号量执行V操作
(3)信号量的控制semctl
semctl函数可用于对信号量集中的信号量进行初始化,亦可用于删除信号量集。

原型:int semctl(int semid, int semnum, int cmd, union semun arg);
参数:semid为信号量集引用标识符,即semget()的返回值。

semnum是信号量在集合中的序号。

cmd表示调用该函数执行的操作(用于表示要设初值还是要进行其他操作),其取值和对应操作如下:
●IPC_STAT 返回信号量集当前的semid_ds结构(读取一个信号量集的数据结构
semid_ds),并将其存储在semun类型的第4个参数的的buf中。

●IPC_SET 设置信号量集的semid_ds结构中的元素ipc_perm,其取值来自semun
中的buf参数。

●IPC_RMID把由semid指定的信号量集从系统中删除掉。

●GETALL 读取信号量集中所有信号量的值。

●SETALL 设置信号量集中所有信号量的值。

●GETVAL 读取信号量集中单个信号量的值。

●SETVAL 设置信号量集中单个信号量的值。

●GETPID返回最后一个执行semop操作的进程的PID。

●GETNCNT返回正在等待资源的进程数目。

●GETZCNT返回这在等待完全空闲的资源的进程数目。

第四个参数arg是可选的,取决于第三个参数cmd(如果cmd要设置初值的话,那么第4个参数就是要设置的初始值)。

union semun结构如下:
union semun{
int val; /* used for SETVAL only */
struct semid_ds *buf; /* used for IPC_SET and IPC_STAT */
ushort *array; /* used for GETALL and SETALL */
};
这个联合体并没有出现在任何头文件中,如果用到该联合体必须由应用程序声明。

初始化信号量举例:
#define EMPTY 0
#define FULL 1
semctl(semid, EMPTY, SETVAL, 3); //索引为EMPTY的信号量值初始化为3
semctl(semid, FULL, SETVAL, 0); //索引为FULL的信号量值初始化为0
删除信号量举例:
semctl(semid,0, IPC_RMID, 0); //第三个参数cmd为IPC_RMID时,整个信号集被删除,第二个参数semnum被忽略。

相关文档
最新文档