XMU《UNIX 系统程序设计》第五次实验报告(编制模拟“五个哲学家”问题的程序)

想知道第三、四次实验去哪儿了吗?我也想知道。

实验五 编制模拟“五个哲学家”问题的程序

一、实验内容描述

编制模拟“五个哲学家”问题的程序

目的

学习和掌握并发进程同步的概念和方法。

要求

  1. 程序语法

    philosopher   [ -t  <time> ]
    

    <time> 是哲学家进餐和沉思的持续时间值,缺省值为2秒。

  2. 五个哲学家的编号为0~4,分别用五个进程独立模拟。

  3. 程序的输出要简洁,仅输出每个哲学家进餐和沉思的信息。例如,当编号为3的哲学家在进餐时,就打印:

philosopher 3 is eating

而当他在沉思时,则打印:

philosopher 3 is thinking

除此之外不要输出其他任何信息。
4. 利用课堂已教授的知识而不使用线程或IPC机制进行同步。
5. 程序应该一直运行,直到人为地终止它(按 Ctrl-CCtrl-\)。不允许出现僵尸进程。
6. 12月15日24点为实验完成deadline。

二、设计与实现

哲学家问题是这样的,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。吃东西的时候,他们就停止思考,思考的时候也停止吃东西。餐桌上有五碗意大利面,每位哲学家之间各有一只餐叉。因为用一只餐叉很难吃到意大利面,所以假设哲学家必须用两只餐叉吃东西。他们只能使用自己左右手边的那两只餐叉。

这个实验的目标就是使用并发进程同步的方法,模拟他们就餐的顺序。

程序的框架设计很明确,实验文档中也已经给出来了相关的代码:在主程序中,我们调用 fork 建立五个子进程,每个子进程代表一个哲学家。

在每个子进程中,我们使用一个死循环,重复进行思考、拿起叉子、就餐、放下叉子……其中,思考、就餐两个步骤我们只需要输出一条 philosopher i is eating/thinking,然后进行 sleep 即可。

放下叉子这个时间很容易,没有什么争议,只需要依次方向两只手上的叉子即可,对应在程序中就是解除两个锁。

对于拿起叉子这个事件,实验文档中也给出了设计:先拿起一只手边的叉子,再拿起另一只手的叉子。值得注意的是,对于 \(0\)\(3\) 号哲学家,他们拿起叉子的顺序是先拿起左手边的叉子(forks[i]),再拿起右手边的叉子(forks[i + 1]);而对于 \(4\) 号哲学家,他拿起叉子的顺序是先拿起右手边的叉子(fork[0]),再拿起左手边的叉子(fork[4])。

这样做可以避免出现死锁。考虑到,只有两种情况会出现死锁:在某一时刻,每一个科学家都拿起了左手边的叉子,而没有拿起右手边的叉子;都拿起了右手边的叉子,没有拿起左手边的叉子。这时每一个哲学家都将会去等待另一只手边叉子被解锁,而又不愿意放下自己手中的叉子,导致所有科学家都在不停地等待,从而产生死锁。

上面的设计保证了锁之间的偏序,加锁的顺序不会产生环路,因此不可能存在所有科学家都一只手有叉子,一只手没有叉子的情况。

现在的问题来到怎么实现锁。

因为这个实验只能使用文件作为锁,不能使用线程和 IPC,所以我们考虑到可以以文件被创建和被删除作为锁的状态。

我们规定一个文件 forki 存在,代表叉子 \(i\) 被加了锁;如果不存在,则代表这个叉子没有被加锁。

然后,我们只需要在需要取用锁的时候,轮询判断 forki 是否存在即可。如果存在,我们就继续循环,如果不存在我们就创建文件,然后跳出自旋轮询执行后面的代码。

在我期初的设计中,我使用的是 open(fork, O_RDONLY) 来判断文件是否存在,使用 creat() 系统调用来创建文件。这导致了判断和创建的分离,从而引起了一个竞争问题:如果一个进程 \(i\) 在判断 forki 不存在以后被调度成了进程 \(i + 1\),这个时候进程 \(i + 1\) 也判断 forki 不存在,于是它创建了 forki 文件。如果进程 \(i\) 再次被调度,那么导致其继续执行,于是它重复创建了文件 forki,这就意味着两个进程同时获得了 forki 的锁,从而引起错误。

于是,我发现可以使用 open(fork, O_CREAT | O_EXCL) 来创建文件,O_EXCL 参数使得文件判断和创建合并成了一个原子操作,如果文件已存在则返回错误,如果不存在则自动创建,故而避免了上述问题。

void lock(char* fork) {
    int fd;
    while (1) {
        if ((fd = open(fork, O_CREAT | O_EXCL, 0)) >= 0) {
            close(fd);
            break;
        }
    }
}

void unlock(char* fork) {
    if (access(fork, F_OK) == 0)
        unlink(fork);
}

完整的代码:

#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <fcntl.h>
#include <sys/wait.h>

#define N 5

static char* forks[N] = { "fork0", "fork1", "fork2", "fork3", "fork4" };
static int nsecs;

void philosopher(int i);
void takeFork(int i);
void putFork(int i);
void thinking(int i, int nsecs);
void eating(int i, int nsecs);
void lock(char* fork);
void unlock(char* fork);

int main(int argc, char* argv[]) {
	if (argc == 1)
		nsecs = 2;
	else if (argc == 3 && strcmp(argv[1], "-t") == 0)
		nsecs = atoi(argv[2]);
	else {
		fprintf(stderr, "Usage: %s [-t <time>]\n", argv[0]);
		exit(1);
	}

    for (int i = 0; i < N; ++i)
        unlock(forks[i]);

	for (int i = 0; i < N; ++i) {
		int pid = fork();
		if (pid == 0) {
			philosopher(i);
			exit(0);
		}
	}
	wait(NULL);

    return 0;
}

void lock(char* fork) {
    int fd;
    while (1) {
        if ((fd = open(fork, O_CREAT | O_EXCL, 0)) >= 0) {
            close(fd);
            break;
        }
    }
}

void unlock(char* fork) {
    if (access(fork, F_OK) == 0)
        unlink(fork);
}

void philosopher(int i) {
	while (1) {
		thinking(i, nsecs);
		takeFork(i);
		eating(i, nsecs);
		putFork(i);
	}
}

void takeFork(int i) {
	if (i == N - 1) {
		lock(forks[0]);
		lock(forks[i]);
	} else {
		lock(forks[i]);
		lock(forks[i + 1]);
	}
}

void putFork(int i) {
	if (i == N - 1) {
		unlock(forks[0]);
		unlock(forks[i]);
	} else {
		unlock(forks[i]);
		unlock(forks[i + 1]);
	}
}

void thinking(int i, int nsecs) {
    printf("philosopher %d is thinking\n", i);
    sleep(nsecs);
}

void eating(int i, int nsecs) {
    printf("philosopher %d is eating\n", i);
    sleep(nsecs);
}

三、实验结果

1. 编译程序

gcc philosopher.c -o philosopher -std=c99

2. 运行截图

image-20231201163053191

image-20231201163142233

四、实验体会

这次的实验让我深入理解了并发进程同步的概念和方法,通过模拟“五个哲学家”问题,我更加熟悉了在进程间进行同步操作的重要性。在实验过程中,我遇到了一些挑战,但通过仔细思考和调试,最终成功完成了任务。

这次实验让我对哲学家问题有了更深入的理解。通过构建一个围坐在圆形餐桌旁的场景,每个哲学家都有思考和进餐两个行为,而叉子则成为同步的关键。这种抽象的问题在计算机科学中很有代表性,通过实际编码实现,我更好地理解了理论概念在实践中的应用。

在实验中,锁的设计成为了一个关键点。由于不能使用线程或 IPC 机制,我采用了文件作为锁的形式。通过 open(fork, O_CREAT | O_EXCL) 的方式,我确保了判断和创建文件的原子性,有效避免了竞争问题。这让我认识到在并发编程中,同步机制的设计和实现需要仔细考虑,以防止出现不确定的行为。

另外,通过实现整个过程,我对操作系统中的进程控制有了更深刻的认识。使用 fork 系统调用创建五个独立的子进程,每个代表一个哲学家,这种并发的设计增加了程序的复杂性,但也提高了系统的效率。

实验的难点主要在于正确处理锁的逻辑,以及保证程序不出现僵尸进程。通过不断地思考和调试,我逐渐解决了这些问题,确保了程序的正确性和稳定性。实验文档中对于程序输出的简洁要求也锻炼了我的编程规范意识,使得输出信息更加清晰明了。

总的来说,通过这次实验,我不仅学到了并发进程同步的具体实现方式,还提高了对操作系统中进程控制的理解。同时,通过遇到问题并逐一解决,我培养了解决实际编程难题的能力。这次实验是我在学术和实践中的一次全面提升,让我更加深入地了解了操作系统的底层原理和并发编程的精髓。

posted @ 2024-04-28 09:11  hankeke303  阅读(154)  评论(0编辑  收藏  举报