关于System V共享内存和信号量的知识已经学过了,这次主要是综合这两者来解决一个问题达到灵活运用的目的,下面开始:
关于什么是“生产者消费者”,应该都比较清楚,这里还是先贴上百度百科对它的描述:
下面用图来说明一下该问题:
以上就是生产者消费者从逻辑上的一个解决方案,从中可以看到这是互斥跟同步相结合的例子,下面则用所画的这些模型来实现一下shmfifo
为什么要实现共享内存的先进先出的缓冲区(shmfifo)呢?实际上要实现进程间通信可以直接用消息队列来实现先进先出的队列,但是,由于消息队列还实现了其它的功能,如果仅仅只是想要先进先出这样的一个功能的话,能使用共享内存来实现的话,效率会更高,因为对共享内存的访问不涉及到对内核的操作,这个之前也有讲过,因此就有必要实现一个shmfifo。
要实现这样的一个缓冲区,我们可以做一些假定,假定放到缓冲区当中的数据块是定长的,并且可以有多个进程往缓冲区中写入数据,也有多个进程往缓冲区中读取数据,所以这是典型的生产者消费者问题,这块缓冲区刚才说过可以用共享内存的方式来实现,但是有一个问题需要思考:生产者进程当前应该在什么位置添加产品,消费者进程又从什么位置消费产品呢?所以说还需要维护这些状态,所以很自然地就能想到将这些状态保存在共享内存当中,如下:
由于多个生产者都能往里面添加产品,多个消费者也能够从里面消费产品,那生产者在生产产品的时候应该放在什么位置呢?消费者又该从哪里消费产品呢?下面来说明下:
而这时再次生产就会是在0的位置上开始了:
可见这是一个环形缓冲区,可以重复利用的,基于这些分析下面来看一下所定义出来的数据结构:
有了这些数据结构实际上就能够实现了shmfifo了,下面实现一下:
由于用到了信号量,所以将之前的信号量相关的函数及定义放到一个单独的文件当中,里面代码都是之前学过的,就不多解释了:
ipc.h:
#ifndef _IPC_H_
#define _IPC_H_
#include <sys/types.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <errno.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define ERR_EXIT(m) \
do \
{ \
perror(m); \
exit(EXIT_FAILURE); \
} while(0)
union semun {
int val; /* value for SETVAL */
struct semid_ds *buf; /* buffer for IPC_STAT, IPC_SET */
unsigned short *array; /* array for GETALL, SETALL */
/* Linux specific part: */
struct seminfo *__buf; /* buffer for IPC_INFO */
};
int sem_create(key_t key);
int sem_open(key_t key);
int sem_p(int semid);
int sem_v(int semid);
int sem_d(int semid);
int sem_setval(int semid, int val);
int sem_getval(int semid);
int sem_getmode(int semid);
int sem_setmode(int semid,char* mode);
#endif /* _IPC_H_ */
ipc.c:
#include "ipc.h"
int sem_create(key_t key)
{
int semid = semget(key, 1, 0666 | IPC_CREAT | IPC_EXCL);
if (semid == -1)
ERR_EXIT("semget");
return semid;
}
int sem_open(key_t key)
{
int semid = semget(key, 0, 0);
if (semid == -1)
ERR_EXIT("semget");
return semid;
}
int sem_p(int semid)
{
struct sembuf sb = {0, -1, 0};
int ret = semop(semid, &sb, 1);
if (ret == -1)
ERR_EXIT("semop");
return ret;
}
int sem_v(int semid)
{
struct sembuf sb = {0, 1, 0};
int ret = semop(semid, &sb, 1);
if (ret == -1)
ERR_EXIT("semop");
return ret;
}
int sem_d(int semid)
{
int ret = semctl(semid, 0, IPC_RMID, 0);
/*
if (ret == -1)
ERR_EXIT("semctl");
*/
return ret;
}
int sem_setval(int semid, int val)
{
union semun su;
su.val = val;
int ret = semctl(semid, 0, SETVAL, su);
if (ret == -1)
ERR_EXIT("semctl");
//printf("value updated...\n");
return ret;
}
int sem_getval(int semid)
{
int ret = semctl(semid, 0, GETVAL, 0);
if (ret == -1)
ERR_EXIT("semctl");
//printf("current val is %d\n", ret);
return ret;
}
int sem_getmode(int semid)
{
union semun su;
struct semid_ds sem;
su.buf = &sem;
int ret = semctl(semid, 0, IPC_STAT, su);
if (ret == -1)
ERR_EXIT("semctl");
printf("current permissions is %o\n",su.buf->sem_perm.mode);
return ret;
}
int sem_setmode(int semid,char* mode)
{
union semun su;
struct semid_ds sem;
su.buf = &sem;
int ret = semctl(semid, 0, IPC_STAT, su);
if (ret == -1)
ERR_EXIT("semctl");
printf("current permissions is %o\n",su.buf->sem_perm.mode);
sscanf(mode, "%o", (unsigned int*)&su.buf->sem_perm.mode);
ret = semctl(semid, 0, IPC_SET, su);
if (ret == -1)
ERR_EXIT("semctl");
printf("permissions updated...\n");
return ret;
}
以上文件是为了实现shmfifo提供辅助功能的,下面则开始实现它,分头文件及具体实现:
shmfifo.h:
#ifndef _SHM_FIFO_H_
#define _SHM_FIFO_H_
#include "ipc.h"
typedef struct shmfifo shmfifo_t;
typedef struct shmhead shmhead_t;
struct shmhead
{
unsigned int blksize; // 块大小
unsigned int blocks; // 总块数
unsigned int rd_index; // 读索引
unsigned int wr_index; // 写索引
};
struct shmfifo
{
shmhead_t *p_shm; // 共享内存头部指针
char *p_payload; // 有效负载的起始地址
int shmid; // 共享内存ID
int sem_mutex; // 用来互斥用的信号量
int sem_full; // 用来控制共享内存是否满的信号量
int sem_empty; // 用来控制共享内存是否空的信号量
};
shmfifo_t* shmfifo_init(int key, int blksize, int blocks);//初始化
void shmfifo_put(shmfifo_t *fifo, const void *buf);//添加数据到环形缓冲区
void shmfifo_get(shmfifo_t *fifo, void *buf);//从缓冲区中取数据
void shmfifo_destroy(shmfifo_t *fifo);//释放共享内存的环形缓冲区
#endif /* _SHM_FIFO_H_ */
下面来具体解释一下:
下面来具体实现一下些这函数:
这个方法既可以创建共享内存信号量,也可以打开共享内存信号量,所以下面可以做一个判断:
接下来还得初始化共享内存中的其它字段:
接下来对其信号量集中的信号进行初始化:
shmfifo的初始化函数就已经写完了,接下来来实现第二个函数:shmfifo_put(生产产品),对于生产者的过程,上面也说明过,则严格按照该步骤来进行实现:
下面则开始实现,首先先按照流程把代码框架写出来:
那如何生产产品呢?先来看下图:
首先进行数据偏移:
【说明】:关于memcpy函数的使用,说明如下:
在生产一个产品之后,下一次要生产的位置则要发生改变,所以:
这样生产产品的函数实现就如上,类似的,消费产品实现就容易了,依照这个流程:
具体实现如下:
接下来实现最后一个函数,就是资源释放:
好了,shmfifo.c的所有函数已经实现,先贴出代码:
shmfifo.c:
#include "shmfifo.h"
#include <assert.h>
shmfifo_t* shmfifo_init(int key, int blksize, int blocks)
{
//分配内存空间
shmfifo_t *fifo = (shmfifo_t *)malloc(sizeof(shmfifo_t));
assert(fifo != NULL);
memset(fifo, 0, sizeof(shmfifo_t));
int shmid;
shmid = shmget(key, 0, 0);
int size = sizeof(shmhead_t) + blksize*blocks;
if (shmid == -1)
{//创建共享内存
fifo->shmid = shmget(key, size, IPC_CREAT | 0666);
if (fifo->shmid == -1)
ERR_EXIT("shmget");
fifo->p_shm = (shmhead_t*)shmat(fifo->shmid, NULL, 0);
if (fifo->p_shm == (shmhead_t*)-1)
ERR_EXIT("shmat");
fifo->p_payload = (char*)(fifo->p_shm + 1);
fifo->sem_mutex = sem_create(key);
fifo->sem_full = sem_create(key+1);
fifo->sem_empty = sem_create(key+2);
sem_setval(fifo->sem_mutex, 1);
sem_setval(fifo->sem_full, blocks);
sem_setval(fifo->sem_empty, 0);
}
else
{//打开共享内存
fifo->shmid = shmid;
fifo->p_shm = (shmhead_t*)shmat(fifo->shmid, NULL, 0);
if (fifo->p_shm == (shmhead_t*)-1)
ERR_EXIT("shmat");
fifo->p_payload = (char*)(fifo->p_shm + 1);
fifo->sem_mutex = sem_open(key);
fifo->sem_full = sem_open(key+1);
fifo->sem_empty = sem_open(key+2);
}
return fifo;
}
void shmfifo_put(shmfifo_t *fifo, const void *buf)
{
sem_p(fifo->sem_full);
sem_p(fifo->sem_mutex);
//生产产品
memcpy(fifo->p_payload+fifo->p_shm->blksize*fifo->p_shm->wr_index,
buf, fifo->p_shm->blksize);
fifo->p_shm->wr_index = (fifo->p_shm->wr_index + 1) % fifo->p_shm->blocks;
sem_v(fifo->sem_mutex);
sem_v(fifo->sem_empty);
}
void shmfifo_get(shmfifo_t *fifo, void *buf)
{
sem_p(fifo->sem_empty);
sem_p(fifo->sem_mutex);
memcpy(buf, fifo->p_payload+fifo->p_shm->blksize*fifo->p_shm->rd_index,
fifo->p_shm->blksize);
fifo->p_shm->rd_index = (fifo->p_shm->rd_index + 1) % fifo->p_shm->blocks;
sem_v(fifo->sem_mutex);
sem_v(fifo->sem_full);
}
void shmfifo_destroy(shmfifo_t *fifo)
{
//删除创建的信息量集
sem_d(fifo->sem_mutex);
sem_d(fifo->sem_full);
sem_d(fifo->sem_empty);
//删除共享内存
shmdt(fifo->p_shm);//删除共享内存头部
shmctl(fifo->shmid, IPC_RMID, 0);//删除整个共享内存
//释放fifo的内存
free(fifo);
}
下面则写两个测试程序,分别用来生产、消费产品:
下面来实现一下:
同样的,取出存放进去的学生信息,如下:
下面编译运行:
居然生产第一个产品的时候就已经报错了,从这个错误当中很难定位到问题在哪,而这个例外肯定是产生了一个信号,所以下面用gdb来调试一下程序,再正式调查试之前,需要将之前创建的共享内存及信号量集给清掉,否则再次运行就不会报这个错,而是阻塞了:
而手动一个个去删除这些比较麻烦,因为我们已经编写好了资源的释放函数了,所以可以编写一个专门释放的程序,如下:
下面先将之前资源清除掉:
下面再次运行就会抛出例外,所以这次就可以进行gdb调试来看问题出在哪?
所以怎么来修复这个问题就比较容易了,只要做下初始化操作既可:
下面再来运行:
接下来运行一下接收产品的程序,一起来看下生产者消费者的一个效果:
从实验结果来看,当接收了数据之后,原来还在等待的2个学生信息就被成功发送了,这就是生产者与消费者的一个效果,实际中生产者可以有多个,消费者也可以有多个,这次学的内容有些多,通过这个例子可以学习怎么利用共享内存和信号量来实现一个先进先出的环形缓冲区shmfifo,需好好消化,下回见~