一、進程的並發執行
1. 並發是所有問題產生的基礎。
2. 進程的特征:
並發:進程執行時間斷性的,執行速度是不可預測的;
共享:進程/線程之間的制約性;
不確定性:進程執行的結果和執行的相對速度有關,所以是不確定的;
3. 舉例:
1) 銀行業務系統:進程的關鍵活動出現交叉;
2) get-->copy-->put;
並發環境下執行,時間上,速度上都不確定,執行次序的不同會導致不同的結果。
4. 並發環境的制約關系:進程前趨圖
二、進程互斥(MUTUAL EXCLUSIVE)
1. 競爭條件:兩個或多個進程在讀寫某些共享數據時,而最后的結果取決於進程運行的精確的時序;
2. 進程互斥:由於各個進程要求使用共享資源,而這些資源需要排他性的使用,各個進程之間競爭使用這些資源,這一關系稱為進程互斥;
3. 臨界資源:系統中某些資源一次只希望允許一個進程使用,稱這樣的資源為臨界資源或者互斥資源或共享變量;
4. 臨界區(互斥區):多個進程中對某個臨界資源實施操作的程序片段;
5. 臨界區的使用原則:
1)如果沒有進程在臨界區,想進入臨界區的進程就可以進入;
2)不允許兩個進程同時處於臨界區內;
3)臨界區外運行的進程不得阻塞其他進程進入臨界區;
4)不得使進程無限期等待進入臨界區;
6. 實現進程互斥的方法:軟件方案、硬件方案;
三、進程互斥的軟件解法
軟件方法:保護臨界區
正確算法:
1. DEKKER算法
2. PETERSON算法(更好)
四、進程互斥的硬件解法
用特殊指令來達到保護臨界區的目的;
1. 開關中斷指令:
1)簡單、高效
2)代價高,限制CPU的並發能力
3)不適用於多處理器
4)適用於操作系統本身,不適用於用戶程序
2. 測試並加鎖指令:
3. 交換指令:
4. 忙等待:進程在得到臨界區訪問權限之前,持續做測試而不做其他事情;(單CPU不提倡)
自旋鎖:(多處理器情況),忙等待就比較好,因為切換的開銷是很大的;
五. 進程同步
1. 進程同步(synchronization):指系統中多個進程發生的時間存在某種時序關系,需要相互合作,共同完成某一項任務;
進程之間的協作關系;
2. 生產者消費者問題(又稱為緩沖區問題):
生產者進程-->緩沖區-->消費者進程
只能由一個生產者或者消費者對緩沖區進行操作;
避免忙等待:
睡眠與喚醒操作(原語):sleep() 與 wakeup() 操作
3. SPOOLing系統:生產者消費者問題
六. 信號量及P、V操作(一種經典的進程同步機制):
1. 1965荷蘭學者Dijkstrat提出;P與V分別是荷蘭語 test (proberen) 和 increment (verhogen);
信號量:
一個特殊變量,用於在進程間傳遞信息的一個整數值;
定義如下:
struct semaphore{
int count;
queueType queue;
}
信號量說明:semaphore a;
對信號量可以實施的操作:初始化、P操作和V操作;
2. P(down,semWait)、V(up,semSignal)操作
P操作相當於申請資源,而V操作相當於釋放資源。所以要記住以下幾個關鍵字:
P操作----->申請資源
V操作----->釋放資源
P操作:信號量值減1;
然后判斷信號量值是否小於0,如果小於0,則將該進程設置為阻塞狀態;將該進程插入相應的等待隊列s.queue末尾;
否則實施P操作的進程就繼續執行;
V操作:信號量值加1;
如果信號量值<=0,則說明原來信號量上有進程在等待,所以喚醒s.queue中的第一個等待進程;改變其為就緒態,並將其插入就緒隊列;
否則,實施v操作的進程就繼續執行;
3. 說明:
- P操作和V操作是原語操作;
- 信號量上定義了三個操作:初始化(非負數)、P操作和V操作;
- 最初提出的是二元信號量(解決互斥),最后推廣到一半信號量(多值)或計數信號量解決同步;
4. 在理解了PV操作的的含義后,就必須講解利用PV操作可以實現進程的兩種情況:互斥和同步。
1)一個生產者,一個消費者,公用一個緩沖區。
可以作以下比喻:將一個生產者比喻為一個生產廠家,如伊利牛奶廠家,而一個消費者,比喻是學生小明,而一個緩沖區則比喻成一間好又多。
第一種情況,可以理解成伊利牛奶生產廠家生產一盒牛奶,把它放在好又多一分店進行銷售,而小明則可以從那里買到這盒牛奶。只有當廠家把牛奶放在商店里面后,小明才可以從商店里買到牛奶。所以很明顯這是最簡單的同步問題。
解題如下:
定義兩個同步信號量:
empty——表示緩沖區是否為空,初值為1。
full——表示緩沖區中是否為滿,初值為0。
生產者進程
while(TRUE){
生產一個產品;
P(empty);
產品送往Buffer;
V(full);
}
消費者進程
while(TRUE){
P(full);
從Buffer取出一個產品;
V(empty);
消費該產品;
}
2)一個生產者,一個消費者,公用n個環形緩沖區。
第二種情況可以理解為伊利牛奶生產廠家可以生產好多牛奶,並將它們放在多個好又多分店進行銷售,而小明可以從任一間好又多分店中購買到牛奶。同樣,只有當廠家把牛奶放在某一分店里,小明才可以從這間分店中買到牛奶。
不同於第一種情況的是,第二種情況有N個分店(即N個緩沖區形成一個環形緩沖區),所以要利用指針,要求廠家必須按一定的順序將商品依次放到每一個分店中。緩沖區的指向則通過模運算得到。
解題如下:
定義兩個同步信號量:
empty——表示緩沖區是否為空,初值為n。
full——表示緩沖區中是否為滿,初值為0。
設緩沖區的編號為1~n-1,定義兩個指針in和out,分別是生產者進程和消費者進程使用的指針,指向下一個可用的緩沖區。
生產者進程
while(TRUE){
生產一個產品;
P(empty);
產品送往buffer(in);
in=(in+1)mod n;
V(full);
}
消費者進程
while(TRUE){
P(full);
從buffer(out)中取出產品;
out=(out+1)mod n;
V(empty);
消費該產品;
}
3)一組生產者,一組消費者,公用n個環形緩沖區
第三種情況,可以理解成有多間牛奶生產廠家,如蒙牛,達能,光明等,消費者也不只小明一人,有許許多多消費者。不同的牛奶生產廠家生產的商品可以放在不同的好又多分店中銷售,而不同的消費者可以去不同的分店中購買。當某一分店已放滿某個廠家的商品時,下一個廠家只能把商品放在下一間分店。所以在這種情況中,生產者與消費者存在同步關系,而且各個生產者之間、各個消費者之間存在互斥關系,他們必須互斥地訪問緩沖區。
解題如下:
定義四個信號量:
empty——表示緩沖區是否為空,初值為n。
full——表示緩沖區中是否為滿,初值為0。
mutex1——生產者之間的互斥信號量,初值為1。
mutex2——消費者之間的互斥信號量,初值為1。
設緩沖區的編號為1~n-1,定義兩個指針in和out,分別是生產者進程和消費者進程使用的指針,指向下一個可用的緩沖區。
生產者進程
while(TRUE){
生產一個產品;
P(empty);
P(mutex1);
產品送往buffer(in);
in=(in+1)mod n;
V(mutex1);
V(full);
}
消費者進程
while(TRUE){
P(full);
P(mutex2);
從buffer(out)中取出產品;
out=(out+1)mod n;
V(mutex2);
V(empty);
}
5. 用P、V操作解決進程間互斥問題:
- 分析並發進程的關鍵活動,划定臨界區;
- 設置信號量mutex,初值為1;
- 在臨界區之前實施P(mutex);
- 在臨界區之后實施V(mutex);
七. 生產者消費者問題
生產者、緩沖區、消費者
生產者-消費者(producer-consumer)問題,也稱作有界緩沖區(bounded-buffer)問題,兩個進程共享一個公共的固定大小的緩沖區。
其中一個是生產者,用於將消息放入緩沖區;另外一個是消費者,用於從緩沖區中取出消息。
問題出現在當緩沖區已經滿了,而此時生產者還想向其中放入一個新的數據項的情形,其解決方法是讓生產者此時進行休眠,等待消費者從緩沖區中取走了一個或者多個數據后再去喚醒它。
同樣地,當緩沖區已經空了,而消費者還想去取消息,此時也可以讓消費者進行休眠,等待生產者放入一個或者多個數據時再喚醒它。
聽起來好像蠻對的,無懈可擊似的,但其實在實現時會有一個競爭條件存在的。為了跟蹤緩沖區中的消息數目,需要一個變量 count。
如果緩沖區最多存放 N 個消息,則生產者的代碼會首先檢查 count 是否達到 N,如果是,則生產者休眠;否則,生產者向緩沖區中放入一個消息,並增加 count 的值。
消費者的代碼也與此類似,首先檢測 count 是否為 0,如果是,則休眠;否則,從緩沖區中取出消息並遞減 count 的值。同時,每個進程也需要檢查是否需要喚醒另一個進程。
代碼可能如下:
// 緩沖區大小 #define N 100 int count = 0; // 跟蹤緩沖區的記錄數 /* 生產者進程 */ void procedure(void) { int item; // 緩沖區中的數據項 while(true) // 無限循環 { item = produce_item(); // 產生下一個數據項 if (count == N) // 如果緩沖區滿了,進行休眠 { sleep(); } insert_item(item); // 將新數據項放入緩沖區 count = count + 1; // 計數器加 1 if (count == 1) // 表明插入之前為空, { // 消費者等待 wakeup(consumer); // 喚醒消費者 } } } /* 消費者進程 */ void consumer(void) { int item; // 緩沖區中的數據項 while(true) // 無限循環 { if (count == 0) // 如果緩沖區為空,進入休眠 { sleep(); } item = remove_item(); // 從緩沖區中取出一個數據項 count = count - 1; // 計數器減 1 if (count == N -1) // 緩沖區有空槽 { // 喚醒生產者 wakeup(producer); } consume_item(item); // 打印出數據項 } }
看上去很美,哪里出了問題,這里對 count 的訪問是有可能出現競爭條件的:緩沖區為空,消費者剛剛讀取 count 的值為 0,而此時調度程序決定暫停消費者並啟動執行生產者。生產者向緩沖區中加入一個數據項,count 加 1。
現在 count 的值變成了 1,它推斷剛才 count 為 0,所以此時消費者一定在休眠,於是生產者開始調用 wakeup(consumer) 來喚醒消費者。但是,此時消費者在邏輯上並沒有休眠,所以 wakeup 信號就丟失了。
當消費者下次運行時,它將測試先前讀到的 count 值,發現為 0(注意,其實這個時刻 count 已經為 1 了),於是開始休眠(邏輯上)。而生產者下次運行的時候,count 會繼續遞增,並且不會喚醒 consumer 了,所以遲早會填滿緩沖區的,
然后生產者也休眠,這樣兩個進程就都永遠的休眠下去了。
使用信號量解決生產者-消費者問題
首先了解一下信號量吧,信號量是 E.W.Dijkstra 在 1965 年提出的一種方法,它是使用一個整型變量來累計喚醒的次數,供以后使用。在他的建議中,引入了一個新的變量類型,稱為信號量(semaphore).
一個信號量的取值可以為 0(表示沒有保存下來的喚醒操作)或者為正值(表示有一個或多個喚醒操作)。
並且設立了兩種操作:down 和 up(分別為一般化后的 sleep 和 wakeup,其實也是一般教科書上說的 P/V 向量)。對一個信號量執行 down 操作,表示檢查其值是否大於 0,如果該值大於 0,則將其值減 1(即用掉一個保存的喚醒信號)並繼續;
如果為 0,則進程休眠,而且此時 down 操作並未結束。另外,就是檢查數值,修改變量值以及可能發生的休眠操作都作為單一的,不可分割的 原子操作 來完成。
下面開始考慮用信號量來解決生產者-消費者問題了,不過在此之前,再次分析一下這個問題的本質會更清晰點:問題的實質在於發給一個(尚)未休眠進程(如上的消費者進程在只判斷了 count == 0 后即被調度出來,還未休眠)的 wakeup 信號丟失
(如上的生產者進程在判斷了 count == 1 后以為消費者進程休眠,而喚醒它)了。如果它沒有丟失,則一切都會很好。
#define N 100 // 緩沖區中的槽數目 typedef int semaphore; // 信號量一般被定義為特殊的整型數據 semaphore mutex = 1; // 控制對臨界區的訪問 semaphore empty = N; // 計數緩沖區中的空槽數目 semaphore full = 0; // 計數緩沖區中的滿槽數目 /* 生產者進程 */ void proceducer(void) { int item; while(1) { item = procedure_item(); // 生成數據 down(&empty); // 將空槽數目減 1 down(&mutex); // 進入臨界區 insert_item(item); // 將新數據放入緩沖區 up(&mutex); // 離開臨界區 up(&full); // 將滿槽的數目加 1 } } /* 消費者進程 */ void consumer(voi) { int item; while(1) { down(&full); // 將滿槽數目減 1 down(&mutex); // 進入臨界區 item = remove_item(); // 從緩沖區中取出數據項 up(&mutex); // 離開臨界區 up(&empty); // 將空槽數目加 1 consumer_item(item); // 處理數據項 } }
該解決方案使用了三個信號量:一個為 full,用來記錄充滿的緩沖槽的數目,一個為 empty,記錄空的緩沖槽總數,一個為 mutex,用來確保生產者和消費者不會同時訪問緩沖區。
mutex 的初始值為 1,供兩個或者多個進程使用的信號量,保證同一個時刻只有一個進程可以進入臨界區,稱為二元信號量(binary semaphore)。
如果每一個進程在進入臨界區前都執行一個 down(...),在剛剛退出臨界區時執行一個 up(...),就能夠實現互斥。
另外,通常是將 down 和 up 操作作為系統調用來實現,而且 OS 只需要在執行以下操作時暫時禁止全部中斷:測試信號量,更新信號量以及在需要時使某個進程休眠。
這里使用了三個信號量,但是它們的目的卻不相同,其中 full 和 empty 用來同步(synchronization),而 mutex 用來實現互斥。
七. 用信號量解決讀者/寫者問題: