操作系统之进程篇(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! 

 

 

 

 

 

相关内容