实验四:进程同步实验
1.实验目的
加深对并发协作进程同步与互斥概念的理解,观察和体验并发进程同步与互斥 操作的效果,分析与研究经典进程同步与互斥问题的实际解决方案。了解Linux系 统中IPC进程同步工具的用法,练习并发协作进程的同步与互斥操作的编程与调试技术
2.示例实验
以下示例实验程序应能模拟多个生产/消费者在有界缓冲上正确的操作。它利 用N个字节的共享内存作为有界循环缓冲区,利用写一字符模拟放一个产品,利用 读一字符模拟消费一个产品。当缓冲区空时消费者应阻塞睡眠,而当缓冲区满时生 产者应当阻塞睡眠。一旦缓冲区中有空单元,生产者进程就向空单元中入写字符, 并报告写的内容和位置。一旦缓冲区中有未读过的字符,消费者进程就从该单元中 读出字符,并报告读取位置。生产者不能向同一单元中连续写两次以上相同的字 符,消费者也不能从同一单元中连续读两次以上相同的字符。
- 在当前新建文件夹中建立以下名为ipc.h的C程序的头文件,该文件中定义了生产者/消费者共用的IPC函数的原型和变量:
#pragma once
/*
* Filename : ipc.h
* copyright : (C) 2006 by zhonghonglie
* Function : 声明IPC机制的函数原型和全局变量
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.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;
//生产消费者共享缓冲区即其有关的变量
extern key_t buff_key;
extern int buff_num;
extern char* buff_ptr;
//生产者放产品位置的共享指针
extern key_t pput_key;
extern int pput_num;
extern int* pput_ptr;
//消费者取产品位置的共享指针
extern key_t cget_key;
extern int cget_num;
extern int* cget_ptr;
//生产者有关的信号量
extern key_t prod_key;
extern key_t pmtx_key;
extern int prod_sem;
extern int pmtx_sem;
//消费者有关的信号量
extern key_t cons_key;
extern key_t cmtx_key;
extern int cons_sem;
extern int cmtx_sem;
extern int sem_val;
extern int sem_flg;
extern int shm_flg;
2.在当前新建文件夹中建立以下名为ipc.c的C程序,该程序中定义了生产者/消费者共用的IPC函数
/*
* Filename : ipc.c
* copyright : (C) 2006 by zhonghonglie
* Function : 一组建立IPC机制的函数
*/
#include "ipc.h"
/*
* get_ipc_id() 从/proc/sysvipc/文件系统中获取IPC的id号
* pfile: 对应/proc/sysvipc/目录中的IPC文件分别为
* msg-消息队列,sem-信号量,shm-共享内存
* key: 对应要获取的IPC的id号的键值
*/
//生产消费者共享缓冲区即其有关的变量
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 prod_key;
key_t pmtx_key;
int prod_sem;
int pmtx_sem;
//消费者有关的信号量
key_t cons_key;
key_t cmtx_key;
int cons_sem;
int cmtx_sem;
int sem_val;
int sem_flg;
int shm_flg;
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);
}
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';
if (atoi(colum) != key) 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;
}
/*
* 信号灯上的down/up操作
* semid:信号灯数组标识符
* semnum:信号灯数组下标
* buf:操作信号灯的结构
*/
int down(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("down error ");
exit(EXIT_FAILURE);
}
return EXIT_SUCCESS;
}
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("up error ");
exit(EXIT_FAILURE);
}
return EXIT_SUCCESS;
}
/*
* set_sem函数建立一个具有n个信号灯的信号量
* 如果建立成功,返回 一个信号灯数组的标识符sem_id
* 输入参数:
* 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)
{
//semget新建一个信号灯,其标号返回到sem_id
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;
}
/*
* set_shm函数建立一个具有n个字节 的共享内存区
* 如果建立成功,返回 一个指向该内存区首地址的指针shm_buf
* 输入参数:
* shm_key 共享内存的键值
* shm_val 共享内存字节的长度
* 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);
}
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;
}
/*
* set_msq函数建立一个消息队列
* 如果建立成功,返回 一个消息队列的标识符msq_id
* 输入参数:
* msq_key 消息队列的键值
* msq_flag 消息队列的存取权限
*/
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;
}
3.在当前新文件夹中建立生产者程序producer.c
/*
* Filename : producer.c
* copyright : (C) 2006 by zhonghonglie
* Function : 建立并模拟生产者进程
*/
#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;//缓冲区任给的长度
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);
//信号量使用的变量
prod_key = 201;//生产者同步信号灯键值
pmtx_key = 202;//生产者互斥信号灯键值
cons_key = 301;//消费者同步信号灯键值
cmtx_key = 302;//消费者互斥信号灯键值
sem_flg = IPC_CREAT | 0644;
//生产者同步信号灯初值设为缓冲区最大可用量
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;
//获取生产者互斥信号灯,引用标识存pmtx_sem
pmtx_sem = set_sem(pmtx_key, sem_val, sem_flg);
//循环执行模拟生产者不断放产品
while (1) {
//如果缓冲区满则生产者阻塞
down(prod_sem);
//如果另一生产者正在放产品,本生产者阻塞
down(pmtx_sem);
//用写一字符的形式模拟生产者放产品,报告本进程号和放入的字符及存放的位置
buff_ptr[*pput_ptr] = 'A' + *pput_ptr;
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;
//唤醒阻塞的生产者
up(pmtx_sem);
//唤醒阻塞的消费者
up(cons_sem);
}
return EXIT_SUCCESS;
}
4.在当前新文件夹中建立消费者程序consumer.c
/*
Filename : consumer.c
copyright : (C) by zhanghonglie
Function : 建立并模拟消费者进程
*/
#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);
//信号量使用的变量
prod_key = 201; //生产者同步信号灯键值
pmtx_key = 202; //生产者互斥信号灯键值
cons_key = 301; //消费者同步信号灯键值
cmtx_key = 302; //消费者互斥信号灯键值
sem_flg = IPC_CREAT | 0644; //信号灯操作权限
//生产者同步信号灯初值设为缓冲区最大可用量
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;
//获取消费者互斥信号灯,引用标识存pmtx_sem
cmtx_sem = set_sem(cmtx_key, sem_val, sem_flg);
//循环执行模拟消费者不断取产品
while (1) {
//如果无产品消费者阻塞
down(cons_sem);
//如果另一消费者正在取产品,本消费者阻塞
down(cmtx_sem);
//用读一字符的形式模拟消费者取产品,报告本进程号和获取的字符及读取的位置
sleep(rate);
printf(" % d consumer get : % c from Buffer[% d]\n", getpid(), buff_ptr[*cget_ptr], *cget_ptr);
//读取位置循环下移
* cget_ptr = (*cget_ptr + 1) % buff_num;
//唤醒阻塞的消费者
up(cmtx_sem);
//唤醒阻塞的生产者
up(prod_sem);
}
return EXIT_SUCCESS;
}
5.在当前文件夹中建立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) -ohdrs = 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 *.oconsumer
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
(6)使用make命令编译连接生成可执行的生产者、消费者程序
gcc -g -c producer.c ipc.c
gcc producer.o ipc.o -o producer
gcc -g -c consumer.c ipc.c
gcc consumer.o ipc.o -o consumer
(7)在当前终端窗体中启动执行速率为1秒的一个生产者进程
$./producer 1
可以看到12263号进程在向共享内存中连续写入了8个字符后因为缓冲区满而阻塞。
(8)打开另一终端窗体,进入当前工作目录,从中再启动另一执行速率为3的生产者进程:
$./producer 3
可以看到该生产者进程因为缓冲区已满而立即阻塞。
(9)再打开另外两个终端窗体,进入当前工作目录,从中启动执行速率为2和4的两个消费者进程:
$ ./consumer 2
$./consumer 4
在第一个生产者窗体中生产者 1 被再此唤醒输出
在第二个生产者窗体中生产者 2 也被再此被唤醒输出
可以看到由于消费者进程读出了写入缓冲区的字符,生产者从新被唤醒继续向 读过的缓冲区单元中同步的写入字符。
3.独立实验----抽烟者问题
抽烟者问题。假设一个系统中有三个抽烟者进程,每个抽烟者不断地卷烟并抽烟。抽烟者卷起并抽掉一颗烟需要有三种材料:烟草、纸和胶水。一个抽烟者有烟 草,一个有纸,另一个有胶水。系统中还有两个供应者进程,它们无限地供应所有 三种材料,但每次仅轮流提供三种材料中的两种。得到缺失的两种材料的抽烟者在卷起并抽掉一颗烟后会发信号通知供应者,让它继续提供另外的两种材料。这一过程重复进行。请用以上介绍的IPC 同步机制编程,实现该问题要求的功能。
smoker1.h
#include "smoker1.h"
void sem_down(int sem_id) {
struct sembuf sem_op = {0, -1, SEM_UNDO};
if (semop(sem_id, &sem_op, 1) < 0) {
perror("sem_down failed");
exit(EXIT_FAILURE);
}
}
void sem_up(int sem_id) {
struct sembuf sem_op = {0, 1, SEM_UNDO};
if (semop(sem_id, &sem_op, 1) < 0) {
perror("sem_up failed");
exit(EXIT_FAILURE);
}
}
void init_ipc() {
shm_key = 1234;
sem_key = 5678;
shm_id = shmget(shm_key, sizeof(shm_data_t), IPC_CREAT | 0666);
if (shm_id < 0) {
perror("shmget failed");
exit(EXIT_FAILURE);
}
shm_data = (shm_data_t *)shmat(shm_id, NULL, 0);
if (shm_data == (void *)-1) {
perror("shmat failed");
exit(EXIT_FAILURE);
}
sem_id = semget(sem_key, 1, IPC_CREAT | 0666);
if (sem_id < 0) {
perror("semget failed");
exit(EXIT_FAILURE);
}
semctl(sem_id, 0, SETVAL, 1); // 初始化信号量为1
}
void cleanup_ipc() {
shmdt(shm_data);
shmctl(shm_id, IPC_RMID, NULL);
semctl(sem_id, 0, IPC_RMID);
}
smoker1.c
#include "smoker1.h"
void sem_down(int sem_id) {
struct sembuf sem_op = {0, -1, SEM_UNDO};
if (semop(sem_id, &sem_op, 1) < 0) {
perror("sem_down failed");
exit(EXIT_FAILURE);
}
}
void sem_up(int sem_id) {
struct sembuf sem_op = {0, 1, SEM_UNDO};
if (semop(sem_id, &sem_op, 1) < 0) {
perror("sem_up failed");
exit(EXIT_FAILURE);
}
}
void init_ipc() {
shm_key = 1234;
sem_key = 5678;
shm_id = shmget(shm_key, sizeof(shm_data_t), IPC_CREAT | 0666);
if (shm_id < 0) {
perror("shmget failed");
exit(EXIT_FAILURE);
}
shm_data = (shm_data_t *)shmat(shm_id, NULL, 0);
if (shm_data == (void *)-1) {
perror("shmat failed");
exit(EXIT_FAILURE);
}
sem_id = semget(sem_key, 1, IPC_CREAT | 0666);
if (sem_id < 0) {
perror("semget failed");
exit(EXIT_FAILURE);
}
semctl(sem_id, 0, SETVAL, 1); // 初始化信号量为1
}
void cleanup_ipc() {
shmdt(shm_data);
shmctl(shm_id, IPC_RMID, NULL);
semctl(sem_id, 0, IPC_RMID);
}
smoker_process1.c
#include "smoker1.h"
#include <unistd.h> // 确保包含unistd.h
void smoker(material_t my_material) {
while (1) {
sem_down(sem_id);
if (shm_data->ready) {
if (shm_data->materials[0] != my_material && shm_data->materials[1] != my_material) {
printf("Smoker with %d is making a cigarette\n", my_material);
shm_data->ready = 0;
sem_up(sem_id);
sleep(1);
continue;
}
}
sem_up(sem_id);
sleep(1);
}
}
int main(int argc, char *argv[]) {
if (argc != 2) {
fprintf(stderr, "Usage: %s <material>\n", argv[0]);
exit(EXIT_FAILURE);
}
material_t my_material = atoi(argv[1]);
init_ipc();
smoker(my_material);
cleanup_ipc();
return 0;
}
supplier_process1.c
#include "smoker1.h"
#include <unistd.h> // 确保包含unistd.h
#include <stdlib.h>
#include <time.h>
void supplier() {
material_t materials[3] = {TOBACCO, PAPER, MATCH};
int i, j;
srand(time(NULL)); // 初始化随机数生成器
while (1) {
sem_down(sem_id);
if (!shm_data->ready) {
i = rand() % 3;
j = (i + 1 + rand() % 2) % 3;
shm_data->materials[0] = materials[i];
shm_data->materials[1] = materials[j];
shm_data->ready = 1;
printf("Supplier provides: %d and %d\n", materials[i], materials[j]);
}
sem_up(sem_id);
sleep(1);
}
}
int main() {
init_ipc();
supplier();
cleanup_ipc();
return 0;
}
makefile文件
CC = gcc
CFLAGS = -g -Wall
TARGETS = smoker1 supplier1
all: $(TARGETS)
smoker1: smoker_process1.o smoker1.o
$(CC) $(CFLAGS) -o smoker1 smoker_process1.o smoker1.o
supplier1: supplier_process1.o smoker1.o
$(CC) $(CFLAGS) -o supplier1 supplier_process1.o smoker1.o
smoker_process1.o: smoker_process1.c smoker1.h
$(CC) $(CFLAGS) -c smoker_process1.c
supplier_process1.o: supplier_process1.c smoker1.h
$(CC) $(CFLAGS) -c supplier_process1.c
smoker1.o: smoker1.c smoker1.h
$(CC) $(CFLAGS) -c smoker1.c
clean:
rm -f $(TARGETS) *.o
执行
终端输入make
$ make
编译完之后分别在四个终端输入
$ ./supplier1
$ ./smoker1 0
$ ./smoker1 1
$ ./smoker1 2
运行结果示例
可以看到供应者一开始供应完一次材料之后因为无人取走,阻塞进程不再执行。而当抽烟者拿走材料后又唤醒供应者程序继续供应材料。