山东大学软件学院操作系统实验四——进程同步实验

Posted 叶卡捷琳堡

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了山东大学软件学院操作系统实验四——进程同步实验相关的知识,希望对你有一定的参考价值。

一、实验时间

2021年5月6日星期四,第10周

二、实验目的

加深对并发协作进程同步与互斥概念的理解,观察和体验并发进程同步与互斥操作的效果,分析与研究经典进程同步与互斥问题的实际解决方案。了解Linux系统中IPC进程同步工具的用法,练习并发协作进程的同步与互斥操作的编程与调试技术。

三、示例实验

在这次实验中,示例实验极其重要,可以说理解清楚了示例实验,独立实验就能很快的做出来,独立实验的大部分代码都是复制粘贴示例实验的代码的

示例实验中的ipc.h

#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;

// 生产消费者共享缓冲区即其有关的变量
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;

在头文件中,要重点理解down函数和up函数的作用,以及信号量的声明及其作用
比如在头文件中声明了以下几个信号量:
在接下来的独立实验里,我们同样可以定义和使用自己的信号量

// 生产者有关的信号量
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;

ipc.c文件

这个文件中主要是上面头文件里方法的具体实现,最后独立实验的时候直接复制粘贴即可,不需要做任何改动。在看示例实验的时候,要求理解清楚各个函数的作用即可,函数里面的具体内容不需要掌握

#include "ipc.h"
/*
* get_ipc_id() 从/proc/sysvipc/文件系统中获取 IPC 的 id 号
* pfile: 对应/proc/sysvipc/目录中的 IPC 文件分别为
*
msg-消息队列,sem-信号量,shm-共享内存
* key: 对应要获取的 IPC 的 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);
	}
	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;
}

producer.c

这个文件主要描述了生产者生产商品的过程
其中,循环生产的伪代码如下

while(1)
{
	//如果缓冲区已满,则不再生产
	wait(full);
	//如果另一个生产者在生产,则等待
	wait(mutex_producer);
	生产产品
	signal(mutex_producer);
	//唤醒消费者
	signal(consumer);
}

下面的代码使用的是ipc.h中定义的down和up函数实现了wait和signal的功能,原理都是一样的
还要注意的一点是在进入while循环之前,使用了很多变量,并利用这些变量获取信号量的值,这个在之后写独立实验的代码中要使用

#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;
}

consumer.c

这个文件主要描述的是消费者从缓冲区获取生产者生产的东西的过程
其中,循环获取的微代码为
下面的代码使用的是ipc.h中定义的down和up函数实现了wait和signal的功能,原理都是一样的

while(1)
{
		// 如果无产品消费者阻塞
		wait(consumer);
		// 如果另一消费者正在取产品,本消费者阻塞
		down(mutex_consumer);
		消费商品
		// 唤醒阻塞的消费者
		up(mutex_consumer);
		// 唤醒阻塞的生产者
		up(full);
}
#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;
}

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

示例实验的实验结果
在这里插入图片描述

四、独立实验

4.1 实验说明

抽烟者问题。假设一个系统中有三个抽烟者进程,每个抽烟者不断地卷烟并抽烟。抽烟者卷起并抽掉一颗烟需要有三种材料:烟草、纸和胶水。一个抽烟者有烟草,一个有纸,另一个有胶水。系统中还有两个供应者进程,它们无限地供应所有三种材料,但每次仅轮流提供三种材料中的两种。得到缺失的两种材料的抽烟者在卷起并抽掉一颗烟后会发信号通知供应者,让它继续提供另外的两种材料。这一过程重复进行。 请用以上介绍的IPC同步机制编程,实现该问题要求的功能。

首先需要明确题意,题意告诉我们以下几点

  1. 有两个生产者,它们向缓冲区循环放入材料,我的理解是缓冲区的数组长度为1,两个生产者向缓冲区轮流放入两个不同的材料。比如第一次生产者A向缓冲区放入烟草和纸,第二次生产者B向缓冲区放入烟草和胶水,等等。
  2. 三个抽烟者,这三个抽烟者每个有一样固定的东西,这个东西是胶水,纸或烟草,而其它两样东西则从缓冲区中获取,如果获取到,则抽烟,如果没有,就等待
  3. 关于生产者还有另一种理解,缓冲区长度为2,两个生产者分别往buffer[0]和buffer[1]中放入材料,两个生产者轮流放材料,放入的材料不能相同。之后抽烟者从缓冲区中取相应的材料。

生产者和抽烟者

生产者和抽烟者(消费者)需要以下几个信号量,这几个信号量需要自己在ipc.h中定义

// 生产者有关的信号量
//缓冲区已满的信号量
key_t full_key;
//保证放材料的时候生产者互斥的信号量
key_t mutex_key;
int full_sem;
int mutex_sem;

// 消费者有关的信号量
//有胶水和草
key_t GlueGrass_key;
//有胶水和纸
key_t GluePaper_key;
//有纸和草
key_t PaperGrass_key;
int GlueGrass_sem;
int GluePaper_sem;
int PaperGrass_sem;

生产者循环放物品的伪代码

while(1)
{
		// 如果缓冲区满则生产者阻塞
		wait(full_sem);
		// 如果另一生产者正在放产品,本生产者阻塞
		wait(mutex_sem);
		生产者根据if条件判断生产对应的商品
		// 唤醒阻塞的生产者
		signal(mutex_sem);
		//唤醒正在等待的消费者
		signal(对应的消费者);
}

抽烟者的伪代码

while(1)
{
		// 如果无产品消费者阻塞
		wait(对应材料的信号量);
		抽烟者获取材料并抽烟
		// 唤醒阻塞的生产者
		signal(full_sem);
}

4.2 完整代码

ipc.h

大学计算机相关专业实验实训整理

实验四

操作系统原理(基于清华大学视频教程)

山东大学软件学院数据库系统实验——基于华为RDS for MySQL数据库的实验

山东大学软件学院计算机组成原理课程设计整机实验

山东大学软件学院计算机组成原理课程设计整机实验