项目概述
在Linux系统中利用进程间通信(interprocess communication)IPC中的3个对象:共享内存、信号量数组、消息队列,来解决协作并发进程间的同步与互斥问题。本实验需要实现抽烟者问题:假设⼀个系统中有三个抽烟者进程,每个抽烟者不断地卷烟并抽 烟。抽烟者卷起并抽掉⼀颗烟需要有三种材料:烟草、纸和胶⽔。⼀个抽烟者有烟草,⼀个有纸,另⼀个有胶⽔。系统中还有两个供应者进程,它们⽆限地供应所有三种材料,但每次仅轮流提供三种材料中的两种。得到缺失的两种材料的抽烟者在卷起并抽掉⼀颗烟后会发信号通知供应者,让它继续提供另外的两种材料。这⼀过程重复进⾏。 **⽤IPC 同步机制编程实现该问题要求的功能。**设计思路
①关系分析:供应者与三个抽烟者分别是同步关系。由于抽烟者无法同时满足两个或以上的抽烟者,三个抽烟者对抽烟这个动作互斥(或由三个抽烟者轮流抽烟得知)。 ②整体思路:一共设计六个文件,ipc.h个ipc.c设计信号量机制实现的底层逻辑,例如消息队列、P操作、V操作等。设计四个进程,供应者作为生产者向三个抽烟者提供材料。 ③信号量设置:信号量offer1、offer2、offer3分别表示烟草和纸组合,烟草和胶水组合,胶水和纸组合。信号量finish表示抽烟完成信号。它们之间的协同关系如下图:同时还要设置一个互斥信号量,保证4个进程互斥访问缓冲区,对应的代码结构为:
该互斥信号量不是必须的,因为本例中缓冲区大小只为1。
生产者的主要代码为:
由于finish信号量设置初值为0,因此此时down(finish)操作应放在while循环末尾。
整个程序的设计逻辑参照如下图:
生产者:首先如果缓冲区满则生产者阻塞,其次生产者不能同时进行提供材料,所以要设置互斥锁使其不能同时进行执行。生产者每次提供其中的两种后,唤醒抽烟者。
抽烟者:而对于抽烟者来说,刚开始并没有所需要的材料,要等待生产者的唤醒,同时还要设置互斥锁防止抽烟者同时进行吸烟。判断当前放的两个材料是否是其所需,是则从共享缓冲区间读取所需要的数据,然后唤起两个生产者和其他抽烟者。若不是,则唤醒其他抽烟者抽烟。
代码
一共5个文件,各文件功能已注释清除,运行方法和主要问题赋于文末// ipc.h
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/ipc.h>
#include <sys/shm.h>
#include <sys/sem.h>
#include <sys/msg.h>
#define BUFSZ 256
//建立或获取 ipc 的一组函数的原型说明
int get_ipc_id(char *proc_file,key_t key);
char *set_shm(key_t shm_key,int shm_num,int shm_flag);
int set_msq(key_t msq_key,int msq_flag);
int set_sem(key_t sem_key,int sem_val,int sem_flag);
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;
int buff_num;
char *buff_ptr;
//生产者放产品位置的共享指针
key_t pput_key;
int pput_num;
int *pput_ptr;
//消费者取产品位置的共享指针
key_t cget_key;
int cget_num;
int *cget_ptr;
//生产者和消费者有关的信号量
key_t offer1_key;
key_t offer2_key;
key_t offer3_key;
key_t finish_key;
key_t mutex_key;
int offer1;
int offer2;
int offer3;
int finish;
int mutex;
int sem_val;
int sem_flg;
int shm_flg;
// ipc.c
#include "ipc.h"
// 从/proc/sysvipc/文件系统中获取IPC中3个对象的id号
// key是要获取的对象的id号
int get_ipc_id(char *proc_file, key_t key)
{
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);
}
// 从文件中读取一行并储存在line[]中
fgets(line, BUFSZ, pf);
while(!feof(pf))
{
i = j = 0;
fgets(line, BUFSZ,pf);
while(line[i] == ' ')
i++;
while(line[i] != ' ')
colum[j++] = line[i++];
colum[j] = '\0';
// 找不到对应的key,就继续读
if(atoi(colum) != key)
continue;
// 找到了对应的key,读出紧挨着的id
j = 0;
while(line[i] == ' ')
i++;
while(line[i] !=' ')
colum[j++] = line[i++];
colum[j] = '\0';
i = atoi(colum);
fclose(pf);
// 返回id值
return i;
}
fclose(pf);
return -1;
}
// 定义信号量sem_id上的down操作(P操作)
int down(int sem_id)
{
struct sembuf buf;
// 对索引值为0的信号量定义P操作
buf.sem_num = 0;
// 调用sem_op后,信号量的值-1
buf.sem_op = -1;
// 若P操作后信号量<=0,进程等待
buf.sem_flg = SEM_UNDO;
// 系统调用执行定义的P操作
if((semop(sem_id,&buf,1)) < 0)
{
perror("P error ");
exit(EXIT_FAILURE);
}
return EXIT_SUCCESS;
}
// 定义信号量sem_id上的up操作(V操作)
int up(int sem_id)
{
struct sembuf buf;
buf.sem_op = 1;
buf.sem_num = 0;
buf.sem_flg = SEM_UNDO;
if((semop(sem_id,&buf,1)) < 0)
{
perror("V error ");
exit(EXIT_FAILURE);
}
return EXIT_SUCCESS;
}
// 把sem_key对应的信号量赋值为sem_val,权限位为sem_flag
int set_sem(key_t sem_key, int sem_val, int sem_flg)
{
int sem_id;
// 保存信号量状态的结构体
Sem_uns sem_arg;
// 测试由 sem_key 标识的信号量数组是否已经建立
if((sem_id = get_ipc_id("/proc/sysvipc/sem", sem_key)) < 0 )
{
// 新建1个信号量,键值为sem_key,权限位为sem_flag,标号返回到sem_id
if((sem_id = semget(sem_key, 1, sem_flg)) < 0)
{
perror("semaphore create error");
exit(EXIT_FAILURE);
}
// 设置信号量的初值
sem_arg.val = sem_val;
// 在sem_id标识的信号量数组的第0个信号量上执行SETVAL操作,arg保存信号量状态的联合体
// SETVAL为设置信号量集中的一个单独的信号量的值
if(semctl(sem_id, 0, SETVAL, sem_arg) < 0)
{
perror("semaphore set error");
exit(EXIT_FAILURE);
}
}
return sem_id;
}
// 把shm_key对应的缓冲区开辟空间shm_num,权限位为shm_flag
char* set_shm(key_t shm_key, int shm_num, int shm_flg)
{
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
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);
}
// 初始化内存为 0
for(i = 0; i < shm_num; i++)
shm_buf[i] = 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)
{
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;
}
// producer.c
#include "ipc.h"
#include<stdlib.h>
int main(int argc,char *argv[])
{
int rate;
// 可在在命令行第一参数指定一个进程睡眠秒数,以调解进程执行速度,不指定则为1秒
if(argv[1] != NULL)
rate = atoi(argv[1]);
else
rate = 1;
// 共享内存使用的变量
buff_key = 101; // 缓冲区键值
buff_num = 1; // 缓冲区长度
pput_key = 102; // 生产者放产品指针的键值
pput_num = 1; // 指针数
// 0644 = 110100100,生产者可读可写,抽烟者只可读
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);
// 信号量键值设置
offer1_key = 201;
offer2_key = 202;
offer3_key = 203;
finish_key = 204;
mutex_key = 205;
// 访问权限设置
sem_flg = IPC_CREAT | 0644;
// 同步信号量设置
sem_val = 0;
offer1 = set_sem(offer1_key, sem_val, sem_flg);
offer2 = set_sem(offer2_key, sem_val, sem_flg);
offer3 = set_sem(offer3_key, sem_val, sem_flg);
finish = set_sem(finish_key, sem_val, sem_flg);
// 互斥信号量设置
sem_val = 1;
mutex = set_sem(mutex_key, sem_val, sem_flg);
int i = 0;
while(1)
{
i = (i + 1) % 3;
buff_ptr[*pput_ptr] = i + 1;
sleep(rate);
down(mutex); // 互斥访问缓冲区
*pput_ptr = (*pput_ptr + 1) % buff_num;
if(i == 0) {
printf("%d put offer1 into buffer[%d]\n", getpid(), *pput_ptr);
up(offer1); // 将组合1放到桌上
}
else if(i == 1) {
printf("%d put offer2 into buffer[%d]\n", getpid(), *pput_ptr);
up(offer2); // 将组合2放到桌上
}
else {
printf("%d put offer3 into buffer[%d]\n", getpid(), *pput_ptr);
up(offer3); // 将组合3放到桌上
}
up(mutex); // 互斥访问缓冲区
down(finish); // 前V后P
}
return EXIT_SUCCESS;
}
// consumer_A.c
#include "ipc.h"
int main(int argc,char *argv[])
{
int rate;
if(argv[1] != NULL)
rate = atoi(argv[1]);
else
rate = 1;
//共享内存 使用的变量
buff_key = 101; // 缓冲区键值
buff_num = 1; // 缓冲区长度
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);
// 信号量键值设置
offer1_key = 201;
offer2_key = 202;
offer3_key = 203;
finish_key = 204;
mutex_key = 205;
// 访问权限设置
sem_flg = IPC_CREAT | 0644;
// 同步信号量设置
sem_val = 0;
offer1 = set_sem(offer1_key, sem_val, sem_flg);
finish = set_sem(finish_key, sem_val, sem_flg);
// 互斥信号量设置
sem_val = 1;
mutex = set_sem(mutex_key, sem_val, sem_flg);
while(1)
{
sleep(rate);
down(offer1);
down(mutex);
*cget_ptr = (*cget_ptr + 1) % buff_num;
printf("%d smoker get offer%d from buffer[%d]\n", getpid(), buff_ptr[*cget_ptr], *cget_ptr);
up(mutex);
up(finish);
}
return EXIT_SUCCESS;
}
// consumer_B.c
#include "ipc.h"
int main(int argc,char *argv[])
{
int rate;
// 可在在命令行第一参数指定一个进程睡眠秒数,以调解进程执行速度
if(argv[1] != NULL)
rate = atoi(argv[1]);
else
rate = 1; // 不指定为 1 秒
//共享内存 使用的变量
buff_key = 101; // 缓冲区键值
buff_num = 1; // 缓冲区长度
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);
// 信号量键值设置
offer1_key = 201;
offer2_key = 202;
offer3_key = 203;
finish_key = 204;
mutex_key = 205;
// 访问权限设置
sem_flg = IPC_CREAT | 0644;
// 同步信号量设置
sem_val = 0;
offer2 = set_sem(offer2_key, sem_val, sem_flg);
finish = set_sem(finish_key, sem_val, sem_flg);
// 互斥信号量设置
sem_val = 1;
mutex = set_sem(mutex_key, sem_val, sem_flg);
while(1)
{
sleep(rate);
down(offer2);
down(mutex);
*cget_ptr = (*cget_ptr + 1) % buff_num;
printf("%d smoker get offer%d from buffer[%d]\n", getpid(), buff_ptr[*cget_ptr], *cget_ptr);
up(mutex);
up(finish);
}
return EXIT_SUCCESS;
}
// consumer_C.c
#include "ipc.h"
int main(int argc,char *argv[])
{
int rate;
// 可在在命令行第一参数指定一个进程睡眠秒数,以调解进程执行速度
if(argv[1] != NULL)
rate = atoi(argv[1]);
else
rate = 1; // 不指定为 1 秒
//共享内存 使用的变量
buff_key = 101; // 缓冲区键值
buff_num = 1; // 缓冲区长度
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);
// 信号量键值设置
offer1_key = 201;
offer2_key = 202;
offer3_key = 203;
finish_key = 204;
mutex_key = 205;
// 访问权限设置
sem_flg = IPC_CREAT | 0644;
// 同步信号量设置
sem_val = 0;
offer2 = set_sem(offer2_key, sem_val, sem_flg);
finish = set_sem(finish_key, sem_val, sem_flg);
// 互斥信号量设置
sem_val = 1;
mutex = set_sem(mutex_key, sem_val, sem_flg);
while(1)
{
sleep(rate);
down(offer2);
down(mutex);
*cget_ptr = (*cget_ptr + 1) % buff_num;
printf("%d smoker get offer%d from buffer[%d]\n", getpid(), buff_ptr[*cget_ptr], *cget_ptr);
up(mutex);
up(finish);
}
return EXIT_SUCCESS;
}
// makefile
hdrs = ipc.h
opts = -g -c
p_src = producer.c ipc.c
p_obj = producer.o ipc.o
a_src = consumer_A.c ipc.c
a_obj = consumer_A.o ipc.o
b_src = consumer_B.c ipc.c
b_obj = consumer_B.o ipc.o
c_src = consumer_C.c ipc.c
c_obj = consumer_C.c ipc.o
all: consumer_A consumer_B consumer_C producer
consumer_A: $(a_obj)
gcc $(a_obj) -o consumer_A
consumer_A.o: $(a_src) $(hdrs)
gcc $(opts) $(a_src) -lrt
consumer_B: $(b_obj)
gcc $(b_obj) -o consumer_B
consumer_B.o: $(b_src) $(hdrs)
gcc $(opts) $(b_src) -lrt
consumer_C: $(c_obj)
gcc $(c_obj) -o consumer_C
consumer_C.o: $(c_src) $(hdrs)
gcc $(opts) $(c_src) -lrt
producer: $(p_obj)
gcc $(p_obj) -o producer
producer.o: $(p_src) $(hdrs)
gcc $(opts) $(p_src) -lrt
clean:
rm consumer_A consumer_B consumer_C producer *.o
运行方法:
①make文件夹
②在shell1运行./producer
③在shell2运行./consumer_A
④在shell2运行./consumer_B
⑤在shell2运行./consumer_C
运行截图:
主要问题(超级坑)
①在ipc.c里定义对信号量的加减函数分别为int V(int sem_id) 和 int P(int sem_id),但是操作系统中好像有这两个函数的定义,所以在其他文件调用P函数和V函数时无法对信号量执行加减操作,一开始我的producer.c一直运行,我调试了半天,使用函数semget()查看信号量的值,发现并没有改变后,把P函数和V函数的名字改为down和up,终于执行成功; ②由于该程序会修改文件/proc/sysvipc/sem中的记录,且运行完后不会删除,因此如果多次对文件进行修改,并make后执行,可能使用到一个错误的信号量id,对该信号量的操作不会反映到程序中,程序会直接跳过对信号量的P操作和V操作。解决方法是重启虚拟机,这点也困扰了我很久。 ③1.对于ipc.c中中函数int get_ipc_id(char *proc_file, key_t key) 的分析:该函数根据传入的key在系统文件/proc/sysvipc中寻找对应的id值,文件中key的存储情况如下:
根据传入的key找到相应项后,打开它,例如传入的key == msg.key,msg的项如下存储:
另一个问题:
finish同步信号量的设置可以为1或0,如果是1,则应该在while循环开始处调用P操作,这样第一遍循环可以顺利执行,阻塞在第二次循坏的开始处;如果是0,则阻塞在while循坏的尾部。