首页 > 代码库 > 操作系统之进程篇(2)

操作系统之进程篇(2)

进程间通信(InterProcess Communication,IPC):

进程通信中遇到的三个问题:

   a) 进程之间如何进行信息的传递?

   b) 多个进程在执行自己的核心代码时如何能够不相互影响?

   c) 当进程之间出现相互依赖关系时,如何才能合理的调度进程的执行顺序!

1. 竞争情形:

   当两个或多个进程同时读写某个共享资源的时候,程序运行的最终结果由各个进程的具体执行的情况所决定!

如何避免竞争情形的出现,那么我们首先引入关键代码区的定义:

   程序中访问共享内存或其他共享资源的代码区被称为关键代码区! 如果我们可以保证在每一个时刻都没有多个

进程处于自己的关键代码区,那么我们当然就可以避免竞争情形的出现.

   但为了能使并行的进程正确的进行合作并高效的利用共享资源,那么下面的四个条件应该的到满足:

 

  • 没有两个进程会同时出现在它们的代码区中;
  • 没有对CPU数量和速度的假设;
  • 运行在关键代码区之外的进程不能阻塞其他进程;
  • 没有进程可以永远等待进入其关键代码段中;
 2. Mutual Exclusion With Busy Waiting:

 

   2.1 停止中断:

   实现进程之间互斥的最简单的一种方法就是:当一个进程进入自己的关键代码段之后,关闭所有的中断功能,当程序离开关键代码段之后,恢复这些中断功能。

显然这种方法是一个不好的方法,因为赋予用户进程关闭中断的能力并不是一件安全的事情,如果一个进程关闭中断之后不在打开这些中断,那么系统将会崩溃.

 

   2.2 锁变量:

   作为一种尝试,下面用一种软件解决方案:

   设置单个,共享的锁变量,初始化为0,表示没有进程进入关键代码段。当一个进程要进去自己的关键代码段是,首先对锁变量进行测试,如果变量为0表示当前没有

其他进程在其关键代码段中,那么该进程进入关键代码段中,并将锁变量置为1。进程执行完关键代码段之后,将锁变量恢复成0.

   但这个方法同样存在缺陷,当一个进程a检测到锁变量的值是0的时候,表明没有其他进程在其关键代码段中,在其将锁变量置为1之前,可能另外的一个进程b也检测到

锁变量是0,然后进程b被调度,执行,并将锁变量置为1。当进程a回来将锁变量重新置为1后,进程a同样也进入了关键代码区。这样冲突在所难免!

   上述现象的根本原因是无法保证对锁变量的原子访问! 

 

   2.3 strict alternation:

while (TRUE) {

while (turn != 0)

     ; /* wait  */

critical_section();
turn = 1;
non_critical_section();
}
while (TRUE) {
while (turn != 1)

     ; /* wait */

critical_section();
turn = 0;
non_critical_section();
}

 原理是用一个turn变量控制每个进程对关键代码段的访问,这种take turns的方式在一个进程的速度比另外一个进程缓慢的时候效果并不好。

 运行在关键代码区之外的进程不能阻塞其他进程!

 

   2.4 Peterson‘s Solution:

   Peterson算法是一个实现互斥锁并发程序设计算法,可以控制两个进程访问一个共享的单用户资源而不发生访问冲突。

   算法使用两个控制变量flagturn. 其中flag[n]的值为真,表示ID号为n的进程希望进入该临界区. 标量turn保存有权访问共享资源的进程的ID号.

//flag[] is boolean array; and turn is an integer

flag[0]   = false;
flag[1]   = false;
turn;

P0: flag[0] = true;
    turn = 1;
    while (flag[1] == true && turn == 1)
    {
        // busy wait
    }
    // critical section
    flag[0] = false;
    // end of critical section


P1: flag[1] = true;
    turn = 0;
    while (flag[0] == true && turn == 0)
    {
        // busy wait
    }
    // critical section
    flag[1] = false;
    // end of critical section

 上面的算法最复杂的情形莫过于两个进程同时要求进入关键代码段!还是将上面的代码修改一下为好:


 1 #define FALSE 0
 2 #define TRUE  1
 3 #define N     2          /* numbers of process */
 4  
 5 int turn;                /* whose turn is it? */
 6 int interested[N];       /* all values initially 0 */
 7 
 8 void enter_region(int process)  /* process 0 or 1 */
 9 {
10     int other;                  /* the number of the other process */
11     
12     other = 1 - process;        /* the opposite process */
13 
14     interested[process] = TRUE;
15     turn = process; 
16     if(turn == process && interested[other] == TRUE)  /* null statement */
17 }
18 
19 void leave_region(int process)
20 {
21    interested[process] = FALSE;   
22 }

 当process 1和process 2同时运行enter_region函数时,两个进程将interested[0,1]都设为1,但turn只能被设置成0或1,取决与执行turn = process;

语句的先后顺序,比如process 0先执行这条语句,那么最终turn将变成1,那么在process 1中将执行空循环,这样process 0就可以进入自己的关键代码段了!

 

   2.5 The TSL Instuction

   TSL(Test and Set Lock): 检查并设置;

   许多计算机有这样的TSL指令,这条指令读取内存中某个word的内容进入寄存器之中,并在内存中的这个地址上设置一个非0值!

   这样的一个测试并设置的过程是一个不可分割的原子操作!


1 enter_region:
2    tsl register, lock   | copy lock to register and set lock to 1
3    cmp register, #1     | was lock zero ?
4    jne enter_region     | if it was non zero, lock was set, so loop
5    ret                  | return to caller, critical region entered
6 
7 leave_region:
8     move lock,#0        | store a 0 in lock
9     ret                 | return to caller

 使用TSL方法的原其实是和锁变量的方法是相同的,只是在TSL方法用硬件手段为锁变量的访问和修改加上了原子性!

 一个进程应该合理调用enter_region和leave_region。

 

3. 进程的睡眠和唤醒:

    上述的方法都存在一个致命问题--busy waiting! 并有可能会出现priority inversion problem(权限倒置问题: 权限高的进程反而得不到想要的资源)。

 

    Priority inversion is a problem, not a solution. The typical example is a low priority process acquiring a resource that a high priority process needs, and then being preempted by a medium priority process, so the high priority process is blocked on the resource while the medium priority one finishes (effectively being executed with a lower priority)

 

   3.2 生产者--消费者 问题:

 1 #define N 100    /* number of slots in the buffer */

 2 int count = 0;   /* the initial number of items in  the buffer */
 3 
 4 void producer(void)
 5 {
 6    while(TRUE)   /* repeated forever */
 7    {
 8       produce_item();         /* generate next item */
 9       if(count == N) sleep(); /* if has no buffer slot, go to sleep */
10       enter_item();           /* put item in buffer */
11       count = count + 1;      /* increment count of items in buffer */
12       if(count == 1) wakeup(consumer);
13 /* if buffer is not empty! wake up the consumer */
14    }        
15 }
16 
17 void consumer(void)
18 {
19   while(TRUE)
20   {
21      if(count == 0) sleep();  /* if the buffer is empty, go to sleep */
22      remove_item();           /* take item out of buffer */
23      count = count - 1;       /* decrement count of items in buffer */
24      if(count == N - 1) wakeup(producer); /* if the buffer is not full, then wake up the producer */
25      consume_item();          /* print item */
26   }   
27 }

 上面的生产者消费者问题可能出现race condition!由于生产者和消费者对count的访问是没有限制的,考虑如下情形:

  • 开始时buffer为空,consumer读取count发现为0,这时调度器决定终止consumer进程的执行(注意,这是调度器的安排,consumer并没有进入sleep状态)
  • producer进程执行,在buffer中放置一件产品,将count加1,然后向consumer进程发送一个wakeup信号。
  • 但由于consumer进程并不是处于asleep状态的,所以consumer进程丢弃了刚才这个wakeup信号!
  • cosumer再次运行,看到上次读取的count值0,然后进入sleep状态!
  • 迟早producer将填满整个buffer,这样两个进程都进入sleep状态!

 

 

   3.3 信号量机制:

Semaphore分为单值和多值两种,前者只能被一个线程获得,后者可以被若干个线程获得。
以一个停车场是运作为例。为了简单起见,假设停车场只有三个车位,一开始三个车位都是空的。这时如果同时来了五辆车,看门人允许其中三辆不受阻碍的进入,然后放下车拦,剩下的车则必须在入口等待,此后来的车也都不得不在入口处等待。这时,有一辆车离开停车场,看门人得知后,打开车拦,放入一辆,如果又离开两辆,则又可以放入两辆,如此往复。
在这个停车场系统中,车位是公共资源,每辆车好比一个线程,看门人起的就是信号量的作用。
更进一步,信号量的特性如下:信号量是一个非负整数(车位数),所有通过它的线程(车辆)都会将该整数减一(通过它当然是为了使用资源),当该整数值为零时,所有试图通过它的线程都将处于等待状态。在信号量上我们定义两种操作: Wait(等待) 和 Release(释放)。 当一个线程调用Wait(等待)操作时,它要么通过然后将信号量减一,要么一直等下去,直到信号量大于一或超时。Release(释放)实际上是在信号量上执行加操作,对应于车辆离开停车场,该操作之所以叫做“释放”是因为加操作实际上是释放了由信号量守护的资源。
在java中,还可以设置该信号量是否采用公平模式,如果以公平方式执行,则线程将会按到达的顺序(FIFO)执行,如果是非公平,则可以后请求的有可能排在队列的头部。

      以上内容来自百度百科!

      使用信号量机制来解决生产者消费者问题:


  1 #define N 100   /* number of slots in the buffer */

 2 typedef int semaphore; /* semaphores are a special kind of int */
 3 semaphore mutex = 1;   /* controls access to critical section */
 4 semaphore empty = N;   /* controls empty buffer slots */
 5 semaphore full = 0;    /* controls full buffer slots */
 6 
 7 void producer(void)
 8 {
 9    int item;
10    while(TRUE)
11    {
12       produce_item(&item);  /* generate something to put in buffer */
13       down(&empty);
14       down(&mutex);
15       enter_item(item);
16       up(&mutex);
17       up(&full);
18    }   
19 }
20 
21 void consumer(void)
22 {
23    int item;
24    while(TRUE)
25    {
26       down(&full);
27       down(&mutex);
28       remove_item(&item);
29       up(&mutex);
30       up(&empty);
31       consume_item(&item);
32    }       
33 }

 这里分别设置了三个信号量,mutex信号量用来控制对buffer的互斥访问,full和empty用来进行buffer状态计数。

 这里mutex信号量为互斥信号量,用来保证多个进程对资源的互斥访问,而full和empty是同步信号量,负责进程之间的同步工作!