進程喚醒與睡眠

注:本文主要參考自<<現代操作系統>>2.3.4, 2.3.5節

進程喚醒與睡眠

使用睡眠與喚醒避免忙等待

在前一節如何避免多進程(線程)因競爭條件引發的錯誤?,我們提出了集中能保證多個進程互斥訪問臨界區,我們所提出的解決方案均使用忙等待策略,即在進程等待進入臨界區時,其持續檢查,直到能夠進入臨界區爲止.我們能否在進程未滿足下一步工作條件時進入休眠狀態,當進程滿足下一步工作條件時,由其他進程喚醒該進程呢?如果可以實現這種方法,則我們可以避免進程的忙等待,從而節省CPU資源.

操作系統提供的sleep系統調用可以使當前進程進入休眠狀態,此時進程將被阻塞,進入阻塞狀態,CPU的佔有權被交出.如果當前某一進程達到喚醒條件,則可以執行wakeup(pid)喚醒編號爲pid的進程.

使用如下生產者-消費者問題解釋進程的睡眠與喚醒.爲簡化問題,我們假定只有一個生產者,一個消費者.如果當前用於存放待消費物品的空間有限,假定其一次最多存放N個物品.因此,如果當前空間滿,則生產者應當停止生產,進入睡眠狀態.當空間不滿時,再喚醒生產者.如果當前空間爲空,則消費者進程應當停止消費,進入休眠狀態,當空間不爲空時,再喚醒消費者進程.我們可以用如下僞代碼實現這一算法:

int N = 100;
int count = 0;

void producer(){
	int item = produce_item();
	if(count == N)
		sleep();
	count++;
	if(count == 1)
		wakeup(consumer);
}

void consumer(){
	if(count == 0)
		sleep();
	int item = remove_item();
	count--;
	if(count == N-1)
		wakeup(producer);
	counsume_item(item);
}

以上代碼初看起來沒有問題,當注意到生產者進程與消費者進程具有共享內存區,且對共享內存區變量的訪問並未進行有效保護.在一般情況下,兩個進程同時訪問共享內存區似乎不會有太大問題,例如假設此時count == 3, 此時如果消費者進程被執行,則count變爲2,假設此時還未進行實際消費,由於時鐘中斷,進程被切換到生成者進程,此時生產者添加生產item, count變爲3,隨後進程切換回消費者,完成實際的消費.注意到,雖然count變量的增加與實際物品的生產與消費被劃分爲兩個階段,但並未造成不良後果.

但如果是更爲特殊的情形,則可能就會有麻煩了.假設此時count==1, 消費者進程首先執行,假設消費者進程消費完後,count = 0, 消費者程序再次調用判斷count == 0,此時發生時鐘中斷,由於消費者進程被中斷,因此並未執行sleep()這條語句,即消費者進程還未進入休眠狀態.此時切換到執行生產者進程.生產者完成生產後,count變量變爲1,生產者隨後執行wakeup(counsumer)系統調用喚醒消費者進程.然而,由於此時消費者進程並未進入休眠狀態,因此當前信號將被忽略.等到再次切換會消費者進程時,消費者進程執行sleep()進入休眠狀態,但該進程永遠不會被喚醒了.因爲count變量此後會一直增加到N, 永遠不會再次等於1了.當count等於N時,生產者進程也進入睡眠狀態.由此,兩個進程均進入睡眠狀態,無法再次喚醒.

問題出在哪了?問題在於喚醒消費者進程的信號發送的太早了,此時消費者進程並未進入睡眠狀態,因此該信號被忽略了.如果我們在接收到喚醒信號時保存起來,當消費者進程準備進入休眠狀態時,我們檢查是否存在喚醒信號,如果有,則不進入休眠,並將當前喚醒信號清楚.否則,進入休眠狀態.如果我們有多個進程,那我們就需要爲每一進程保存可能接受到的喚醒信號,有沒有其他更合適的方法呢?

信號量

信號量是E.W.Dijkstra在1965年提出的一種方法,它使用一個整型變量來累計喚醒次數,供以後使用。在他的建議中引入了一個新的變量類型,稱作信號量(semaphore)。一個信號量的取值可以爲0(表示沒有保存下來的喚醒操作)或者爲正值(表示有一個或多個喚醒操作)。

Dijkstra建議設立兩種操作:down和up(分別爲一般化後的sleep和wakeup)。對一信號量執行down操作,則是檢查其值是否大於0。若該值大於0,則將其值減1(即用掉一個保存的喚醒信號)並繼續;若該值爲0,則進程將睡眠,而且此時down操作並未結束。檢查數值、修改變量值以及可能發生的睡眠操作均作爲一個單一的、不可分割的原子操作完成。保證一旦一個信號量操作開始,則在該操作完成或阻塞之前,其他進程均不允許訪問該信號量。這種原子性對於解決同步問題和避免競爭條件是絕對必要的。所謂原子操作,是指一組相關聯的操作要麼都不間斷地執行,要麼都不執行。原子操作在計算機科學的其他領域也是非常重要的。

確保信號量能正確工作,最重要的是要採用一種不可分割的方式來實現它。通常是將up和down作爲系統調用實現,而且操作系統只需在執行以下操作時暫時屏蔽全部中斷:測試信號量、更新信號量以及在需要時使某個進程睡眠。由於這些動作只需要幾條指令,所以屏蔽中斷不會帶來什麼副作用。如果使用多個CPU,則每個信號量應由一個鎖變量進行保護。通過TSL或XCHG指令來確保同一時刻只有一個CPU在對信號量進行操作。

up操作對信號量的值增1。如果一個或多個進程在該信號量上睡眠(在信號量爲0時調用了down操作),無法完成一個先前的down操作,則由系統選擇其中的一個(如隨機挑選)並允許該進程完成它的down操作。於是,對一個有進程在其上睡眠的信號量執行一次up操作之後,該信號量的值仍舊是0,但在其上睡眠的進程卻少了一個。信號量的值增1和喚醒一個進程同樣也是不可分割的。不會有某個進程因執行up而阻塞,正如在前面的模型中不會有進程因執行wakeup而阻塞一樣。

注意到,多個進程可以共用一個信號量.例如,在生產者-消費者問題中,假設存在多個生產者與多個消費者.則我們可以僅使用一個信號量來紀錄對消費者進程可能的睡眠與喚醒信號.假設當前信號量爲0,此時如果3個消費者進程先後嘗試執行,則其無法成功執行down操作,當前信號量上有三個進程進入睡眠狀態.此後,如果生產者進程被調用,則其在信號量上執行up操作,由於3個消費者進程在該信號量上被阻塞,因此隨機選擇其中一個進程喚醒.此時該信號量上的睡眠進程數變爲2.爲了表示生產者進程的睡眠與喚醒信號,我們需要使用另一個信號量.每次調用消費者進程時,在該信號量上執行up操作,每次調用生產者進程時,在該信號量上執行down操作.

基於信號量的生產者-消費者多進程解決方案

該解決方案使用了三個信號量:一個稱爲full,用來記錄充滿的緩衝槽數目;一個稱爲empty,記錄空的緩衝槽總數;一個稱爲mutex,用來確保生產者和消費者不會同時訪問緩衝區。full的初值爲0,empty的初值爲緩衝區中槽的數目,mutex初值爲1。供兩個或多個進程使用的信號量,其初值爲1,保證同時只有一個進程可以進入臨界區,稱作二元信號量(binary semaphore)。如果每個進程在進入臨界區前都執行一個down操作,並在剛剛退出時執行一個up操作,就能夠實現互斥。

#define N 100
typedef int semaphore

semaphore mutex 1;
semaphore full 0;
semaphore empty N;

void producer(){
	int item;
	item = produce_item();
	down(&empty);
	down(&mutex);
	insert_item(item);
	up(&full);
	up(&mutex);
}

void consume(){
	int item = remove_item();
	down(&full);
	down(&mutex);
	consume_item(item);
	up(&empty);
	up(&mutex);
}

信號量的另一種用途是用於實現同步(synchronization)。信號量full和empty用來保證某種事件的順序發生或不發生。在本例中,它們保證當緩衝區滿的時候生產者停止運行,以及當緩衝區空的時候消費者停止運行。這種用法與互斥是不同的。而信號量mutex在本例中的作用則是保證互斥.

注意觀察上面使用信號量的程序,如果我們在生產者進程中,將down(&mutex)放在down(&empty)前面執行可能會造成什麼問題?

假設當前緩衝區已滿,此時如果生產者進程執行down(&mutex)成功,則在執行down(&empty)時,會被阻塞.因此,其無法執行up(&mutex).這導致消費者進程無法執行down(&mutex).因此消費者進程無法消耗緩衝區中的內容.此時兩個進程均進入阻塞狀態,即死鎖.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章