进程间通信
- 一个进程如何把信息传递给另一个。
- 确保两个或更多的进程在关键活动中不会出现交叉。
- 确保进程运行的正确顺序。
竞争条件
两个或多个进程读写某些共享数据,而最后的结果取决于进程运行的精确时序,称为竞争条件。
互斥访问(mutual exclusion)
临界区
我们把对共享内存进行访问的程序片段称作临界区域(critical region)。如果我们能够适当地安排,使得两个进程不可能同时处于临界区中,就能够避免竞争条件。
尽管这样的要求避免了竞争条件,但它还不能保证使用共享数据的并发进程能够正确和高效地进行协作。对于一个好的解决方案,需要满足以下4个条件:
- 任何两个进程不能同时处于临界区。
- 不应对 CPU 的速度和数量做任何假设。
- 临界区外运行的进程不得阻塞其他进程。
- 不得使进程无限期等待进入临界区。
忙等待的互斥
- 屏蔽中断
在单处理器系统中,最简单的方法是使每个进程在刚刚进入临界区后立即屏蔽所有中断,并在就要离开之前再打开所有中断。屏蔽中断后,时钟中断也被屏蔽。而 CPU 只有发生时钟中断或其他中断时才会进行进程切换,这样,在屏蔽中断之后 CPU 将不会被切换到其他进程。于是,一旦某个进程屏蔽中断之后,它就可以检查和修改共享内存,而不必担心其他进程介入。
但是把屏蔽中断的权力交给用户进程是不明智的。但是对内核来说,当它在更新变量或列表的几条指令期间将中断屏蔽是很方便的。所以结论是:屏蔽中断对于操作系统本身而言是一项很有用的技术,但对于用户进程则不是一种合适的通用互斥机制。
- 锁变量
设想有一个共享(锁)变量,其初始值为0。当一个进程想进入其临界区时,它首先测试这把锁。如果该锁的值为0,则该进程将其设置为1并进入临界区。若这把锁的值已经为,则该进程将等待直到其值变为0。
但是,这样还是会造成竞争条件。
- 严格轮换法
整型变量 turn,初始值为0,用于记录轮到哪个进程进入临界区,并检查或更新共享内存。开始时,进程0检查 turn,发现其值为0,于是进入临界区。进程1也发现其值为0,所以在一个等待循环中不断测试 turn,看其值何时变为1。
存在问题:忙等待(用于忙等待的锁,称为自旋锁,spin lock),两个进程速度不匹配,进程0在进程1在非临界区时被阻塞(违反了条件3)。
- Peterson 解法
#define FALSE 0
#define TRUE 1
#define N 2 /*number of processes*/
shared int turn; /*whose turn is it?*/
shared int interested[N]; /*all values initially 0*/
void enter_region(int process)
{
int other;
other=1-process;
interested[process]=TRUE;
turn=process;
while(turn == process && interested[other] == TRUE);
}
void leave_region(int process) {
interested[process]=FALSE;
}
- TSL 指令,测试并枷锁(test and set lock)
需要硬件支持的一种方案:TSL RX, LOCK。它将一个内存字 lock 读到寄存器 RX 中,然后在该内存地址上存一个非零值。
enter_region:
TSL REGISTER,LOCK /复制锁到寄存器并将锁设为1/
CMP REGISTER,#0 /若锁不是0则循环/
JNE enter_region
RET
leave_region:
MOVE LOCK,#0
RET
一个可替代 TSL 的指令是 XCHG,它原子性地交换了两个位置的内容。
enter_region:
MOVE REGISTER,#1
XCHG REGISTER,LOCK /交换寄存器与锁变量的内容/
CMP REGISTER,#0 /若锁不是0则循环/
JNE enter_region
RET
leave_region:
MOVE LOCK,#0
RET
- 总结
- 忙等待
- 优先级倒置(priority inversion):在某一时刻,Low 处于临界区中,此时 High 变到就绪态。现在 High 开始忙等待,但是由于 High 优先级高,所以 Low 不会被调度也就无法离开临界区,所以 High 将永远忙等待下去。
同步
信号量(semaphore)
- Down,P 操作:若其值大于0,则将其减1并继续;若该值为0,则进程将休眠,而且测试 down 操作并未结束。
- Up,V 操作:对信号量的值增1。如果一个或多个进程在该信号量上睡眠,无法完成先前的 down 操作,则由系统选择其中的一个并允许该进程完成它的 down 操作。于是 up 操作之后,该信号量的值仍旧是0,但在其上睡眠的进程却少了一个。
信号量解决生产者-消费者问题:
#define N 100 /*number of slots in the buffer*/
typedef int semaphore;
semaphore mutex=1;
semaphore empty=N;
semaphore full=0;
void producer(void){
int item;
while(TRUE){
produce_item(&item);
down(&empty);
down(&mutex);
enter_item(item);
up(&mutex);
up(&full);
}
}
void consumer(void){
int item;
while(TRUE){
down(&full);
down(&mutex);
remove_item(&item);
up(&mutex);
up(&empty);
consume_item(item)
}
}
互斥量(mutex)
如果不需要信号量的计数能力,可以使用信号量的一个简化版本,称为互斥量(mutex)。由于互斥量在实现时既容易又有效,所以互斥量在实现用户空间线程包时非常有用。
互斥量是一个可以处于两态之一的变量:解锁和加锁。
用户级线程包的 mutex_lock 和 mutex_unlock 的代码:
mutex_lock:
TSL REGISTER,MUTEX
CMP REGISTER,#0
JZE ok
CALL thread_yield ;如果互斥信号不为0,则调度另一个线程,稍后再尝试
JMP mutex_lock
ok: RET
mutex_unlock:
MOVE MUTEX,#0
RET
mutex_lock 的代码与上面 TSL 中 enter_region 的代码类似。但是当后者在进入临界区失败时,会始终重复测试锁(忙等待),而由于时钟超时的作用,会调度其他进程运行。但在用户线程中,没有时钟会停止运行时间过长的线程,所以前者需要主动放弃 CPU 给另一个线程,这样就没有忙等待。
条件变量(condition variable)
互斥量可以允许或阻塞对临界区的访问,而条件变量则允许线程由于一些未达到的条件而阻塞。
条件变量与互斥量经常一起使用。这种模式用于让一个线程锁住一个互斥量,然后当它不能获得它期待的结果时等待一个条件变量。最后另一个线程会向它发信号,使它可以继续执行。wait(condition,mutex) 原子性地调用并解锁它持有的互斥量,所以互斥量也是 wait(condition,mutex) 的参数之一。
但是条件变量不会像信号量那样存在内存中,如果将一个信号传递给一个没有线程在等待的条件变量,那么这个信号就会丢失。所以必须小心使用以防丢失信号。
管程(monitor)
管程是一种高级同步原语。是一个由过程、变量及数据结构等组成的一个集合,它们组成一个特殊的模块或软件包。进程可在任何需要的时候调用管程中的过程,但它们不能再管程之外声明的过程中直接访问管程内的数据结构。
管程有一个很重要的特性,即任一时刻管程中只能有一个活跃进程,这一特性使管程能有效地完成互斥。而进入管程时的互斥由编译器负责,一般的解决方法是引入条件变量,wait 和 signal 操作。
Java 中可以使用 synchronized ,wait 和 notify 来实现管程,没有内嵌的条件变量,而且方法 wait 会被中断,需要显式表示异常处理。
消息传递(message passing)
消息传递这种进程间通信的方法使用两条原语 send(des,&msg) 和 receive(src,&msg),它们像信号量而不像管程,是系统调用而不是语言成分。send 向一个给定的目标发送一条消息,receive 从一个给定的源接收一条消息,如果没有消息可用,则接收者可能阻塞,直到一条消息到达,或者带着一个错误码立即返回。
屏障(barrier)
用于进程组,实现除非所有进程都准备就绪进入下一个阶段,否则任何进程和都不能进入。可以通过在每个阶段的结尾安置屏障(barrier)来实现。当一个进程到达屏障时,它就被屏障阻拦,直到所有进程都到达该屏障为止。屏障可以用于一组进程同步。
信号量集合(semaphore set)
事件计数器(event counter)
经典的 IPC 问题
哲学家就餐问题
哲学家就餐问题对于互斥访问有限资源的竞争问题(如I/O设备)一类的建模过程十分有用。
#define N 5
#define LEFT (i+N-1)%N
#define RIGHT (i+1)%N
#define THINKING 0
#define HUNGRY 1
#define EATING 2
typedef int semaphore;
int state[N];
semaphore mutex=1;
semaphore s[N]; // 每个哲学家一个信号量
void philosopher(int){
while(TRUE){
think();
take_forks(i);
eat();
put_forks(i);
}
}
void take_forks(int i){
down(&mutex);
state[i]=HUNGRY;
test(i); // 尝试获取两把叉子
up(&mutex);
down(&s[i]); // 如果得不到所需要的叉子则阻塞
}
void put_forks(int i){
down(&mutex);
state[i]=THINKING;
test(LEFT); // 检查左边的邻居现在可以吃吗
test(RIGHT);
up(&mutex);
}
void test(int i){
if(state[i]==HUNGRY && state[LEFT]!=EATING && state[RIGHT]!=EATING){
state[i]=EATING;
up(&s[i]);
}
}
读者-写者问题
读者-写者问题为数据库访问建立了一个模型。多个进程同时读数据库是可以接受的,但是如果一个进程正在更新(写)数据库,则所有的其他进程都不能访问该数据库,即使读操作也不可以。
typedef int semaphore;
semaphore mutex=1; // 控制对 rc 的访问
semaphore db=1; // 控制对数据库的访问
int rc=0; // 正在读或者即将读的进程数目
void reader(void){
while(TRUE){
down(&mutex);
rc=rc+1; // 现在多了一个读者
if(rc==1) down(&db); // 如果这是第一个读者
up(&mutex);
read_data_base();
down(&mutex);
rc=rc-1; // 现在少了一个读者
if (rc==0) up(&db); // 如果这是最后一个读者
up(&mutex);
use_data_read();
}
}
void writer(void){
while(TRUE){
think_up_data();
down(&db);
write_data_base();
up(&db);
}
}
睡眠的理发师问题
有一个理发师,有一个理发椅,5个等候椅,如果没有顾客,则理发师睡觉,如果有顾客,则叫醒理发师;理发师理发时,如果有顾客过来,且有等候椅,则坐下来等候;如果没有等候椅,则离开。
#define CHAIRS 5
typedef int semaphore;
semaphore customers=0;
semaphore barbers=0;
semaphore mutex=1;
int waiting =0;
void barber(void){
while(TRUE){
down(&customers);
down(&mutex);
waiting=waiting-1;
up(&barbers); // 如果有顾客,理发师醒来
up(&mutex);
cut_hair();
}
}
void customer(void){
down(&mutex);
if(waiting<CHAIRS){
waiting=waiting+1;
up(&customers);
up(&mutex);
down(&barbers);
get_haircut();
}else{
up(&mutex);
}
}
通信
共享内存系统(shared-memory system)
可以由多个程序同时访问的存储器,旨在提供它们之间的通信或避免冗余副本。
消息传递系统(message passing system)
#define N 100
void producer(void){
int item;
message m;
while(TRUE){
item=prodece_item();
receive(consumer,&m);
build_message(&m,item);
send(consumer,&m);
}
}
void consumer(void){
int item,i;
message m;
for(i=0;i<N;i++) send(producer,&m);
while(TRUE){
receive(producer,&m);
item=extract_item(&m);
send(producer,&m);
consume_item(item);
}
}
流水线系统(pipeline system)
串联连接的一组数据处理元件,其中一个元件的输出是下一个元件的输入。