文章目录
- 一、 实验目的
- 二、 实验原理
- 三、实验内容
- 四、我的代码内容和现象
- 1、philosopher1
- 2、philosopher2
- 这个程序不会发生死锁,因为
- 五、课后习题:
- 1.什么是死锁?产生死锁的原因和必要条件是什么?
- 2.实验中给出的伪代码流程,实现多位哲学家就餐问题,是否会产生死锁?若会,请说明产生死锁的原因。
- 3.针对本实验的哲学家就餐问题,如何设置信号量以避免产生死锁?
- 六、心得体会
- 每天进步一点点 笔记仅供自学,用来回看复习,不一定适合你,如有错误请指出。
一、 实验目的
1、了解进程的互斥与同步的概念,理解经典进程同步问题的本质
2、熟悉 Linux 的进程同步机制,掌握相关 API 的使用方法
3、能利用信号量机制,采用多种同步算法实现不会发生死锁的哲学家进餐程序
二、 实验原理
有点长,所以分开了,点击这里跳转进入!!!!!!!!!!!!!!!
三、实验内容
1、以哲学家进餐模型为依据,在 Linux 控制台环境下创立 5 个进程,用 semget 函数创立一个
信号量集〔5 个信号量,初值为 1〕,模拟哲学家的思考和进餐行为:每一位哲学家饥饿时,先拿
起左手筷子,再拿起右手筷子;筷子是临界资源,为每一支筷子定义 1 个互斥信号量;想拿到筷
子需要先对信号量做 P 操作,使用完释放筷子对信号量做 V 操作。
伪代码描述:
semaphore chopstick[5]={1,1,1,1,1};
第 i 位哲学家的活动可描述为:
do{
printf("Philopher %d is thinking\n",i);
printf("Philopher %d is hungry\n",i);
P(chopstick[i]); //拿左筷子
P(chopstick[(i+1) % 5]); //拿右筷子
printf("Philopher %d is eating\n",i);
V(chopstick[i]); //放左筷子
V(chopstick[(i+1) % 5]); //放右筷子
…
}while[true];
请根据伪代码编写程序 philosopher1.c,创建该组进程并运行,观察进程是否能一直运行下去。假设停滞那么发生了什么现象?请分析原因,并给出解决方法,编写程序 philosopher2.c 进行验证。
2、五位哲学家围坐在一张圆形桌子上,桌子上有一盘饺子。每一位哲学家要么思考,要么等待,要么吃饺子。为了吃饺子,哲学家必须拿起两只筷子,但是每个哲学家旁边只有一只筷子,也就是筷子数量和哲学家数量相等,所以每只筷子必须由两个哲学家共享。设计一个算法以允许哲学家吃饭。算法必须保证互斥(没有两位哲学家同时使用同一只筷子),同时还要避免死锁(每人拿着一只筷子不放,导致谁也吃不了)。
请根据哲学家就餐问题流程和上文提供的伪代码编写程序,并分析程序存在的问题及改进办法。
程序运行如下:
四、我的代码内容和现象
1、philosopher1
这个代码会发生死锁的原因是,哲学家 0 和哲学家 4 都拿起了左手边的筷子,然后都在等待右手边的筷子,而右手边的筷子分别被哲学家 1 和哲学家 3 拿起了,所以哲学家 0 和哲学家 4 无法获取右手边的筷子,从而导致死锁。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <time.h>
#include <sys/wait.h>
#define NUM_PHILOSOPHERS 5
#define NUM_FORKS 5
int main()
{
// 创建信号量集,包含 NUM_FORKS 个信号量,初始值都为 1
int semid = semget(IPC_PRIVATE, NUM_FORKS, IPC_CREAT | 0666);
if (semid < 0)
{
perror("semget failed");
exit(1);
}
// 创建 NUM_PHILOSOPHERS 个进程,每个进程模拟一位哲学家的思考和进餐行为
for (int i = 0; i < NUM_PHILOSOPHERS; i++)
{
if (fork() == 0)
{
// 当前进程是哲学家 i,先拿起左手筷子,再拿起右手筷子
struct sembuf sb[2];
sb[0].sem_num = i;
sb[0].sem_op = -1;
sb[0].sem_flg = 0;
semop(semid, sb, 2);
printf("Philosopher %d is putting up left chopstick.\n", i);
sb[1].sem_num = (i + NUM_PHILOSOPHERS - 1) % NUM_FORKS; // 修改这行代码,使得哲学家拿起筷子的顺序反过来
sb[1].sem_op = -1;
sb[1].sem_flg = 0;
semop(semid, sb, 2);
printf("Philosopher %d is putting up right chopstick.\n", i);
// 模拟进餐行为
printf("Philosopher %d is eating\n", i);
sleep(1);
// 释放筷子
sb[0].sem_op = 1;
semop(semid, sb, 2);
printf("Philosopher %d is putting down left chopstick.\n", i);
sb[1].sem_op = 1;
printf("Philosopher %d is putting down right chopstick.\n", i);
semop(semid, sb, 2);
// 模拟思考行为
printf("Philosopher %d is thinking\n", i);
sleep(1);
}
}
// 删除信号量集
semctl(semid, 0, IPC_RMID);
return 0;
}
2、philosopher2
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/ipc.h>
#include <sys/sem.h>
#include <time.h>
#include <sys/wait.h>
#define NUM_PHILOSOPHERS 5 //信号量数组,分别用于维护每一位哲学家左手和右手的筷子,避免发生死锁
#define NUM_chopstick 5 //筷子数量
/*哲学家当前状态的定义*/
#define THINKING 0//思考中
#define HUNGRY 1 //饥饿中
#define EATING 2 //吃饭中
int state[NUM_PHILOSOPHERS];//数组,存储哲学家当前的状态
int sem_id; //用于存储信号量集的标识符。
//打印当前在思考中的哲学家
void think(int philosopher)
{
printf("Philosopher %d is thinking.\n", philosopher);
sleep(rand() % 5); //模拟思考的过程,思考需要时间
}
//打印当前在拿筷子的哲学家
void take_chopstick(int philosopher)
{
state[philosopher] = HUNGRY;
printf("Philosopher %d is hungry.\n", philosopher);
// wait for left chopstick
struct sembuf sops;
sops.sem_num = philosopher;
sops.sem_op = -1; //操作数option为 -1 ,即为P操作
sops.sem_flg = 0;
semop(sem_id, &sops, 1);
printf("Philosopher %d is putting up left chopstick.\n", philosopher);
// wait for right chopstick
sops.sem_num = (philosopher + 1) % NUM_chopstick;
semop(sem_id, &sops, 1);
printf("Philosopher %d is putting up right chopstick.\n", philosopher);
}
void put_chopstick(int philosopher)
{
state[philosopher] = THINKING;
// 放下左筷子
struct sembuf sops;
sops.sem_num = philosopher;
sops.sem_op = 1; //操作数option为 1 ,即为V操作
sops.sem_flg = 0;
semop(sem_id, &sops, 1);
printf("Philosopher %d is putting down left chopstick.\n", philosopher);
// 放下右筷子
sops.sem_num = (philosopher + 1) % NUM_chopstick;
semop(sem_id, &sops, 1);
printf("Philosopher %d is putting down left chopstick.\n", philosopher);
}
//打印正在吃饭的哲学家
void eat(int philosopher)
{
state[philosopher] = EATING;
printf("Philosopher %d is eating.\n", philosopher);
sleep(rand() % 5); //模拟哲学家吃饭,吃饭需要花费一段时间
}
int main()
{
// 为随机数生成器提供种子
srand(time(NULL));
// 用初始值创建信号量集
sem_id = semget(IPC_PRIVATE, NUM_chopstick, 0600);
if (sem_id < 0)
{
perror("semget failed");
exit(1);//返回非0,表示运行有错,异常终止
}
semctl(sem_id, 0, SETALL, 1);
// 创造哲学家的进程
for (int i = 0; i < NUM_PHILOSOPHERS; i++)
{
if (fork() == 0)
{
// 哲学家的进程
while (1)
{
think(i);
take_chopstick(i);
eat(i);
put_chopstick(i);
}
}
}
// 删除信号量集
semctl(sem_id, 0, IPC_RMID);
return 0;
}
这个代码实现了哲学家进餐问题的模拟。它使用了信号量来维护每一支筷子的互斥访问,并使用了 P 操作和 V 操作来控制对信号量的访问。
1、在主函数中,首先使用semget 函数
创建了一个信号量集,并初始化了NUM_chopstick
个信号量,每个信号量的初始值都为 1
。然后使用fork 函数
创建了NUM_PHILOSOPHERS
个进程,每个进程模拟一位哲学家的思考和进餐行为。
2、每个哲学家进程都会执行一个无限循环,在循环中,它会先执行think 函数
模拟思考的过程,然后执行take_chopstick 函数
模拟拿起筷子的过程。这个函数会使用semop 函数
执行P 操作
来获取左手边的筷子和右手边的筷子。
– 如果获取成功,就会执行eat 函数
模拟进餐的过程,然后执行put_chopstick 函数
模拟释放筷子的过程。这个函数会使用semop 函数
执行V 操作
来释放左手边的筷子和右手边的筷子。
3、最后,主函数会使用 wait 函数等待所有的哲学家进程结束,然后使用semctl 函数
删除信号量集。
这个程序不会发生死锁,因为
1、在程序中,哲学家的进餐行为是通过调用 take_chopstick() 函数实现的。这个函数首先会将当前哲学家的状态设置为饥饿状态,然后会调用 semop() 函数对左手筷子执行 P 操作,再对右手筷子执行 P 操作。
2、当哲学家吃饱之后,会调用 put_chopstick() 函数将左右手的筷子放回桌子上。这个函数会将当前哲学家的状态设置为思考状态,然后对左手筷子和右手筷子分别执行 V 操作,释放这两支筷子。
3、由于这个程序中没有设置资源不足的情况,也没有设置资源分配不当的情况,并且哲学家在吃饭完成后会立即释放筷子,因此该程序不会产生死锁。
五、课后习题:
1.什么是死锁?产生死锁的原因和必要条件是什么?
答:死锁是指两个或多个进程因争夺资源而相互等待,导致系统无法继续运行的状态。
产生死锁的原因有以下几种:
(1)资源不够:系统中资源数量有限,如果进程需要的资源数量超过了系统能够提供的数量,那么就会发生死锁。
(2)资源分配不当:如果资源分配不当,比如一个进程已经占用了资源 A 和资源 B,另一个进程已经占用了资源 B 和资源 C,那么这两个进程就会相互等待,从而导致死锁。
(3)资源占用不当:如果进程在使用资源的过程中不释放资源,就会导致其他进程无法获取所需资源,从而导致死锁。
产生死锁的必要条件包括四个要素:
(1)互斥条件:所谓互斥条件是指系统中的资源至多只能被一个进程占用。如果一个进程占用了资源,则其他进程就不能占用这些资源。
(2)请求与保持条件:所谓请求与保持条件是指进程已经保持至少一个资源,但又提出了新的资源请求,而该资源正被其他进程占有,此时请求进程阻塞,但又对自己已获得的资源保持不放。
(3)不剥夺条件:所谓不剥夺条件是指进程已获得的资源在未使用完之前,不能强行剥夺。
(4)循环等待条件:所谓循环等待条件是指存在一个进程的资源请求序列{P0, P1, …, P0},使得 Pi 必须等到 Pj 释放资源后才能得到资源,其中 i≠j。
当满足上述四个条件时,就会产生死锁。
2.实验中给出的伪代码流程,实现多位哲学家就餐问题,是否会产生死锁?若会,请说明产生死锁的原因。
答:在这个伪代码中,会产生死锁的原因是:
(1)每个哲学家都在循环中进行思考、饥饿和吃饭的行为,并没有退出循环的条件。
(2)在拿筷子之前,每个哲学家都会先拿起左手的筷子,然后再拿起右手的筷子。
(3)当两个哲学家同时拿起了自己的左手筷子时,如果他们都想要拿起右手的筷子,就会产生死锁。
具体情况是这样的:哲学家 1 和哲学家 2 同时拿起了自己的左手筷子,然后同时等待右手的筷子。但是右手的筷子都被对方拿起了,所以他们就陷入了死锁。
为了避免这种情况的发生,可以使用一些机制来解决死锁问题,比如说:
(1)对于每个哲学家,使用随机数来决定先拿哪只手的筷子。
(2)在拿筷子的时候,设置一个超时时间,如果超过了这个时间还没有拿到筷子,就退出循环。
(3)使用顺序等待,让每个哲学家按照固定的顺序拿筷子,这样就不会出现同时拿起左手筷子的情况。
总之,死锁的产生是由于多个进程因为竞争临界资源。
3.针对本实验的哲学家就餐问题,如何设置信号量以避免产生死锁?
答:为了避免在本实验的哲学家就餐问题中产生死锁,可以设置一个顺序信号量,使得哲学家在拿筷子时按照固定顺序进行。比如,哲学家 0 只能先拿左边的筷子再拿右边的筷子,哲学家 1 只能先拿右边的筷子再拿左边的筷子,以此类推。这样,只要所有的哲学家都按照这个顺序拿筷子,就不会产生死锁的情况。
六、心得体会
完成本试验后,我认识到了信号量的作用和使用方法,了解了互斥信号量和条件信号量的区别,并了解了死锁的产生原因和如何避免死锁的方法。在实验中,我还学会了使用 semget 函数和 semop 函数来创建信号量集和进行信号量操作。
总的来说,本实验使我加深了对进程同步和死锁问题的理解,并学会了使用信号量来解决实际问题。