linux编程综合案例(生产者消费者问题)

linux编程综合案例(生产者消费者问题)

前面一系列练习已经把进程控制、线程、进程间通信的大概知识过了一遍,现在进入综合练习,首先练习经典问题:生产者和消费者问题。

(不要问我前面一系列到底是个什么东西,csdn上基本上全都是转载,且都有这句话,我查了半天这个篇文章哪里来的,最早我只能追溯到Linux编程日日练 --生产者消费者问题2010年5月18日的文章,结果文章上面也标明了是转载,也不知道从哪转的,更早的我已经找不到了。
  因为强迫症,实在看不惯前面的人随便转载,为了操作系统的实验又必须学(ノ`Д)ノ,自己又小白不怎么懂/(ㄒoㄒ)/~~,就简单排版,再加些自己了解的简单内容,重新整理编排了下。前半部分主要对这个例子简单加了点分析,后半部分加了点修正。简单看看就好。
  好像有点长,可以从左边目录跳转到需要看的内容。

1.问题概述

多个生产/消费者在有界缓冲上操作。它利用N个字节的共享内存作为有界循环缓冲区,利用写一字符模拟放一个产品,利用读一字符模拟消费一个产品。当缓冲区空时消费者阻塞睡眠,而当缓冲区满时生产者阻塞睡眠。一旦缓冲区中有空单元,生产者进程就向空单元中入写字符,并报告写的内容和位置。一旦缓冲区中有未读过的字符,消费者进程就从该单元中读出字符,并报告读取位置。生产者不能向同一单元中连续写两次以上相同的字符,消费者也不能从同一单元中连续读两次以上相同的字符。

2.问题分析

首先看阶层,有两个,分别是生产者和消费者,他们之间的缓冲区是共享内存,首先想到一点:System V共享内存实现这一个缓冲区;又因为缓冲区是临界资源,所以要用一个互斥信号量实现;生产者和消费者要采用PV信号量操作实现进程同步

因为要求多个进程能同步,所以进程访问缓冲的指针也需要共享内存实现

大致框图:

生产者
消费者
生产者位置指针
消费者位置指针
共享内存

3.原语

进程:Producer - 生产者进程,Consumer - 消费者进程

共有的数据结构:

buffer: array [0…k-1] of integer;
  in,out: 0…k-1;

— in记录第一个空缓冲区,out记录第一个不空的缓冲区

prod_key(缓冲区空的个数),cons_key(缓冲区满的个数),mutex(临界区): semaphore;

— prod_key控制缓冲区不满,cons_key控制缓冲区不空,mutex保护临界区;

初始化prod_key=k,cons_key=0,mutex=1
  
  下面的部分为伪代码

producer(生产者进程):
Item_Type item;
{
    while (true)
    {
        produce(&item);  //生产数据
        p(prod_key);  //缓冲区满,阻塞,prod_key=k,p操作,将运行变为阻塞,申请空闲资源,信号量减一,成功退出,失败阻塞
        p(mutex);  //使用临界资源时,阻塞,mutex=1,临界资源互斥
        buffer[in]:=item;  //将item元素写入buffer,in记录写入元素位置
        in:=(in+1) mod k;
        v(mutex);  //v操作将阻塞变为运行,释放被占用的资源,若有被阻塞的资源,选一个运行
        v(cons_key);  //释放消费者进程资源
    }
}
consumer(消费者进程):
Item_Type item;
{
    while (true)
    {
        p(cons_key);  //cons_key=0,0-1=-1,阻塞,直到produce释放才能0+1,退出阻塞
        p(mutex);  //使用临界资源
        item:=buffer[out];  //将item元素读出buffer,out记录写出元素位置
        out:=(out+1) mod k;
        v(mutex);  //释放临界资源
        v(prod_key);  //释放生产者进程
        consume(&item);  //消费数据
    }
}
producer
申请空闲资源,失败阻塞
申请临界资源
向buffer写入元素
释放临界资源
释放消费者进程
consumer
申请空闲资源,失败阻塞
从buffer读出元素
释放生成者进程

​ 在这个实验中,缓冲区大小被设置为8,producer初始可以申请8个资源,comsumer初始设置为可申请0个,也就是,必须要produce先释放,comsumer,才可以申请,然后运行。

4.函数原型

自定义的函数

get_ipc_id
int get_ipc_id(char *proc_file,key_t key);  //从/proc/sysvipc/文件系统中获取IPC的id号

get_ipc_id() 从/proc/sysvipc/文件系统中获取IPC 的id 号,msg-消息队列,sem-信号量,shm-共享内存

  • pfile: 对应/proc/sysvipc/目录中的IPC 文件分别为

  • key: 对应要获取的IPC 的id 号的键值

  • 返回-1

set_sem()
int set_sem(key_t sem_key,int sem_val,int sem_flag);  //set_sem 函数建立一个信号量,初值设为sem_val

set_sem 函数建立一个具有n 个信号灯的信号量

如果建立成功,返回一个信号灯数组的标识符sem_id

输入参数:

  • sem_key 信号灯数组的键值
  • sem_val 信号灯数组中信号灯的个数
  • sem_flag 信号灯数组的存取权限
  • 返回id

实现过程:建立信号灯->设置信号灯初值->返回ID

set_shm()
char *set_shm(key_t shm_key,int shm_num,int shm_flag);  // set_shm 函数建立一个具有shm_num个字节的共享内存区

set_shm 函数建立一个具有n 个字节的共享内存区

如果建立成功,返回一个指向该内存区首地址的指针shm_buf

输入参数:

  • shm_key 共享内存的键值
  • shm_val 共享内存字节的长度
  • shm_flag 共享内存的存取权限

实现过程:创建共享内存->映射到进程的指针并返回

set_msq()
int set_msq(key_t msq_key,int msq_flag);  //set_msq 函数建立一个消息队列

set_msq 函数建立一个消息队列

如果建立成功,返回一个消息队列的标识符msq_id

  • 输入参数:
  • msq_key 消息队列的键值
  • msq_flag 消息队列的存取权限
  • 返回id

实现过程:创建消息队列->返回ID

P/V操作
int P_operation(int sem_id)

请求P操作

int V_operation(int sem_id)

释放V操作

信号灯上的P/V 操作,sem_num:信号灯数组下标,buf:操作信号灯的结构

  • sem_id:信号灯数组标识符

原有的函数

<sys/sem.h>

引自Linux进程间通信(五)

semget()
int semget(key_t key, int num_sems, int sem_flags);

创建一个新的信号量或取得一个已有的信号量。

  • 参数key是整数值(唯一非零),不相关的进程可以通过它访问同一个信号量,它代表程序可能要使用的某个资源,程序对所有信号量的访问都是间接的,程序先通过调用semget()函数并提供一个键,再由系统生成一个相应的信号标识符(semget()函数的返回值),只有semget()函数才直接使用信号量键,所有其他的信号量函数使用由semget()函数返回的信号量标识符。如果多个程序使用相同的key值,key将负责协调工作。

  • 参数num_sems指定需要的信号量数目,它的值几乎总是1。

  • 参数sem_flags是一组标志,当想要当信号量不存在时创建一个新的信号量,可以和值IPC_CREAT做按位或操作。设置了IPC_CREAT标志后,即使给出的键是一个已有信号量的键,也不会产生错误。而IPC_CREAT | IPC_EXCL则可以创建一个新的,唯一的信号量,如果信号量已存在,返回一个错误。

  • semget()函数成功返回一个相应信号标识符(非零),失败返回-1。

semop()
int semop(int sem_id, struct sembuf *sem_opa, size_t num_sem_ops);

用来改变信号量作用

  • 参数sem_id是semget()返回的信号量标识符。

  • 参数sembuf结构体定义如下。

struct sembuf{
	short sem_num;  // 信号索引:除非使用一组信号量,否则它为0,0代表第一个信号
	short sem_op;   // 操作类型:操作标志信号量在一次操作中需要改变的数据,通常是两个数,
                    // 一个是-1,即P(等待)操作,一个是+1,即V(发送信号)操作。
	short sem_flg;  // 操作标志:通常为SEM_UNDO,使操作系统跟踪信号,
                    // 并在进程没有释放该信号量而终止时,操作系统释放信号量
};
  • 参数 nsops 指出将要进行操作的信号的个数。

调用成功返回0,失败返回-1。

semctl
int semctl(int sem_id, int sem_num, int command, ...)

该函数用来直接控制信号量信息

  • 参数sem_id同上。

  • 参数sem_num同上中sembuf结构体中的sem_num。

  • command通常是下面两个值中的其中一个。

    SETVAL:用来把信号量初始化为一个已知的值。p 这个值通过union semun中的val成员设置,其作用是在信号量第一次使用前对它进行设置。

    IPC_RMID:用于删除一个已经无需继续使用的信号量标识符。

  • arg union semun结构体,详细见链接内容

  • 失败时 semctl() 返回 -1 并设置 errno 指明错误,否则该系统调用返回一个依赖于 cmd 的非负值。

<sys/shm.h>

引自Linux进程间通信(六)

shmget()
int shmget(key_t key, size_t size, int shmflg);

该函数用来创建共享内存。

  • 参数key,与信号量的semget函数一样,程序需要提供一个参数key(非0整数),它有效地为共享内存段命名,shmget()函数成功时返回一个与key相关的共享内存标识符(非负整数),用于后续的共享内存函数。调用失败返回-1。

    不相关的进程可以通过该函数的返回值访问同一共享内存,它代表程序可能要使用的某个资源,程序对所有共享内存的访问都是间接的,程序先通过调用shmget()函数并提供一个键,再由系统生成一个相应的共享内存标识符(shmget()函数的返回值),只有shmget()函数才直接使用信号量键,所有其他的信号量函数使用由semget函数返回的信号量标识符。

  • 参数size,size以字节为单位指定需要共享的内存容量。

  • 参数shmflg,shmflg是权限标志,它的作用与open函数的mode参数一样,如果要想在key标识的共享内存不存在时,创建它的话,可以与IPC_CREAT做或操作。共享内存的权限标志与文件的读写权限一样,举例来说,0644,它表示允许一个进程创建的共享内存被内存创建者所拥有的进程向共享内存读取和写入数据,同时其他用户创建的进程只能读取共享内存。

  • 成功返回共享存储的id,失败返回-1。

shmat()
void *shmat(int shm_id, const void *shm_addr, int shmflg);
  • 参数shm_id,shm_id是由shmget()函数返回的共享内存标识。

  • 参数shma_ddr,shm_addr指定共享内存连接到当前进程中的地址位置,通常为空,表示让系统来选择共享内存的地址。

  • 参数shmflg,shm_flg是一组标志位,通常为0。

  • 调用成功时返回一个指向共享内存第一个字节的指针,如果调用失败返回-1。

其他

shmdt()分离共享内存 shmctl()与semctl()一样,控制共享内存。本实验未使用,详见上链接。

<sys/msg.h>

引自消息队列函数

msgget()
int msgget(key_t key, int msgflg);

得到消息队列标识符或创建一个消息队列对象并返回消息队列标识符。

  • key:

    0(IPC_PRIVATE):会建立新的消息队列;

    大于0的32位整数:视参数msgflg来确定操作。通常要求此值来源于ftok返回的IPC键值。

  • msgflg:

    IPC_CREAT:当msgflg&IPC_CREAT为真时,如果内核中不存在键值与key相等的消息队列,则新建一个消息队列;如果存在这样的消息队列,返回此消息队列的标识符;IPC_CREAT|IPC_EXCL:如果内核中不存在键值与key相等的消息队列,则新建一个消息队列;如果存在这样的消息队列则报错。

  • 成功:返回消息队列的标识符,出错:-1,错误原因存于error中。

其他

msgctl()、msgsnd()、msgrcv()详见上链接。

5.IPC操作函数

我们编写一个函数实现对IPC 信息队列、共享内存、信号量的包装,以备接下来更好的编写程序

头文件ipc.h:

声明ipc操作函数和一些变量

/*Filename : ipc.h*/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/msg.h>

#define BUFSZ 256

//建立或获取ipc 的一组函数的原型说明
int get_ipc_id(char *proc_file,key_t key);  //从/proc/sysvipc/文件系统中获取IPC的id号
int set_sem(key_t sem_key,int sem_val,int sem_flag);  //set_sem函数建立一个初值为sem_val的信号量
char *set_shm(key_t shm_key,int shm_num,int shm_flag);  //set_shm函数建立一个具有shm_num个字节的共享内存区,shm_key为共享内存段
int set_msq(key_t msq_key,int msq_flag);  //set_msq 函数建立一个消息队列
int down(int sem_id);
int up(int sem_id);

typedef union semuns  //信号灯控制用的共同体
{
	int val;  //信号灯的个数
} Sem_uns;

typedef struct msgbuf  //消息结构体
{
	long mtype;  //消息类型
	char mtext[1];  //消息内容
} Msg_buf;

//生产消费者共享缓冲区即其有关的变量
key_t buff_key;  //IPC key标识
int buff_num;  //缓冲区长度
char *buff_ptr;  //指向缓冲区地址

//生产者放产品位置的共享指针
key_t pput_key;  //IPC key标识
int pput_num;  //指针个数1
int *pput_ptr;  //生产者放产品位置指针

//消费者取产品位置的共享指针
key_t cget_key;  //IPC key标识
int cget_num;  //指针个数1
int *cget_ptr;  //消费者取产品位置指针

//生产者有关的信号量
key_t prod_key;  //IPC key标识
int prod_sem;  //生产者同步信号量

//消费者有关的信号量
key_t cons_key;  //IPC key标识
int cons_sem;  //消费者同步信号量
int sem_val;  //信号灯个数
int sem_flg;  //使系统跟踪信号,常为SEM_UNDO,在进程未释放该信号量终止时,操作系统释放信号量
int shm_flg;  //读取权限,标志位

//互斥信号量
key_t mtx_key;  //IPC key标识
int mtx_sem;

ipc.c:

​ 这里包装了一些ipc的操作函数,包括信息队列、共享内存、信号量的创建/获得,及PV操作
  这里逐一分析它们的实现过程:(在注释中)

/*ipc.c*/
#include "ipc.h"
int get_ipc_id(char *proc_file,key_t key)  //get_ipc_id() 从/proc/sysvipc/文件系统中获取IPC的id号
{
	FILE *pf;
	int i,j;
	char line[BUFSZ],colum[BUFSZ];
	if((pf = fopen(proc_file,"r")) == NULL)
	{
		perror("Proc file not open");
		exit(EXIT_FAILURE);
	}
	fgets(line, BUFSZ,pf);  //从pf中读取长度为BUFSZ的数组放入line中
	while(!feof(pf))  //如果没有读到文件尾
	{
		i = j = 0;
		fgets(line, BUFSZ,pf);
        while(line[i] == ' ')
           	i++;
        while(line[i] !=' ')
            colum[j++] = line[i++];
        colum[j] = '\0';
        if(atoi(colum) != key)  //atoi()将字符串转为整数
       	    continue;
        j=0;
        while(line[i] == ' ')
       	    i++;
        while(line[i] !=' ')
            colum[j++] = line[i++];
        colum[j] = '\0';
        i = atoi(colum);
        fclose(pf);
        return i;
    }
    fclose(pf);
    return -1;
}

//请求P操作
int P_operation(int sem_id)  //信号灯数组标识符
{
    struct sembuf buf;  //信号灯结构
    buf.sem_op = -1;  //信号量在一次操作中需要改变的数据,-1即p操作等待
    buf.sem_num = 0;  //信号灯数组下标,除非使用一组信号量,否则为0
    buf.sem_flg = SEM_UNDO;  //通常都是undo,使系统跟踪信号
    if((semop(sem_id,&buf,1)) <0)  //调用semop,成功就使之等待,不成功显示错误
    {
        perror("down error ");
        exit(EXIT_FAILURE);
    }
    return EXIT_SUCCESS;
}

//释放 V操作
int V_operation(int sem_id)
{
    struct sembuf buf;  //信号灯结构
    buf.sem_op = 1;  //信号量在一次操作中需要改变的数据,+1即v操作发送信号
    buf.sem_num = 0;  //信号灯数组下标,除非使用1组,否则为0
    buf.sem_flg = SEM_UNDO;  //通常使undo,使系统跟踪信号
    if((semop(sem_id,&buf,1)) < 0)  //调用semop,成功释放占用,使之运行
    {
        perror("up error ");
        exit(EXIT_FAILURE);
    } 
    return EXIT_SUCCESS;
}

int set_sem(key_t sem_key,int sem_val,int sem_flg)  //set_sem 函数建立一个具有n个信号灯的信号量
{
    int sem_id;
    Sem_uns sem_arg;
    //测试由sem_key标识的信号灯数组是否已经建立
    if((sem_id = get_ipc_id("/proc/sysvipc/sem",sem_key)) < 0 )  //获取ipc的id
    {
        //如果没有获取成功,semget新建一个信号灯,其标号返回到sem_id,1代表信号量数目
        if((sem_id = semget(sem_key,1,sem_flg)) < 0)
        {
            perror("semaphore create error");
            exit(EXIT_FAILURE);
        }
        sem_arg.val = sem_val;  //设置信号灯的初值
        if(semctl(sem_id,0,SETVAL,sem_arg) <0)
        {
            perror("semaphore set error");
            exit(EXIT_FAILURE);
        }
    }
    return sem_id;  //返回ipc的id
}

char * set_shm(key_t shm_key,int shm_num,int shm_flg)  //set_shm 函数建立一个具有n个字节的共享内存区
{
    int i,shm_id;
    char * shm_buf;
    //测试由shm_key 标识的共享内存区是否已经建立
    if((shm_id = get_ipc_id("/proc/sysvipc/shm",shm_key)) < 0 )
    {
        //shmget 新建一个长度为shm_num 字节的共享内存,其标号返回到shm_id,权限为shm_flg
        if((shm_id = shmget(shm_key,shm_num,shm_flg)) <0)
        {
            perror("shareMemory set error");
            exit(EXIT_FAILURE);
        }
        //shmat 将由shm_id 标识的共享内存附加给指针shm_buf,该函数用来启用对该共享内存的访问
        if((shm_buf = (char *)shmat(shm_id,0,0)) < (char *)0)
        {
            perror("get shareMemory error");
            exit(EXIT_FAILURE);
        }
        for(i=0; i<shm_num; i++)  //共享内存区初始化
            shm_buf[i] = 0; //初始为0
    }
    //shm_key 标识的共享内存区已经建立,将由shm_id 标识的共享内存附加给指针shm_buf
    if((shm_buf = (char *)shmat(shm_id,0,0)) < (char *)0)
    {
        perror("get shareMemory error");
        exit(EXIT_FAILURE);
    }
    return shm_buf;
}

int set_msq(key_t msq_key,int msq_flg)  //set_msq 函数建立一个消息队列
{
    int msq_id;
    //测试由msq_key 标识的消息队列是否已经建立
    if((msq_id = get_ipc_id("/proc/sysvipc/msg",msq_key)) < 0 )
    {
        //msgget 新建一个消息队列,其标号返回到msq_id
        if((msq_id = msgget(msq_key,msq_flg)) < 0)
        {
            perror("messageQueue set error");
            exit(EXIT_FAILURE);
        }
    }
    return msq_id;
}

6.生产者程序实现

首先建立(已存在时为打开)一系列的信号量和共享内存,接着就按照操作原语去实现了,代码如下:

/*Filename : producer.c*/
#include "ipc.h"
int main(int argc,char *argv[])
{
	int rate;
	//可在在命令行第一参数指定一个进程睡眠秒数,以调解进程执行速度
	if(argv[1] != NULL)
		rate = atoi(argv[1]);
	else
		rate = 3;  //默认sleep(3)
	
    //共享内存使用的变量,其中键值任给,但是注意键值的
	//唯一性,在另外的文件中要用同一共享内存也要采用统一键值
	buff_key = 101;  //缓冲区任给的键值
	buff_num = 8;  //缓冲区任给的长度
	pput_key = 102;  //生产者放产品指针的键值
	pput_num = 1;  //指针数
	shm_flg = IPC_CREAT | 0644;  //共享内存读写权限,用户具有读写权限,组用户和其它用户具有只读权限
	//获取缓冲区使用的共享内存,buff_ptr 指向缓冲区首地址
	buff_ptr = (char *)set_shm(buff_key,buff_num,shm_flg);
	//获取生产者放产品位置指针pput_ptr
	pput_ptr = (int *)set_shm(pput_key,pput_num,shm_flg);
    
	//信号量使用的变量,键值与comesumer.c中相同
	prod_key = 201;  //生产者同步信号灯键值
	mtx_key = 202;  //互斥信号灯键值
	cons_key = 301;  //消费者同步信号灯键值
	sem_flg = IPC_CREAT | 0644;
    
	sem_val = buff_num;  //生产者同步信号灯初值设为缓冲区最大可用量8
	//获取生产者同步信号灯,引用标识存prod_sem
	prod_sem = set_sem(prod_key,sem_val,sem_flg);
	//消费者初始无产品可取,同步信号灯初值设为0
	sem_val = 0;
	//获取消费者同步信号灯,引用标识存cons_sem
	cons_sem = set_sem(cons_key,sem_val,sem_flg);
	//生产者互斥信号灯初值为1
	sem_val = 1;
	//获取互斥信号灯,引用标识存mtx_sem
	mtx_sem = set_sem(mtx_key,sem_val,sem_flg);

	while(1)  //循环执行模拟生产者不断放产品
	{
		//如果缓冲区满则生产者阻塞
		P_operation(prod_sem);
		//如果有进程进入,本生产者阻塞
		P_operation(mtx_sem);
		//用写一字符的形式模拟生产者放产品,报告本进程号和放入的字符及存放的位置
		buff_ptr[*pput_ptr] = 'A'+ *pput_ptr;
		//挂起rate秒
		sleep(rate);
		printf("%d producer put: %c to Buffer[%d]\n",getpid(),buff_ptr[*pput_ptr],*pput_ptr);
		//存放位置循环下移
		*pput_ptr = (*pput_ptr+1) % buff_num;
		//唤醒阻塞的进程
		V_operation(mtx_sem);
		//唤醒阻塞的消费者
		V_operation(cons_sem);
	}
	return EXIT_SUCCESS;
}

7.消费者程序实现

如同生产者上述,代码如下:

/* consumer.c*/
#include "ipc.h"
int main(int argc,char *argv[])
{
	int rate;
	//可在在命令行第一参数指定一个进程睡眠秒数,以调解进程执行速度
	if(argv[1] != NULL)
		rate = atoi(argv[1]);
	else
		rate = 3; //默认为3 秒
	
    //共享内存使用的变量
	buff_key = 101; //缓冲区任给的键值
	buff_num = 8; //缓冲区任给的长度
	cget_key = 103; //消费者取产品指针的键值
	cget_num = 1; //指针数
	shm_flg = IPC_CREAT | 0644; //共享内存读写权限
	//获取缓冲区使用的共享内存,buff_ptr 指向缓冲区首地址
	buff_ptr = (char *)set_shm(buff_key,buff_num,shm_flg);
	//获取消费者取产品指针,cget_ptr 指向索引地址
	cget_ptr = (int *)set_shm(cget_key,cget_num,shm_flg);
	
    //信号量使用的变量,键值与producer中相同
	prod_key = 201; //生产者同步信号灯键值
	mtx_key = 202; //互斥信号灯键值
	cons_key = 301; //消费者同步信号灯键值
	sem_flg = IPC_CREAT | 0644; //信号灯操作权限
    
	//生产者同步信号灯初值设为缓冲区最大可用量8
	sem_val = buff_num;
	//获取生产者同步信号灯,引用标识存prod_sem
	prod_sem = set_sem(prod_key,sem_val,sem_flg);
	//消费者初始无产品可取,同步信号灯初值设为0
	sem_val = 0;
	//获取消费者同步信号灯,引用标识存cons_sem
	cons_sem = set_sem(cons_key,sem_val,sem_flg);
	//消费者互斥信号灯初值为1
	sem_val = 1;
	//获取互斥信号灯,引用标识存mtx_sem
	mtx_sem = set_sem(mtx_key,sem_val,sem_flg);

	while(1)  //循环执行模拟消费者不断取产品
    {
		//如果无产品消费者阻塞
		P_operation(cons_sem);
		//如果有进程进入,本消费者阻塞
		P_operation(mtx_sem);
		//用读一字符的形式模拟消费者取产品,报告本进程号和获取的字符及读取的位置
		sleep(rate);
		printf("%d consumer get: %c fromBuffer[%d]\n",getpid(),buff_ptr[*cget_ptr],*cget_ptr);
		//读取位置循环下移
		*cget_ptr = (*cget_ptr+1) % buff_num;
		//唤醒阻塞的进程
		V_operation(mtx_sem);
		//唤醒阻塞的生产者
		V_operation(prod_sem);
	}
	return EXIT_SUCCESS;
}

8.编写Makefile

由于本项目工程有多个文件,所以需要Makefile来方便编译
  $emacs makefile

hdrs = ipc.h
opts = -g -c
c_src = consumer.c ipc.c
c_obj = consumer.o ipc.o
p_src = producer.c ipc.c
p_obj = producer.o ipc.o
all: producer consumer
consumer: $(c_obj)
	gcc $(c_obj) -o consumer
consumer.o: $(c_src) $(hdrs)
	gcc $(opts) $(c_src)
producer: $(p_obj)
	gcc $(p_obj) -o producer
producer.o: $(p_src) $(hdrs)
	gcc $(opts) $(p_src)
clean:
	rm consumer producer *.o

在这里插入图片描述

9.编译

$ make

运行时打开多个终端窗口,输入

$./producer 1

另一个窗口输入:

$./consumer 1

这时可以看到同步过程
  在这里插入图片描述

​ 这里的多个,不是指两个,可以多输入几个producer,consumer,时间也可以不一样,这样才是多个生产/消费者。

总结

这个实验,建立了ipc.h头文件,ipc.c,consumer.c,producer.c四个文件,然后用makefile进行编译。在编写过程中,用了共享内存,消息队列,还有信号,所以,就很复杂。
它的consumer和producer是分开进行的,也可以写多个fork(),将这几个程序合并为一个,一起运行。
小白大概就似懂非懂写了这么些(´。_。`)

补充

合并后的源代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/msg.h>

#define BUFSZ 256

typedef union semuns  //信号灯控制用的共同体
{
	int val;  //信号灯的个数
} Sem_uns;

typedef struct msgbuf  //消息结构体
{
	long mtype;  //消息类型
	char mtext[1];  //消息内容
} Msg_buf;

//生产消费者共享缓冲区即其有关的变量
key_t buff_key;  //IPC key标识
int buff_num;  //缓冲区长度
char *buff_ptr;  //指向缓冲区地址

//生产者放产品位置的共享指针
key_t pput_key;  //IPC key标识
int pput_num;  //指针个数1
int *pput_ptr;  //生产者放产品位置指针

//消费者取产品位置的共享指针
key_t cget_key;  //IPC key标识
int cget_num;  //指针个数1
int *cget_ptr;  //消费者取产品位置指针

//生产者有关的信号量
key_t prod_key;  //IPC key标识
int prod_sem;  //生产者同步信号量

//消费者有关的信号量
key_t cons_key;  //IPC key标识
int cons_sem;  //消费者同步信号量
int sem_val;  //信号灯个数
int sem_flg;  //使系统跟踪信号,常为SEM_UNDO,在进程未释放该信号量终止时,操作系统释放信号量
int shm_flg;  //读取权限,标志位

//互斥信号量
key_t mtx_key;  //IPC key标识
int mtx_sem;

int get_ipc_id(char *proc_file,key_t key)  //get_ipc_id() 从/proc/sysvipc/文件系统中获取IPC的id号
{
	FILE *pf;
	int i,j;
	char line[BUFSZ],colum[BUFSZ];
	if((pf = fopen(proc_file,"r")) == NULL)
	{
		perror("Proc file not open");
		exit(EXIT_FAILURE);
	}
	fgets(line, BUFSZ,pf);  //从pf中读取长度为BUFSZ的数组放入line中
	while(!feof(pf))  //如果没有读到文件尾
	{
		i = j = 0;
		fgets(line, BUFSZ,pf);
        while(line[i] == ' ')
           	i++;
        while(line[i] !=' ')
            colum[j++] = line[i++];
        colum[j] = '\0';
        if(atoi(colum) != key)  //atoi()将字符串转为整数
       	    continue;
        j=0;
        while(line[i] == ' ')
       	    i++;
        while(line[i] !=' ')
            colum[j++] = line[i++];
        colum[j] = '\0';
        i = atoi(colum);
        fclose(pf);
        return i;
    }
    fclose(pf);
    return -1;
}

//请求P操作
int P_operation(int sem_id)  //信号灯数组标识符
{
    struct sembuf buf;  //信号灯结构
    buf.sem_op = -1;  //信号量在一次操作中需要改变的数据,-1即p操作等待
    buf.sem_num = 0;  //信号灯数组下标,除非使用一组信号量,否则为0
    buf.sem_flg = SEM_UNDO;  //通常都是undo,使系统跟踪信号
    if((semop(sem_id,&buf,1)) <0)  //调用semop,成功就使之等待,不成功显示错误
    {
        perror("down error ");
        exit(EXIT_FAILURE);
    }
    return EXIT_SUCCESS;
}

//释放 V操作
int V_operation(int sem_id)
{
    struct sembuf buf;  //信号灯结构
    buf.sem_op = 1;  //信号量在一次操作中需要改变的数据,+1即v操作发送信号
    buf.sem_num = 0;  //信号灯数组下标,除非使用1组,否则为0
    buf.sem_flg = SEM_UNDO;  //通常使undo,使系统跟踪信号
    if((semop(sem_id,&buf,1)) < 0)  //调用semop,成功释放占用,使之运行
    {
        perror("up error ");
        exit(EXIT_FAILURE);
    } 
    return EXIT_SUCCESS;
}

int set_sem(key_t sem_key,int sem_val,int sem_flg)  //set_sem 函数建立一个具有n个信号灯的信号量
{
    int sem_id;
    Sem_uns sem_arg;
    //测试由sem_key标识的信号灯数组是否已经建立
    if((sem_id = get_ipc_id("/proc/sysvipc/sem",sem_key)) < 0 )  //获取ipc的id
    {
        //如果没有获取成功,semget新建一个信号灯,其标号返回到sem_id,1代表信号量数目
        if((sem_id = semget(sem_key,1,sem_flg)) < 0)
        {
            perror("semaphore create error");
            exit(EXIT_FAILURE);
        }
        sem_arg.val = sem_val;  //设置信号灯的初值
        if(semctl(sem_id,0,SETVAL,sem_arg) <0)
        {
            perror("semaphore set error");
            exit(EXIT_FAILURE);
        }
    }
    return sem_id;  //返回ipc的id
}

char * set_shm(key_t shm_key,int shm_num,int shm_flg)  //set_shm 函数建立一个具有n个字节的共享内存区
{
    int i,shm_id;
    char * shm_buf;
    //测试由shm_key 标识的共享内存区是否已经建立
    if((shm_id = get_ipc_id("/proc/sysvipc/shm",shm_key)) < 0 )
    {
        //shmget 新建一个长度为shm_num 字节的共享内存,其标号返回到shm_id,权限为shm_flg
        if((shm_id = shmget(shm_key,shm_num,shm_flg)) <0)
        {
            perror("shareMemory set error");
            exit(EXIT_FAILURE);
        }
        //shmat 将由shm_id 标识的共享内存附加给指针shm_buf,该函数用来启用对该共享内存的访问
        if((shm_buf = (char *)shmat(shm_id,0,0)) < (char *)0)
        {
            perror("get shareMemory error");
            exit(EXIT_FAILURE);
        }
        for(i=0; i<shm_num; i++)  //共享内存区初始化
            shm_buf[i] = 0; //初始为0
    }
    //shm_key 标识的共享内存区已经建立,将由shm_id 标识的共享内存附加给指针shm_buf
    if((shm_buf = (char *)shmat(shm_id,0,0)) < (char *)0)
    {
        perror("get shareMemory error");
        exit(EXIT_FAILURE);
    }
    return shm_buf;
}

int set_msq(key_t msq_key,int msq_flg)  //set_msq 函数建立一个消息队列
{
    int msq_id;
    //测试由msq_key 标识的消息队列是否已经建立
    if((msq_id = get_ipc_id("/proc/sysvipc/msg",msq_key)) < 0 )
    {
        //msgget 新建一个消息队列,其标号返回到msq_id
        if((msq_id = msgget(msq_key,msq_flg)) < 0)
        {
            perror("messageQueue set error");
            exit(EXIT_FAILURE);
        }
    }
    return msq_id;
}

int main()
{
  int rate = 3;
	pid_t fpid = fork();
       	fpid = fork();
    //共享内存使用的变量,其中键值任给,但是注意键值的
	//唯一性,在另外的文件中要用同一共享内存也要采用统一键值
	buff_key = 101;  //缓冲区任给的键值 key_t
	buff_num = 8;  //缓冲区任给的长度 int
	pput_key = 102;  //生产者放产品指针的键值 key_t
	pput_num = 1;  //指针数 int
    cget_key = 103; //消费者取产品指针的键值 key_t
	cget_num = 1; //指针数 int
	shm_flg = IPC_CREAT | 0644;  //int 共享内存读写权限,用户具有读写权限,组用户和其它用户具有只读权限
	//获取缓冲区使用的共享内存,buff_ptr 指向缓冲区首地址 char*
	buff_ptr = (char *)set_shm(buff_key,buff_num,shm_flg);
	//获取生产者放产品位置指针pput_ptr char*
	pput_ptr = (int *)set_shm(pput_key,pput_num,shm_flg);
	//获取消费者取产品指针,cget_ptr 指向索引地址 char*
	cget_ptr = (int *)set_shm(cget_key,cget_num,shm_flg);
    
	//信号量使用的变量,键值与comesumer.c中相同
	prod_key = 201;  //生产者同步信号灯键值 ket_t
	mtx_key = 202;  //互斥信号灯键值 ket_t
	cons_key = 301;  //消费者同步信号灯键值 key_t
	sem_flg = IPC_CREAT | 0644;  //int
	
	sem_val = buff_num;  //生产者同步信号灯初值设为缓冲区最大可用量8 int
	//获取生产者同步信号灯,引用标识存prod_sem int
	prod_sem = set_sem(prod_key,sem_val,sem_flg);
	//消费者初始无产品可取,同步信号灯初值设为0
	sem_val = 0;
	//获取消费者同步信号灯,引用标识存cons_sem int
	cons_sem = set_sem(cons_key,sem_val,sem_flg);
	//生产者互斥信号灯初值为1
	sem_val = 1;
	//获取互斥信号灯,引用标识存mtx_sem i
	mtx_sem = set_sem(mtx_key,sem_val,sem_flg);
	
	if(fpid != 0)
	{
	    while(1)  //循环执行模拟生产者不断放产品
	    {
	        //如果缓冲区满则生产者阻塞
	        P_operation(prod_sem);
	        //如果有进程进入,本生产者阻塞
	        P_operation(mtx_sem);
	        //用写一字符的形式模拟生产者放产品,报告本进程号和放入的字符及存放的位置
	        buff_ptr[*pput_ptr] = 'A'+ *pput_ptr;
	        //挂起rate秒
	        sleep(rate-1);
	        printf("%d producer put: %c to Buffer[%d]\n",getpid(),buff_ptr[*pput_ptr],*pput_ptr);
	        //存放位置循环下移
	        *pput_ptr = (*pput_ptr+1) % buff_num;
	        //唤醒阻塞的进程
	        V_operation(mtx_sem);
	        //唤醒阻塞的消费者
	        V_operation(cons_sem);
	    }
	}else
	{
	    while(1)  //循环执行模拟消费者不断取产品
	    {
	        //如果无产品消费者阻塞
	        P_operation(cons_sem);
	        //如果有进程进入,本消费者阻塞
	        P_operation(mtx_sem);
	        //用读一字符的形式模拟消费者取产品,报告本进程号和获取的字符及读取的位置
	        //sleep(rate);
	        printf("%d consumer get: %c fromBuffer[%d]\n",getpid(),buff_ptr[*cget_ptr],*cget_ptr);
	        //读取位置循环下移
	        *cget_ptr = (*cget_ptr+1) % buff_num;
	        //唤醒阻塞的进程
	        V_operation(mtx_sem);
	        //唤醒阻塞的生产者
	        V_operation(prod_sem);
	    }
	}
	return EXIT_SUCCESS;
}
循环
循环
缓冲区初始化
生产者
消费者

根据代码重置流程图

设置生产互斥消费者信号量
设置键值
建立共享内存区
消费者初始化
生产者初始化
缓冲区初始化
prod/mtx/cons
生产201互斥202消费301
buff/pput/cget_ptr
键值102 个数1
键值102 个数1
键值101 长度8
初始化完成
生产者开始
P生产者互斥信号
运行操作
V互斥消费者信号
结束
消费者开始
P消费者互斥信号
运行操作
V互斥生产者信号
结束

关于源代码中的一点问题

仔细分析运行的流程图后发现似乎程序有些内容并不重要

  1. 头文件ipc.h中,”int down(int sem_id);” 与 “int up(int sem_id);”这两个函数在这个实验中并没有用到。而ipc.c中的”int P_operation(int sem_id)“ 与 ”int V_operation(int sem_id)“并没有被声明,所以此处的两个函数声明因换为PV操作的函数声明;
  2. <sys/msg.h>,事实上,这份代码只在ipc.h与ipc.c中写了关于msg的内容,实验过程中,完全没有用上这部分内容,可以删去所有相关内容;
  3. int get_ipc_id(char * proc_file,key_t key) ;这个函数是为了检查原本是否有sem_id,检查信号量是否建立,如果没有,就调用 semget()shmget(),但事实上这两个函数本身就会根据键值去查找是否已经建立,如果建立就返回已建立的信息,没有就建立,所以这个函数也可以删去<sys/ipc.h>也可以直接删掉;
  4. 这个源程序反复运行,后,单独运行producer或consumer可能有bug,嗯,小白搞不明白。
  5. (这之后都是新加的)补充上一点,后来发现sem_id在系统文件中未删除,也就是上一次的运行数据仍然保留,第一次运行后再次运行都是基于之前的结果运行,不知道为什么会出现bug
  6. 为了解决这个问题,就在最终代码删除了这三个的sem_id,之后每一次运行这个程序都会重新创建来避免出现bug
  7. 这个代码原本,往共享区放东西,然后生产者取,取了之后只是读取而已,也就是说,只要共享区被放过东西,那么生产者就可以无限的从中读出数据。大概就是这样……,也正式,第四点,运行过一次后,可以单独运行consumer.c,还能取出东西的原因。

最终的精简代码

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/sem.h>
#include <sys/shm.h>
#include <sys/types.h>

int set_sem(key_t sem_key,int sem_val,int sem_flg)  //set_sem函数建立一个具初值为semval信号量
{
    int sem_id;
    int sem_arg;
    if((sem_id = semget(sem_key,1,sem_flg)) < 0)
    {  //创建一个新的信号量或取得一个原有的信号量
        perror("semaphore create error");
        exit(1);
    }
    if(semctl(sem_id,0,SETVAL,sem_val) <0)
    {  //初始化信号量
        perror("semaphore set error");
        exit(1);
    }
    return sem_id;  //返回ipc的id
}

char * set_shm(key_t shm_key,int shm_num,int shm_flg)  //set_shm函数建立一个具有n个字节的共享内存区
{
    int i,shm_id;
    char * shm_buf;
    //shmget 新建一个长度为shm_num字节的共享内存,其标号返回到shm_id,权限为shm_flg
    if((shm_id = shmget(shm_key,shm_num,shm_flg)) <0)
    {
        perror("shareMemory set error");
        exit(1);
    }
    //shmat 将由shm_id标识的共享内存附加给指针shm_buf,该函数用来启用对该共享内存的访问
    if((shm_buf = (char *)shmat(shm_id,0,0)) < (char *)0)
    {
        perror("get shareMemory error");
        exit(1);
    }
    for(i=0; i<shm_num; i++)  //共享内存区初始化
        shm_buf[i] = 0; //初始为0
    return shm_buf;
}

//请求P操作
int P_operation(int sem_id)  //信号灯数组标识符
{
    struct sembuf buf={0,-1,SEM_UNDO};  //信号灯结构,0信号量编号,-1 p操作
    if((semop(sem_id,&buf,1)) <0)  //调用semop,成功就使之等待,不成功显示错误
    {
        perror("请求失败");
        exit(1);
    }
    return 0;
}

//释放 V操作
int V_operation(int sem_id)
{
    struct sembuf buf={0,1,SEM_UNDO};  //信号灯结构
    if((semop(sem_id,&buf,1)) < 0)  //调用semop,成功释放占用,使之运行
    {
        perror("释放失败");
        exit(1);
    }
    return 0;
}

int main()
{
    int rate = 3;
    pid_t fpid = fork();
    fpid = fork();  //两个fork,模拟多生产者消费者
    //共享内存使用的变量,其中键值任给
    key_t buff_key = 104;  //缓冲区任给的键值
    int buff_num = 8;  //缓冲区任给的长度
    key_t pput_key = 105;  //生产者放产品指针的键值
    int pput_num = 1;  //指针数
    key_t cget_key = 106; //消费者取产品指针的键值
    int cget_num = 1; //指针数
    int shm_flg = IPC_CREAT | 0644;  //共享内存读写权限,用户具有读写权限,组用户和其它用户具有只读权限
    char* buff_ptr = (char *)set_shm(buff_key,buff_num,shm_flg);  //获取缓冲区使用的共享内存,buff_ptr 指向缓冲区首地址 char*
    int* pput_ptr = (int *)set_shm(pput_key,pput_num,shm_flg);  //获取生产者放产品位置指针
    int* cget_ptr = (int *)set_shm(cget_key,cget_num,shm_flg);  //获取消费者取产品指针

    key_t prod_key = 205;  //生产者同步信号灯键值
    key_t mtx_key = 206;  //互斥信号灯键值
    key_t cons_key = 207;  //消费者同步信号灯键值
    int sem_flg = IPC_CREAT | 0644;  //权限

    int prod_sem = set_sem(prod_key,buff_num,sem_flg);  //得到信号量id
    int cons_sem = set_sem(cons_key,0,sem_flg);  //得到信号量id
    int mtx_sem = set_sem(mtx_key,1,sem_flg);  //得到信号量id
    if (fpid != 0)  //父进程生产
    {
        for (int i = 0; i < 20; i++)  //循环执行模拟生产者不断放产品
        {
            int s = rand()%2+1;  //随机睡眠一段时间
            P_operation(prod_sem); //如果缓冲区满则生产者阻塞
            P_operation(mtx_sem);  //如果有进程进入,本生产者阻塞
            sleep(s);     //挂起s秒
            buff_ptr[*pput_ptr] = 65 + (*pput_ptr);  //用写一字符的形式模拟生产者放产品,报告本进程号和放入的字符及存放的位置,原本代码这个操作执行完后就是在白做,放了又放一遍
            printf("%d producer put: %c to Buffer[%d]|\n",getpid(),buff_ptr[*pput_ptr],*pput_ptr);
            *pput_ptr = (*pput_ptr+1) % buff_num;  //存放位置循环下移
            V_operation(mtx_sem);  //唤醒阻塞的进程
            V_operation(cons_sem);  //唤醒阻塞的消费者
        }
    }
    else  //子进程消费
    {
        for (int i = 0; i < 20; i++) //循环执行模拟消费者不断取产品
        {
            int s = rand()%2+1;  //随机睡眠一段时间
            P_operation(cons_sem);  //如果无产品消费者阻塞
            P_operation(mtx_sem);  //如果有进程进入,本消费者阻塞s
            sleep(s); //挂起s秒,用读一字符的形式模拟消费者取产品,报告本进程号和获取的字符及读取的位置
            printf("                                 |");
            printf("%d consumer get: %c fromBuffer[%d]\n", getpid(),buff_ptr[*cget_ptr], *cget_ptr);
            buff_ptr[*cget_ptr] = 0;  //该物品被取走,原本代码没有取走操作!!
            *cget_ptr = (*cget_ptr+1) % buff_num;  //读取位置循环下移
            V_operation(mtx_sem);  //唤醒阻塞的进程
            V_operation(prod_sem);  //唤醒阻塞的生产者
        }
    }
    semctl(cons_sem, 0, IPC_RMID);
    semctl(prod_sem, 0, IPC_RMID);
    semctl(mtx_sem, 0, IPC_RMID);
    return 0;
}

运行结果是这样的
左边生产者,右边消费者
这个代码的fork了两次,两个生产者,两个消费者,谁先到最后,会删除sem_id,然后后面的进程会因为没有id了就停止了。
然后,我亲爱的好证明老师问sleep如果去掉,运行结果是否会相同,我实测是有部分差别的,但是得到的回答是应该相同的。呃,因为互斥的话,似乎,确实该相同,但是,为什么最后运行结果又不同。欸,对,sleep的初衷是为了我能在运行的时候看清咋运行的,随机的睡眠时间则是因为我起初也认为睡眠时间不同会使结果不同。

	头大,不懂,岂可修!/(ㄒoㄒ)/~~
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值