记录于2022.7.7——南林操作系统课设心得
ps:这个实验我写了三天!!!!!!一是因为要参加夏令营和一些学校的宣讲会,二是因为网上大部分关于生产者消费者问题都是用线程实现的,用进程实现比较困难(因为进程比较难实现对信号量和缓冲区等变量的共享,只能通过共享内存来实现,而想通过共享内存来实现对这些变量的共享,要调用一些系统函数,网上关于这些系统函数的介绍又非常少,在我查阅了大量资料后,终于实现了用进程完成生产者消费者问题!!!)
<任务>
用程序实现生产者——消费者问题
问题描述:
一个仓库可以存放K件物品。生产者每生产一件产品,将产品放入仓库,仓库满了就停止生产。消费者每次从仓库中去一件物品,然后进行消费,仓库空时就停止消费。
数据结构:
进程:Producer - 生产者进程,Consumer - 消费者进程
buffer: array [0..k-1] of integer;
in,out: 0..k-1;
in记录第一个空缓冲区,out记录第一个不空的缓冲区
s1,s2,mutex: semaphore;
s1控制缓冲区不满,s2控制缓冲区不空,mutex保护临界区;
初始化s1=k,s2=0,mutex=1
原语描述
producer(生产者进程):
Item_Type item;
{
while (true)
{
produce(&item);
p(s1);
p(mutex);
buffer[in]:=item;
in:=(in+1) mod k;
v(mutex);
v(s2);
}
}
consumer(消费者进程):
Item_Type item;
{
while (true)
{
p(s2);
p(mutex);
item:=buffer[out];
out:=(out+1) mod k;
v(mutex);
v(s1);
}
}
#include <iostream>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/wait.h>
#include <sys/types.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
/* KEY 为申请信号量的键值,通过键值可以用于不同进程间的通信 */
#define KEY (key_t)14010322
/* 仓库大小 */
#define K 5
/*
union semun 常用于semctl的最后一个参数,
有的系统上sem.h已包含,可能会因为重复而报错
*/
#if defined(__GNU_LIBRARY__) && !defined(_SEM_SEMUN_UNDEFINED)
/* 如果union semun已经存在 */
#else
/* 如果不存在 */
union semun {
int val;
struct semid_ds* buf;
unsigned short* array;
};
#endif
/*
仓库,buffer数组用于存贮生产的商品编号(product)
in用于记录生产者生产的下一个商品应存贮在仓库的位置
out用于记录消费者消费的下一个商品在仓库中的位置
*/
typedef struct ShareBuffer {
int buffer[K];
// in记录第一个空缓冲区,out记录第一个不空的缓冲区
int in;
int out;
}ShareBuffer;
pid_t producer, consumer;
/*
struct sembuf{
short sem_num;
short sem_op;
short sem_flg;
};
sem_num是信号量的编号,如果你的工作不需要使用一组信号量,这个值一般就取为0。
sem_op是信号量一次PV操作时加减的数值,一般只会用到两个值,一个是“-1”,也就是P操作,
等待信号量变得可用;另一个是“+1”,也就是我们的V操作,发出信号量已经变得可用
sem_flag通常被设置为SEM_UNDO.她将使操作系统跟踪当前进程对该信号量的修改情况
*/
/*
int semop(int semid, struct sembuf *sops, unsigned nsops);
功能描述:
操作一个或一组信号。
参数:
semid :信号集的识别码,可通过 semget 获取。
sops :指向存储信号操作结构的数组指针,信号操作结构的原型如下
nsops :信号操作结构的数量,恒大于或等于 1 。
*/
/* p操作 */
void p(int semid, int semNum) {
struct sembuf sb;
sb.sem_num = semNum;
sb.sem_op = -1;
sb.sem_flg = SEM_UNDO;
semop(semid, &sb, 1);
}
/* v操作 */
void v(int semid, int semNum) {
struct sembuf sb;
sb.sem_num = semNum;
sb.sem_op = 1;
sb.sem_flg = SEM_UNDO;
semop(semid, &sb, 1);
}
// 随机生产一个1~100的元素
void produce(int& product) {
product = rand() % 100 + 1;
}
void killChildren(int sig) {
// 杀死子进程1,放出信号SIGUSR1
kill(producer, SIGUSR1);
// 杀死子进程2,放出信号SIGUSR2
kill(consumer, SIGUSR2);
// 父进程结束
std::cout << "Parent process is exit!" << std::endl;
}
int main()
{
int shmid; // 共享内存的id
char* shmPtr; // 信号量指针
int semid;
ShareBuffer* pSharebuffer; // 共享内存的指针
srand((unsigned)time(NULL)); // 初始化随机数种子
/*
int shmget(key_t key, size_t size, int shmflg);
函数说明——创建/打开共享内存
key——共享内存名称(ftok()函数获取)
size——开辟共享内存大小(以字节计)
shmflg——权限标志
返回值——成功返回共享内存的标识符。错误返回-1
*/
/* 创建共享内存 */
if ((semid = semget(KEY, 3, IPC_CREAT | 0660)) == -1)
{
std::cout << "semget error!" << std::endl;
return -1;
}
/* 三个信号量的初始赋值 */
// s1控制缓冲区不满, s2控制缓冲区不空, mutex保护临界区;
union semun s1, s2, mutex;
s1.val = K;
s2.val = 0;
mutex.val = 1;
/*
int semctl(int semid, int semnum, int cmd, union semun arg)
功能:控制信号量的信息。
返回值:成功返回0,失败返回-1;
参数:
semid 信号量集标识符
semnum, 操作信号在信号集中的编号。从0开始。
cmd 命令,表示要进行的操作(SETVAL设置信号量集中的一个单独的信号量的值。)
*/
// 定义信号量集
if (semctl(semid, 0, SETVAL, s1) == -1) { // s1编号为0
std::cout << "semctl failed!" << std::endl;
return -1;
}
if (semctl(semid, 1, SETVAL, s2) == -1) { // s2编号为1
std::cout << "semctl failed!" << std::endl;
return -1;
}
if (semctl(semid, 2, SETVAL, mutex) == -1) { // mutex编号为2
std::cout << "semctl failed!" << std::endl;
return -1;
}
/*
int shmget(key_t key, size_t size, int shmflg);
函数说明——创建/打开共享内存
key——共享内存名称(ftok()函数获取)
size——开辟共享内存大小
shmflg——权限标志
返回值——成功返回共享内存的标识符。错误返回-1
*/
// 此共享内存的权限为可读可写
if ((shmid = shmget(IPC_PRIVATE, sizeof(ShareBuffer), IPC_CREAT | 0600)) < 0)
{
std::cout << "shmget error!" << std::endl;
return -1;
}
/*
void *shmat(int shmid, const void *shmaddr, int shmflg);
函数说明——将共享内存与当前进程相关联
shmid——共享内存的标识符(shmget的返回值)
shmaddr——使用0(内核选择存储地址)
shmflg —— 使用0 (内存空间可读可写)
返回值——成功返回该共享内存的首地址。失败返回-1,错误代码在errno
*/
// 将共享内存与当前进程相关联
if ((shmPtr = (char*)shmat(shmid, 0, 0)) == (void*)-1)
{
std::cout << "shmat error!" << std::endl;
return -1;
}
/*
void * memset ( void * ptr, int value, size_t num );
memset() 函数用来将指定内存的前num个字节设置为特定的值
*/
memset((void*)shmPtr, 0, sizeof(ShareBuffer));
// 通过强制转换,来操控共享内存
pSharebuffer = (ShareBuffer*)shmPtr;
// 创建进程
while ((producer = fork()) == -1);
/* 子进程,生产者 */
if (producer == 0) {
// SIGUSR1为用户自定义信号
signal(SIGUSR1, NULL);
while (true)
{
int product;
produce(product);
p(semid, 0); // P(empty)
p(semid, 2); // P(mutex)
pSharebuffer->buffer[pSharebuffer->in] = product;
std::cout << "Producer process put element " << product << " in subscript " << pSharebuffer->in << std::endl;
pSharebuffer->in = (pSharebuffer->in + 1) % K;
v(semid, 2); // V(mutex)
v(semid, 1); // V(full)
// 睡眠10000~50000微秒(10~50毫秒)
int sleepTime = rand() % 40000 + 10000;
usleep(sleepTime);
}
}
else {
while ((consumer = fork()) == -1);
/* 子进程,消费者 */
if (consumer == 0) {
// SIGUSR2为用户自定义信号
signal(SIGUSR2, NULL);
while (true) {
int product;
p(semid, 1); // P(full)
p(semid, 2); // P(mutex)
product = pSharebuffer->buffer[pSharebuffer->out];
std::cout << "Consumer process get element " << product << " from subscript " << pSharebuffer->out << std::endl;
pSharebuffer->out = (pSharebuffer->out + 1) % K;
v(semid, 2); // V(mutex)
v(semid, 0); // V(empty)
// 睡眠10000~50000微秒(10~50毫秒)
int sleepTime = rand() % 40000 + 10000;
usleep(sleepTime);
}
}
else {
// 在指定闹钟时间到期后执行
signal(SIGALRM, killChildren);
// 定时1秒
alarm(1);
// 等待子进程结束
wait(NULL);
wait(NULL);
//去关联共享内存
shmdt(shmPtr);
// 删除信号量
// IPC_RMID 从内核中删除信号量集合
semctl(semid, 0, IPC_RMID, s1);
semctl(semid, 0, IPC_RMID, s2);
semctl(semid, 0, IPC_RMID, mutex);
}
}
return 0;
}
结果:
说明:为了能使进程之间共享信号量与缓冲区,s1、s2、mutex采用信号量集来实现,缓冲区采用共享内存来实现;为了体现出生产者和消费者进程之间的差异,给生产者与消费者进程设置随机睡眠时间。
当生产者进程生产一个商品时,首先利用信号量in来判断当前是否有空缓冲区,若没用,则生产者进程被阻塞,直到消费者进程释放出一个空缓冲区,生产者进程才从阻塞状态变为就绪状态;若当前存在空缓冲区,则消耗一个空缓冲区,通过互斥信号量mutex对缓冲区进行上锁,然后将生产的商品放入缓冲区中,然后释放mutex,并增加一个满缓冲区。
当生产者进程生产一个商品时,首先利用信号量out来判断当前是否有满缓冲区,若没用,则消费者进程被阻塞,直到生产者进程产生一个满缓冲区,消费者进程才从阻塞状态变为就绪状态;若当前存在满缓冲区,则消耗一个满缓冲区,并通过互斥信号量mutex对缓冲区进行上锁,从满缓冲区中获得商品,然后释放mutex,并增加一个空缓冲区。
由于缓冲区是通过循环队列来实现,所以增加s1、s2必须对K取余。
难点在于如何实现进程间对信号量以及缓冲区的共享,以及如何进行P、V操作,实现了这些,之后就没什么难度了。
至于为什么使用union semun,因为P、V操作里需要用到,调用系统函数就是这么奇奇怪怪,有兴趣的朋友可以深入研究一下。
定义ShareBuffer结构体,是为了方便对缓冲区以及in、out指针进行共享(可以理解为捆绑在一起,方便操作)。
另外,这里使用signal()等来终止子进程,可以看我另一篇博客: