操作系统之进程篇(4)--经典进程间通信(IPC)问题

1. 哲学家进餐问题:

问题描述:
五个哲学家在一个圆桌上进餐,每人的面前放了一盘意大利面,两个盘子之间有一个叉子,但是由于盘子里面的面条十分光滑,需要两个叉子才能进行就餐行为。餐桌的布局如下图所示:

假设哲学家的生活中只有两个活动:吃饭和思考[吃饭维持自身之生存,思考探究生存之意义],当然这样的哲学家在现实之中是不存在的。当一个哲学家在殚精竭虑之时,饥饿感随之而来,这是他会拿起左右手边的两个叉子来想享用这俗世之中的美味。酒足饭饱之后,又"躲进小楼成一统,管他春夏与秋冬"去了。问题是:
怎样才能保证每个哲学家都能拿到两个筷子而不会出现死锁的情形呢?[一个典型的死锁情形是: 每个哲学家同时拿起右手边的叉子,都得不到左手边的叉子。]

上述死锁情形可以通过下面的代码描述:


 1 #define N 5                        /* number of philosophers */
2
3 void philosopher(int i) /* i: philosopher number: from 0 to 4 */
4 {
5 while(TRUE):
6 {
7 think(); /* philosppher is thinking */
8 take_fork(i); /* take the left fork */
9 take_fork((i+1) % N); /* take right fork; % is a modulo operator */
10 eat(); /* yum-yum, spaghetti */
11 put_fork(i); /* put back the left fork on the tabel */
12 put_fork((i+1) % N); /* put back the right fork on the table */
13 }
14 }

那么如何解决这个问题呢? 我们经过一番思考得到下面一些方案:

方案1: 当一个哲学家拿起左手边的叉子的时候,判断他是否可以拿到右手边的叉子;
如果右手边的叉子正被别人使用着,那么他就放下左手边的叉子,等待一段时间之后,重复上面的过程。[这个方案仍然解决不了死锁问题,如果五个哲学家同时执行上述过程,都得不到叉子,然后放下,等待相同的一段时间后在重复上述过程......然后是没完没了的重复:Until
the end of the world(直到世界末日)]

方案2 :
将上述方案中等待一定量的时间改为等待随机一段时间。[看上去这个方案可以解决死锁问题,但是我们不能依赖这个随机值,况且计算机世界里面哪有什么绝对的随机数呢?我们不能因为一件事发生的概率极小而断定这件事不会发生,这在赌徒们的牌桌上是可以存在的,但作为一个理性的人,这个念头是荒谬可笑的,就像是将头埋在沙子里的鸵鸟。试想一段控制核电站堆芯工作的程序使用上述策略,那么人类的灭亡也就不远了。]

方案3:
在上面的代码的think()语句作为一个关键代码段,用一个互斥信号量(mutex)来控制每个哲学家进程对关键代码段的访问。[从理论上来说,这个方案是可行的;但从实际效率上考虑,这个方案是不合理的:一段时间内至多只有一个哲学家在进餐。]

方案4:


 1 #define N            5             /* number of philosophers */
2 #define LEFT (i-1) % N /* number of i‘s left neighbor */
3 #define RIGHT (i+1) % N /* number of i‘s right neighbor */
4 #define THINKING 0 /* philosopher is thinking */
5 #define HUNGRY 1 /* philosopher is trying to get forks */
6 #define EATING 2 /* philosopher is eating */
7
8 typedef int semaphore; /* semaphores are a special kind of int */
9 int state[N]; /* array to keep track of every philospphers‘ state */
10 semaphore mutex = 1; /* mutual exclusion for critical regions */
11 semaphore s[N]; /* one semaphore per philosopher */
12
13 void philosopher(int i)
14 {
15 while(TRUE)
16 {
17 think();
18 take_forks(i);
19 eat();
20 put_forks(i);
21 }
22 }
23
24 void take_forks(int i)
25 {
26 down(&mutex);
27 state[i] = HUNGRY;
28 test(i);
29 up(&mutex);
30 down(&s[i]); [如过没有请求到叉子,那么阻塞]
31 }
32
33 void put_forks(int i)
34 {
35 down(&mutex);
36 state[i] = THINKING;
37 test(LEFT(i));
38 test(RIGHT(i));
39 up(&mutex);
40 }
41
42 void test(i)
43 {
44 if(state[i] == HUNGRY && state[LEFT(i)] != EATING && state[RIGHT(i)] != EATING)
45 {
46 state[i] = EATING;
47 up(&s[i]);
48 }
49 }

[注意这里并没有将一个哲学家所能执行的所有动作都放在一个关键代码段中,而是用互斥信号量控制take_forks和get_forks过程,以保证每次只有一个哲学家在申请叉子或释放叉子。但可以有多个哲学家处于EATING的状态。]

2. 读者和写者问题:

哲学家进餐问题描述的是多个进程对有限资源的互斥访问问题,另外一个问题是"读者--写者"问题,描述的是多个进程对数据访问的问题。

要求: 可以有多个用户同时读取数据,但一个时刻只能有一个用户在更新数据,并且在更新数据期间,不容许其他用户更新或读取数据。

下面是解决读者--写者问题的一个方案:


 1 typedef int semaphore;
2 semaphore mutex = 1; /* [控制对读者计数器rc的访问] */
3 semaphore db = 1; /* [控制对数据的访问] */
4 int rc = 0; /* [读者数计数器,初始化为0] */
5
6 void reader(void)
7 {
8 while(true) /* ["久到离谱,一直停不下来"] */
9 {
10 down(&mutex); /* [对读者数计数器互斥访问] */
11 rc = rc + 1; /* [读者计数器+1] */
12 if (rc == 1) down(&db); /* [当有用户在读取数据时,没有人可以修改数据] */
13 up(&mutex);
14
15 read_data(); /* [读取数据] */
16
17 down(&mutex);
18 rc = rc - 1; /* [读取数据完毕,离开] */
19 if(rc == 0) up(&db); /* [如果当前没有用户读取数据,那么容许其他用户的修改] */
20 up(&mutex);
21 use_data_read();
22 }
23 }
24
25 void writer(void)
26 {
27 while(true) /* [you konw that] */
28 {
29 think_up_data(); /* [准备好要更新的数据] */
30 down(&db);
31 write_data();
32 up(&db);
33 }
34 }

当然,这个方案也存在一些问题:当有读者在读取数据,并且有其他读者源源不断的到来的时候,写者进程将永远处于阻塞状态。[当每2秒出现一个读者,而每个读者的平均读取时间是5秒时就会出现这个情形。]

  解决这个问题的一种方案是当写者进程出现时,写者进程之后到来的读者进程都被阻塞,当先前读者读取完毕后写者就可以修改数据了。这个方案虽然可以保证写者不会处于饥饿状态,但却以破坏系统中程序的并发性为代价。

另一种解决方案参见"读者--写者"问题的提出者Courtois的论文: cs.nyu.edu/~lerner/spring12/Read04-ReadersWriters.pdf;

3. 睡觉的理发师问题:

问题描述:
一个理发店有1个理发师,一把理发椅,和有n把椅子的休息区。如果没有客户过来理发,这个理发师久躺在理发椅上呼呼大睡[工作无聊乎?];
当一个客户到来时,它必须侥幸这个理发师[如果这个家伙在现实生活中估计早就被炒鱿鱼了!]。当理发师理发的时候如果由其他客户到来,那么这个客户可以选择在休息区等待(当休息区未满的时候);
也可以选择离开(当休息区没有空闲座位的时候)。问题是:如何编写客户和理发师代码而不出现竞争情形?下面是这个问题的解决方案:


 1 #define       CHAIRS        5   /* [等待区座椅数目] */
2
3 typedef int semaphore;
4
5 semaphore customers = 0; /* [客户信号量:初始化为0] */
6 semaphore barbers = 0; /* [理发师信号量: 初始化为0] */
7 semaphore nutex = 1; /* [互斥信号量: 初始化为1] */
8
9 void barber(void)
10 {
11 while(true)
12 {
13 down(&customers); /* [如果customers = 0 那么理发师就回去睡觉] */
14 down(&mutex); /* [获取对waiting变量的访问权限] */
15 waiting = waiting - 1;
16 up(&barbers); /* [一个理发师来给用户理发] */
17 up(&mutex);
18 cut_hair();
19 }
20 }
21
22 void customer(void)
23 {
24 down(&mutex);
25 if(waiting < CHAIRS) /* [如果空闲区没有椅子,那么客户离开] */
26 {
27 waiting = waiting + 1; /* [增加等待理发的客户数目] */
28 up(customers); /* [唤醒理发师] */
29 up(mutex);
30 down(barbers); /* [使用一个理发师] */
31 get_haircut();
32 }
33 else
34 {
35 up(&mutex);
36 }
37 }

理发师问题可以做这样的类比:
操作系统中提供服务的进程有限[理发师],而请求服务的进程无限[顾客]。以优先之服务供给无限之需求,其中公平和效率兼顾的考量不可缺少!

Ok! This is the end of this artile! Thank you very much for reading it! Good
luck!

操作系统之进程篇(4)--经典进程间通信(IPC)问题

时间: 2024-11-05 10:26:53

操作系统之进程篇(4)--经典进程间通信(IPC)问题的相关文章

操作系统之进程篇(2)

进程间通信(InterProcess Communication,IPC): 进程通信中遇到的三个问题: a) 进程之间如何进行信息的传递? b) 多个进程在执行自己的核心代码时如何能够不相互影响? c) 当进程之间出现相互依赖关系时,如何才能合理的调度进程的执行顺序! 1. 竞争情形: 当两个或多个进程同时读写某个共享资源的时候,程序运行的最终结果由各个进程的具体执行的情况所决定! 如何避免竞争情形的出现,那么我们首先引入关键代码区的定义: 程序中访问共享内存或其他共享资源的代码区被称为关键代

操作系统之进程篇(1)

1.进程介绍: 1.1 进程模型: 进程是一个程序的实际执行,包含了程序计数器的状态,寄存器和变量等等! 程序可以看成是一个状态的序列,程序在不同时刻呈现出不同的状态,而这种状态的前后交替过程可以看成是程序的执行过程.概念上来说,每个程序有自己的虚拟CPU,但在现实中CPU在不同的进程间来回切换,又称这种切换为伪并行! 进程和程序差别看似微小,实际上却是十分精妙; 可以将计算机执行程序的过程看成一次有趣的烹饪过程.食谱就是程序,厨师就是CPU,而食材是输入,得到的输出是鲜美可口的美食. 当厨师在

操作系统之进程篇(3)

1. 信号量机制的缺陷问题: 在上面的生产者消费者实例中,信号量的工作机制如下(我们以生产者的代码为例): 1 down(&empty); 2 down(&mutex); 3 enter_item(item); 4 up(&mutex); 5 up(&full); 如果交换1号和2号语句,变成: 1 down(&mutex); 2 down(&empty); 那么可能会出现下面的情形: mutex变成0,此时empty == 0,那么生产者阻塞; 此时消费者

操作系统之进程篇1

1.进程的出现,让我们需要对进程进行分离存储,而有了内存管理:需要不同进程有条不紊的往前推进而有了进程调度. 2.为什么要有进程?什么是进程? 为了实现程序的并发执行,我们发明了进程.一个程序加载到内存后就变成了进程. 3.注意不是所有进程都一定要终结,实际上,许多系统进程是不会终结的,除非强制终止或关闭计算机. 4.什么时间造成进程的产生? 1)系统初始化:在一个系统初始化时,将有许多进程产生,产生的这些进程是系统正常运行必不可少的. 2)执行进程创立程序:如双击了一个可执行文件. 3)用户请

操作系统之进程篇2

1.进程调度主要要解决的问题是什么? 任意时刻到底由哪个进程执行,那些不执行.正在进展中的程序使用CPU的模式有3种:程序大部分时间在CPU上执行(CPU导向,又称计算密集型程序):程序大部分时间在进行输入输出(I/O导向,又称输入输出密集型程序):程序介入前两种模式之间(平衡型程序). 2.进程调度的目标? 达到极小化平均响应时间,极大化系统吞吐率,保持系统各个功能部件均处于繁忙的状态,并提供某种貌似公平的机制. 3.调度算法有哪些?各算法的利弊? 1)先来先服务FCFS(First Come

操作系统---进程篇

进程(process)就是操作系统的灵魂,一个软件的程序通常就是由若干进程组成的.现在计算机一般采用多道程序设计和多处理机设计(所谓的4核.八核其实就是指多个处理机). 一.并行和并发 并行:就是由多个CPU情况下,多道程序可以在同一时刻依靠不同CPU运行,注意是同一时刻,而不是时间间隔,这就是并行性. 并发:现在我们假设只有一个CPU,那么多道程序要运行,必然在同一时刻只有一个才可以占用CPU,而计算机让CPU在同一时间段内快速地在多道程序间交换,注意这里是同一时间间隔,这样就造成了一种伪并行

进程篇(1: 进程运行环境)--请参照本博客“操作系统”专栏

2014年5月30日  下午1:40:59 1. Unix 进程执行环境: 1.1 终止处理程序: ISO C 规定,一个程序可以登记多达32个函数,这些函数将由exit自动调用.我们称这些函数为终止处理程序(exit handler),并调用atexit函数来登记这些函数.该函数的原型如下: 1 #include <stdlib.h>2 3 int atexit(void (*function)(void)); exit调用这些终止程序的顺序与他们登记时的顺序相反(先登记后调用).同一个函数

进程篇(4: 基本进程控制:其他相关控制)--请参照本博客“操作系统”专栏

1. 更改进程的用户ID和组ID:为什么我们要更改用户ID和组ID的呢? 在UNIX系统中,特权是基于用户和组ID的.当用户需要增加特权,或要访问某个当前没有能力访问的文件时,我们需要更改自己的权限,以让新的ID具有合适的特权或访问权限.与此类似,当程序需要降低其特权或阻止对某些资源的访问时,也需要跟换用户ID或组ID;一般而言,在设计应用程序时,我们总是试图使用"最小特权"模型.依照此模型,我们的程序应当值具有为完成特定的任务所需要的最小特权. NAME getuid, geteui

进程篇(2: C程序的存储空间布局)--请参照本博客“操作系统”专栏

1.  C程序的存储空间布局: C 程序由下面几个部分组成: 正文段(即是代码段): 这是由CPU执行的机器指令部分.通常,正文段是可以共享的,并常常是可读的,以防止程序因为意外原因而修改自身的代码! 初始化数据段(即数据段): 它包含了程序中需要明确的赋初值的变量. 非初始化数据段(bss段):在程序开始执行之前,内核将此段中的数据初始化为0或空指针. 栈.自动变量以及每次函数调用时所需保存的信息都存放在此段中.每次调用函数时,返回地址以及调用者的环境信息(如某些寄存器的值)都存放在栈中.然后