实验三 生产者-消费者问题的实现
实验类型
验证性实验
实验目的
通过生产者-消费者问题的模拟,加深对进程同步、共享存储器通信的理解。
实验要求
实现生产者消费者问题模拟,显示每次添加和读取数据时缓冲区的状态,生产者和消费者用进程模拟,缓冲区用共享内存来实现。
一个大小为3的缓冲区,初始为空;
2个生产者:随机等待一段时间,往缓冲区添加数据,若缓冲区已满,等待消费者取走数据后再添加,重复6次。
3个消费者:随机等待一段时间,从缓冲区读取数据,若缓冲区为空,等待生产者添加数据后再读取,重复4次。
实验指导
一、共享存储区通信 1、共享存储区机制的概念
共享存储区(Share Memory)是UNIX系统中通信速度最高的一种通信机制。该机制可使若干进程共享主存中的某一个区域,且使该区域出现(映射)在多个进程的虚地址空间中。另一方面,一个进程的虚地址空间中又可连接多个共享存储区,每个共享存储区都有自己的名字。当进程间欲利用共享存储区进行通信时,必须先在主存中建立一共享存储区,然后将它附接到自己的虚地址空间上。此后,进程对该区的访问操作,与对其虚地址空间的其它部分的操作完全相同。进程之间便可通过对共享存储区中数据的读、写来进行直接通信。
应当指出,共享存储区机制只为进程提供了用于实现通信的共享存储区和对共享存储区
进行操作的手段,然而并未提供对该区进行互斥访问及进程同步的措施。因而当用户需要使用该机制时,必须自己设置同步和互斥措施才能保证实现正确的通信。
2、涉及的系统调用
(1)shmget( )
创建、获得一个共享存储区。 系统调用格式:
shmid=shmget(key,size,flag)
该函数使用头文件如下:
#include
参数定义
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
#include
参数定义
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
参数定义
int shmdt(addr); char addr;
其中,addr是要断开连接的虚地址,亦即以前由连接的系统调用shmat( )所返回的虚地址。调用成功时,返回0值,调用不成功,返回-1。
(4)shmctl( )
共享存储区的控制,对其状态信息进行读取和修改。(删除共享内存,使用shmctl) 系统调用格式:
shmctl(shmid,cmd,buf)
该函数使用头文件如下:
#include
参数定义
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、信号量集结构体
内核为每个信号量集维护一个信号量结构体,可在
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
(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; // 操作表示符