進程間通信
- 一個進程如何把信息傳遞給另一個。
- 確保兩個或更多的進程在關鍵活動中不會出現交叉。
- 確保進程運行的正確順序。
競爭條件
兩個或多個進程讀寫某些共享數據,而最後的結果取決於進程運行的精確時序,稱爲競爭條件。
互斥訪問(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)
串聯連接的一組數據處理元件,其中一個元件的輸出是下一個元件的輸入。