进程间通信(InterProcess Communication,IPC):
进程通信中遇到的三个问题:
a) 进程之间如何进行信息的传递?
b) 多个进程在执行自己的核心代码时如何能够不相互影响?
c) 当进程之间出现相互依赖关系时,如何才能合理的调度进程的执行顺序!
1. 竞争情形:
当两个或多个进程同时读写某个共享资源的时候,程序运行的最终结果由各个进程的具体执行的情况所决定!
如何避免竞争情形的出现,那么我们首先引入关键代码区的定义:
程序中访问共享内存或其他共享资源的代码区被称为关键代码区!
如果我们可以保证在每一个时刻都没有多个
进程处于自己的关键代码区,那么我们当然就可以避免竞争情形的出现.
但为了能使并行的进程正确的进行合作并高效的利用共享资源,那么下面的四个条件应该的到满足:
- 没有两个进程会同时出现在它们的代码区中;
- 没有对CPU数量和速度的假设;
- 运行在关键代码区之外的进程不能阻塞其他进程;
- 没有进程可以永远等待进入其关键代码段中;
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算法是一个实现互斥锁的并发程序设计算法,可以控制两个进程访问一个共享的单用户资源而不发生访问冲突。
算法使用两个控制变量flag与turn.
其中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是同步信号量,负责进程之间的同步工作!