操作系统实验——哲学家进餐问题


说明:本实验综合学校实验指导书和个人上交的实验报告编写而成,感谢北京信息科技大学计算机学院操作系统实验指导老师的帮助。

一、实验内容

应用编程——哲学家进餐问题

问题描述:五位哲学家围坐在一个圆桌周围,每人面前有一个碗,各碗之间分别有一根筷子,餐桌如下图。

哲学家的生活包括两种活动:吃面条和思考。当哲学家感到饥饿时,他就分两次去取他左边和右边的筷子,每次拿一根(不能强行从邻座手中抢过筷子),如果成功,他就开始吃面条,吃完后把筷子放回原处继续思考。

参考给定的头文件和信号量封装函数文件,使用C语言编写Linux控制台程序,对并发环境下5位哲学家进餐过程进行演示。

问题分析:这是一个典型的对抗死锁的问题,涉及操作系统中的互斥和同步、死锁和饥饿。可以采用以下方法解决:

    (1) 最多只允许4个哲学家同时进餐,保证有一人能够进餐。

    (2) 仅当左、右两支筷子均可用时,才允许他拿起筷子。

    (3) 奇数号哲学家先拿左边的筷子,偶数号先拿右边的筷子。

这里采用第一种方法。

    Linux提供信号量操作的函数接口使用起来不够直观简便,需要对其进行一些封装。经过封装,提供5个操作:信号量的创建、信号量的赋值、P操作、V操作、信号量的删除。在此基础上,在主函数中使用fork创建5进程,模拟5哲学家,用5信号量模拟5筷子

二、应用编程——哲学家

1.原理

至多允许四个哲学家同时提出进餐请求,以保证至少有一个哲学家能够进餐,最终释放出他所使用过的两支筷子,从而可使更多的哲学家进餐。以下将room 作为信号量,只允许4 个哲学家同时进入餐厅就餐,这样就能保证至少有一个哲学家可以就餐,根据FIFO 的原则,每个哲学家总会进入到餐厅就餐,因此不会出现饿死和死锁的现象。

伪码:

  semaphore chopstick[5]={1,1,1,1,1};
  semaphore room=4;
  void philosopher(int i)
  {
      while(true)
      {
          think();
          P(room); //请求进入房间进餐
          P(chopstick[i]); //请求左手边的筷子
          P(chopstick[(i+1)%5]); //请求右手边的筷子
          eat();
          V(chopstick[(i+1)%5]); //释放右手边的筷子
          V(chopstick[i]); //释放左手边的筷子
          V(room); //退出房间释放信号量room
      }
  }

程序的总体流程可参考下图        

2.编写信号量封装的代码

可使用Linux中的编辑器gedit(pluma、vi或其他)新建并打开头文件: gedit  mysemop.h

编写代码如下:

#ifndef MYSEMOP_H
#define MYSEMOP_H
#include <stdlib.h>
#include <stdio.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <errno.h>
#include <fcntl.h>
/* 函数声明 */
/* 信号量管理函数 */
int CreateSem(int value);
int SetSemValue(int sem_id,int value);
void DeleteSem(int sem_id);

/* P、V原语 */
int Psem(int sem_id);
int Vsem(int sem_id);

#endif

使用Linux中的编辑器gedit(pluma、vi或其他)新建并打开C文件   gedit  mysemop.c

编写代码如下:

#include "mysemop.h"
/* 创建信号量 */
int CreateSem(int value)
{
	int sem_id;
	/* 获取一个信号量集的句柄 */
	sem_id = semget(IPC_PRIVATE, 1, 0666 | IPC_CREATE);
	if(sem_id == -1) return -1;
	/* 设置信号量初始值 */
	if(SetSemValue(sem_id, value)== 0) return -1;
	return sem_id;		
}
/* 强制设置信号量的值 */
int SetSemValue(int  sem_id,  int  value)
{
	if (semctl(sem_id, 0, SETVAL, value) == -1) return 0;
	return 1;  }

/* 删除信号量 */
void DeleteSem(int sem_id)
{
	if(semctl(sem_id, 0, IPC_RMID) == -1)
		fprintf(stderr,"Failed to delete semaphore\n");		
}

/* P原语*/
int Psem(int sem_id)
{
	  /*设置结构体struct sembuf */
	struct  sembuf  sem_b;
	sem_b.sem_num = 0;
	sem_b.sem_op = -1;
	sem_b.sem_flg = SEM_UNDO;
	
	/* 调用semop实现P操作 */
	if(semop(sem_id, &sem_b, 1) == -1){
		fprintf(stderr, "P failed\n");
		return 0;
	}
	return 1;
}

/* V原语*/
int Vsem(int sem_id)
{
	   /*设置结构体struct sembuf */
	struct  sembuf  sem_b;
	sem_b.sem_num = 0;
	sem_b.sem_op = 1;
	sem_b.sem_flg = SEM_UNDO;
	
	     /* 调用semop实现V操作 */
	if(semop(sem_id, &sem_b, 1) == -1){
		fprintf(stderr, "V failed\n");
		return 0;
	}
	return 1;
}

3.核心代码

#include "mysemop.h"
#include <stdio.h>
#include <math.h>
#include <stdlib.h>
#include <signal.h>
#include <unistd.h>

int main(int argc, char argv[])
{
	int room = CreateSem(4);	//创建信号量,最多允许4名哲学家同时拿筷子
	int id;			//接收fork()函数的返回值
	int pids[5];			//接收5个子进程的返回值
	int chopsticks[5];		//定义筷子信号量
	int eatTimes[5];               //定义每个哲学家的吃饭次数的信号量
       
       //该函数允许获取或设置信号量的信息,根据信号量的id(信号量标识符)获得信号量的值
       int GetSemValue(int sem_id) {
          return semctl(sem_id, 0, GETVAL);
       }
       
	//循环创建5个筷子信号量
	for(int i = 0;i<5;i++){
		chopsticks[i] = CreateSem(1);//CreateSem函数可能返回的并不是信号量的实际值,而是一个信号量的标识符或键值
		//printf("%d\n",chopsticks[i]);//错误的查看信号量的操作,打印的是信号量的标识符
		printf("%d\n", GetSemValue(chopsticks[i]));//正确的查看信号量的操作
	}
	
	
	for(int i = 0;i<5;i++){
		eatTimes[i] = CreateSem(0);
	}
	
	for(int i = 0;i<5;i++){
		id = fork();		//创建进程  id是fork返回的值.父进程中fork返回值是子进程id。子进程id返回是0
		printf("%d %d\n", id,getpid());//子进程pid,调用fork的进程的pid
		int count = 0;
		if(id<0){
			printf("创建进程失败!");
			exit(-1);//传递 -1 作为退出码来终止进程
		}
		if(id == 0){	//如果是子进程,则执行哲学家进餐的一系列操作
			while(1){
				printf("第%d号哲学家正在思考。进程号%d \n",i ,getpid());
				sleep(1);
				
				//三个P操作
				Psem(room);
							
				Psem(chopsticks[i]);
				printf("第%d号哲学家拿起了他左边的筷子。进程号%d \n",i ,getpid());
				
				Psem(chopsticks[(i+1)%5]);
				printf("第%d号哲学家拿起了他右边的筷子。进程号%d \n",i ,getpid());
				
			
				//临界区代码
				count += 1;
				SetSemValue(eatTimes[i],count);
				printf("第%d号哲学家正在进行第%d次进餐。---------->进程号%d \n",i ,count,getpid());
				sleep(5);
				
				//三个V操作
				Vsem(chopsticks[i]);
				printf("第%d号哲学家放回了他左边的筷子。进程号%d \n",i ,getpid());
				
				Vsem(chopsticks[(i+1)%5]);
				printf("第%d号哲学家放回了他右边的筷子。进程号%d \n",i ,getpid());
				
				Vsem(room);
				printf("第%d号哲学家继续思考。进程号%d \n",i ,getpid());
				sleep(1);
			}
		}
		else{//父进程走的分支
			pids[i] = id;//fork给父进程的返回值是子进程的pid,这里保存子进程的pid
		}
	}
	
	char ch;
	//当键盘输入q时,结束,并删除信号量。
	while(1){
		ch = getchar();
		
		if(ch=='q'){
			for(int i = 0;i<5;i++){
			        printf("第%d个哲学家吃了%d次\n",i,GetSemValue(eatTimes[i]));
				kill(pids[i], SIGTERM);//杀掉子进程  SIGTERM是Linux/Unix系统中的信号,用于通知进程终止运行

				DeleteSem(chopsticks[i]);//删除筷子信号量
				DeleteSem(eatTimes[i]);//删除吃饭次数信号量
				printf("已删除第%d个筷子信号量和吃饭次数信号量。\n",i);
			}
			
			printf("已删除room信号量。\n");
			DeleteSem(room);//删除room信号量
			break;
		}
	}
	
	//printf("%d",GetSemValue(eatTimes[1]));//这是一个查看某哲学家吃了多少次的试验代码
	return 0;
}
//gcc -o myprogram mysemop.c test.c
//./myprogram

4.运行结果

三、实验遇到问题及总结

问题:没有正确理解fork()函数返回值的情况,以为fork()函数返回值就只有0(创建子进程成功)和负数(代表创建进程失败)两种情况。导致根据fork函数的返回值为每个子进程实现内部功能时总是出错。

反思:当程序执行到pid=fork()时,操作系统创建一个新的进程(子进程),并且在进程表中相应为它建立一个新的表项。新进程和原有进程的可执行程序是同一个程序,子进程的上下文和数据是父进程的拷贝,但它们是两个相互独立的进程。

虽然fork() 函数调用一次,但是会返回两个值。对于主进程 fork()返回的值是新建的子进程ID;对于新创建的子进程来说,fork()函数的返回值是0,这俩进程会并行执行。按照这个理解思路将主要的代码框架写完,就能使子进程进入到正确的分区且每次产生的子进程的进程号能储存起来)

for(int i = 0;i<5;i++){
		id = fork();
If(id <0){ //创建子进程失败后走的分支
	      ......
		}
if(id == 0){ //子进程走的分支,完成PV操作	
  ......
} 	
else{ //父进程走的分支	
pids[i] = id; //将fork()函数的返回值(子进程的进程号保存起来)
}
}

②问题:通过设置信号量eatTimes来存储每个子进程的哲学家吃了进餐多少次。但是想要把每个子进程中的哲学家进餐次数在结尾按“q”停止时集中打印出来,直接打印printf("%d",eatTimes[1])时返回的值不正确。

反思:查看信号量的设置方法eatTimes[i] = CreateSem(0),我发现CreateSem函数返回的并不是信号量的实际值,而是一个信号量的标识符。因此通过printf("%d",eatTimes[i])来获取具体的信号量是不对的。我新设计了一个GetSemValue()函数来获取信号量的值,并通过GetSemValue(eatTimes[i])来返回哲学家的进餐次数。

//初始化吃饭次数eatTimes信号量的值
for(int i = 0;i<5;i++){
		eatTimes[i] = CreateSem(0);
 }
//获取信号量的值
int GetSemValue(int sem_id) {
          return semctl(sem_id, 0, GETVAL);
  }
//打印信号量的值
if(ch=='q'){
			for(int i = 0;i<5;i++){
			        printf("第%d个哲学家吃了%d次\n",i,GetSemValue(eatTimes[i]));
				kill(pids[i], SIGTERM);//杀掉子进程
				
				DeleteSem(chopsticks[i]);//删除筷子信号量
				DeleteSem(eatTimes[i]);
				printf("已删除第%d个筷子信号量和吃饭次数信号量。\n",i);
			}
			
			printf("已删除room信号量。\n");
			DeleteSem(room);//删除room信号量
			break;
}

③问题:没有在按“q”结束进程时正确的杀死进程,导致进程无法停止下来。
反思:通过查询使用kill命令,我发现我只终结了信号量,而没有杀死进程。终结了信号量之后信号量被删除了,后续进程在进行P操作时无法对信号量删除,所以会打印出“P failed”的标识。我查询了进程中杀死信号量的操作,发现必须写出这一行才能正常执行:kill(pids[i], SIGTERM)。其中kill杀死的进程是pids[i],即前面通过fork函数返回的子进程pid值,根据pid号一个个杀死子进程,而SIGTERM是Linux系统中的信号,用于通知进程终止运行。

④问题:没有考虑清楚按照Psem(room);Psem(chopsticks[i]) ; Psem(chopsticks[(i+1)%5]);顺序的原因,在编代码时不小心按照Psem(chopsticks[i]);Psem(room);Psem(chopsticks[(i+1)%5])的顺序进行编码,导致了死锁现象的发生。
反思:换成这个顺序,Psem(chopsticks[i]); Psem(room); Psem(chopsticks[(i+1)%5]);如果此时已经有4个人进入餐桌,且每人都拿取左边的餐叉,这时第五轮循环的时候,还是可以通过Psem(chopsticks[i])来先拿取左边的餐叉(此时所有餐叉已经占用),当再执行Psem(room)时,room信号量此时已经变成-1,就会卡在这里等待Vsem(room)操作释放room信号量。但这时大家都拿去了餐叉,每个人都在等待别人用完餐后释放room信号量,无人进餐,出现了死锁问题。

//三个P操作的顺序
				Psem(room);							
				Psem(chopsticks[i]);	
				Psem(chopsticks[(i+1)%5]);

四、改进措施

实验中为了实现打印每个哲学家的进餐次数,我前后想到了3种方法:

是经过fork()函数创建完子进程后,创建变量count,在每个子进程完成3个P操作之后给count自加1,并打印出来。但是这种方法未采用信号量来统计每个哲学家进餐的次数,只是在进程运行中用简单的变量count自加统计进餐次数,因此无法在进程中实现进程间通信,无法在最终结束时打印出每个哲学家的进餐次数。

//未使用信号量的临界区代码
count += 1;
printf("第%d号哲学家正在进行第%d次进餐。---------->进程号%d \n",i ,count,getpid());
sleep(5);

是创建了一个数组eatTimes[5]作为每个哲学家吃饭次数的信号量。在子进程中,通过调用SetSemValue(eatTimes[i],count)将每个哲学家进餐的次数保存到相应的信号量中。最后在结束的时候,通过调用自己编写的GetSemValue()函数来集中获取每个哲学家进餐的次数。

//使用SetSemValue()函数的临界区代码
count += 1;
SetSemValue(eatTimes[i],count);
printf("第%d号哲学家正在进行第%d次进餐。---------->进程号%d \n",i ,count,getpid());
sleep(5);

是在查实验时,老师看到我给eatTimes[5]信号量数组赋值时采用这种方式进行赋值SetSemValue(eatTimes[i],count)。老师说还可以利用Vsem()函数实现每次吃饭计数的功能,这样可以免去设置count变量来通过每次通过自加一的操作count += 1。我按照老师说的去除了count元素,使得代码变得更加简洁优美。以下是我更新前后的临界区的代码繁简对比。

//使用Vsem()函数的临界区代码
Vsem(eatTimes[i]);				
printf("第%d号哲学家正在进行第%d次进餐。---------->进程号%d \n",i ,count,getpid());
sleep(5);

也可以采用共享内存的方式进行来获取每个哲学家的进餐次数。创建一个共享内存区域,用于存储每个哲学家的进餐次数。在子进程中,通过获取共享内存的指针,对对应的哲学家的进餐次数进行递增操作;在主进程中,通过读取共享内存中的数据,获取每个哲学家的进餐次数,并进行打印。

五、实验所用关于系统调用的说明

1.使用一次fork()调用,会得到两个返回值,用这种方式在内存中建立一个新进程。新进程是父进程(parent  process)的副本,称为子进程(child  process),子进程继承了父进程的许多特性,并具有与父进程完全相同的用户级上下文。

进程创建后,父、子进程并发执行,但每个进程都有自己的程序计数器PC。在程序中,可根据pid变量保存的fork()返回值的不同,使它们执行不同的分支语句。

2.实验涉及信号量操作的函数及系统调用

头文件:

#include <sys/types.h>

#include <sys/ipc.h>

#include <sys/sem.h>

 3.系统用semget( )

原型:int semget (key_t  key,    int  nsems,    int  semflg);

作用:创建并打开一个信号量集,或打开一个已创建的信号量集。

参数说明:key是创建或打开信号量集时所需的键值,常采用IPC_PRIVATE,这样可以由系统分配信号量的编号;nsems是信号量个数,如果只是打开信号量,把nsems设置为0即可,此处设置为1,该参数只在创建信号量集时有效;semflg是函数操作的标识位,包含访问权限和函数的操作类型,可以使用IPC_CREAT和IPC_EXCL,IPC_CREAT表示创建(指定键不存在时)或打开(指定键存在时),IPC_CREAT | IPC_EXCL表示仅创建(指定键存在时出错),经常使用的参数是0666 | IPC_CREAT。

返回值:如果成功,则返回信号量集的ID;如果失败则返回-1。

4.semop( )  (对信号量的操作,即改变信号量的值)

信号量的值与相应资源的使用情况有关,当它的值大于0时,表示当前可用资源的数量,当它的值小于0时,其绝对值表示等待使用这个资源的进程个数。

信号量的值仅能由P、V操作来改变。Linux系统中,P、V操作通过调用函数semop实现。

原型:int semop (int sem_id,  struct sembuf  *sops,  unsigned  nsops);

作用:对sem_id中的一个或多个信号量进行操作。

参数说明

    第一个参数:sem_id是由semget返回的信号量集标识符。

    第二个参数:sops是指向结构体数组的首地址,此结构体的具体说明如下:

      struct sembuf

      {

          short  sem_num;//是信号量集合中的信号量编号,除非要使用一组信号量,否则它的取值一般为0

short  sem_op;//是信号量在一次操作中需要改变的数值。通常会用到两个值,一个是-1,也就是P操作,它等待信号量变为可用;一个是+1,也就是V操作,它发出信号表示信号量已能使用。

         short   sem_flg; //通常被设置为SEM_UNDO,程序结束时(不论正常或不正

                      常),保证信号值会被重设为semop()调用前的值。这样做

                      的目的是避免程序在异常结束时(在P、V之间系统崩溃)

                      未将锁定的资源解锁,造成该资源被一直锁定。

      }

   第三个参数nsops:结构体(struct sembuf)数组的元素个数,恒大于或等于1。最常用设置为1,即只做一个信号量的操作。

返回值:若调用成功,则返回0;否则返回-1.

5.semctl函数(可变参函数)

  作用:实现在信号量集上的各种控制操作。

 原型: int semctl (int sem_id ,  int sem_num,  int command,...);

  参数说明

   第一个参数:sem_id是由semget返回的信号量集标识符。

   第二个参数:是信号量编号,当需要用到一组信号量时,就要用到这个参数,它的一般取值为0,表示这是第一个也是唯一的一个信号量,指定对这个信号量操作。

   第三个参数:是将要对信号量集采取的动作。常用的是下面两个:

SETVAL:设置信号量集中的一个信号量的值。

IPC_RMID:从内存中删除一个不再使用的信号量集。

返回值:semctl函数根据command参数的不同返回不同的值。对于SETVAL和IPC_RMID,成功时返回0,失败时返回-1。

  • 47
    点赞
  • 70
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值