死鎖,可以定義爲一組競爭系統資源或者相互通信的進程,相互的“永久性“”的阻塞,若無外力作用,這組進程將永遠沒有辦法繼續執行。很遺憾,目前這種問題沒有一種有效的通用解決方案。
這裏對於死鎖的定義理解要注意死鎖的對象——一組進程,相互競爭。如果是一個進程可能長期或者永久性得不到執行,那麼這個進程是處於飢餓狀態而不是死鎖。同時,飢餓 ≠死鎖!,飢餓現象只是死鎖的一種結果,就是說由於死鎖的出現,導致了死鎖的進程餓死。但是飢餓現象不是一定由於死鎖導致的。比如前面的一些調度算法,也可能造成飢餓,因爲有些程序可能永遠得不到執行,但是不意味着它就死鎖了。用句現在流行的話來說它僅僅是“自閉”了而已。
哲學家進餐問題
在講PV操作的時候,特意沒講這個經典的問題,目的就是讓學到這裏的時候,對死鎖再加深認識,直接舉例子不如分析例子來的好。
問題描述:這個問題由偉大的計算機科學家dijkstra提出並解決。有5個哲學家,他們的生活方式是,思考,進餐。他們圍繞着圓桌而坐,桌子上有5雙筷子和5碗米飯,平時哲學家進行思考,飢餓的時候他拿起左右兩邊的筷子(一根一根的拿起),試圖進餐,進餐完畢後又陷入思考。哲學家只有同時拿到筷子才能進食。如圖:
我們按照之前的模板分析:
- 臨界資源分析:顯然筷子是臨界資源,因爲它一次只允許一個哲學家使用。
- 交互關係分析:這裏有5個哲學家,也就是有5個一樣的進程,他們對筷子的訪問是互斥的。他們之間沒有一定的先後順序,畢竟不知道誰會先餓。
- 思路分析:哲學家能吃到飯的前提是,他手裏有兩根筷子,而拿到筷子的前提是,他左右兩邊的哲學家,恰好在思考。
- 信號量設置分析:這裏筷子是臨界資源,每個筷子都是一個臨界資源,對臨界資源的訪問要以互斥的形式訪問,所以要設置5個互斥信號量,信號量較多的情況下,我們用數組表示會方便很多,比如 chopsticks[5] = {1,1,1,1,1,}.因爲沒有同步關係,因此我們也不用設置其他的資源信號量。
僞代碼如下:
//哲學家進餐問題
semaphore chopsticks[5] = {1,1,1,1,1};//定義互斥信號量數組
void philosopher(int i){ //第i個哲學家進程
while (true){
P(chopsticks[i]);//申請左邊的筷子
P(chopsticks[(i + 1)%5]);//申請右邊的筷子
.......
..進餐. //拿到了左右兩根筷子後,進餐
.......
V(chopsticks[i]);//放下左邊的筷子
V(chopsticks[(i + 1)%5);//放下右邊的筷子
.......
..思考. //吃完飯思考
.......
}
}
上面的代碼,解決了兩個相鄰的哲學家不會同時進餐的問題,但是思考這樣一個問題。假設所有的哲學家同一時間都餓了呢?我們分析一下,進程同時執行下面的語句:
P(chopsticks[i]);申請左邊的筷子
那麼,現在所有的哲學家手裏都有一根筷子,右邊的筷子已經被別人拿走了,所以誰也沒有辦法進食,每個人都希望右邊的人能放下手中的筷子,陷入了無休止的等待當中,哲學家遲早被餓死。這就是死鎖現象。這樣的理解很是深刻。
那麼先就事論事,先解決這個問題再說:
提供一種思路,既然這樣全都得餓死,那不如就先讓一個人吃完,吃完後把筷子讓出來。也就是說,只有當哲學家兩邊的筷子都能用的時候,才允許它吃飯。那麼具體怎麼實現呢?其實不難,我們只要在某個哲學家拿筷子的時候,其他的哲學家都不能拿筷子,也就是說取筷子這個動作是互斥的。於是有了下面的改進代碼:
//哲學家進餐問題
semaphore chopsticks[5] = {1,1,1,1,1};//定義互斥信號量數組
semaphore mutex = 1;//設置使用筷子的互斥信號量
void philosopher(int i){ //第i個哲學家進程
while (true){
P(mutex);//申請使用筷子操作
P(chopsticks[i]);//申請左邊的筷子
P(chopsticks[(i + 1)%5]);//申請右邊的筷子
.......
..進餐. //拿到了左右兩根筷子後,進餐
.......
V(chopsticks[i]);//放下左邊的筷子
V(chopsticks[(i + 1)%5];//放下右邊的筷子
V(mutex);//筷子使用完畢,放回原位
.......
..思考.
.......
}
}
死鎖產生的原因
死鎖產生的原因主要有四個方面:資源不足,進程推進順序非法,資源的使用。
- 資源不足:通常系統中擁有的不可剝奪的資源,數量不足以滿足多個進程的需要,使得進程在運行過程中因爲競爭資源而導致競爭資源陷入僵局,導致死鎖。(比如競爭打印機,剛剛哲學家問題的筷子)
- 進程推進順序非法:進程中申請和釋放資源的順序安排不當(如將生產者消費者之間的PV操作的順序顛倒)。如果進程P不是同時需要兩個資源,先使用一個再申請一個,這樣就不會產生死鎖。
- 資源的使用。系統中的資源主要分兩大類:可重複資源,可消耗資源。
可重複資源包括:處理機,I/O通道,設備文件,以及數據庫等等,這種資源,進程得到後,再釋放,供其他進程再次使用。
可消耗資源包括:中斷,信號量,緩衝區。當一個進程使用該資源後,便不復存在。
就生產者消費者問題而言,按照順序先P(mutex)也是基於此考慮的。
解決死鎖的方法是:預防,檢測和避免。(這個很重要,常識)
死鎖的必要條件
- 互斥。一段時間內,某種資源只能由一個進程佔有,此時其他要求使用該資源的進程只能阻塞。
- 請求與保持。當進程已經佔有了至少一個進程,又提出新的資源要求,而該被請求的資源又被其他進程佔用,此時進程阻塞,但是對它持有的資源,保持不放。
- 不可剝奪。進程已經獲得資源,在它使用完畢前不能被剝奪,只能使用完畢後自己釋放。
- 環路條件。存在一個與資源的環形鏈,設鏈中的每個進程都在等待一個被佔用的資源。(就是哲學家問題中的,每個哲學家都手持一根筷子的情況)。
一般情況下,前面三種情況都是合理的。進程間的互斥,是爲了保證再現性,請求與保持是正常的運行,對於一些資源,比如打印機,是不能剝奪使用的。
123是死鎖的必要條件,而4是123的潛在的結果。當前面的123某一種情況與4同時出現的時候,表明進程間發生了死鎖。
解決死鎖的方法是:預防,檢測和避免。(這個很重要,常識)
死鎖的預防
死鎖的預防可以從4個必要條件入手。
- 對於互斥,這是程序併發必不可少的一種機制,無法避免
- 對於請求與保持。出現問題的原因是因爲,運行過程中自己的資源不夠,而向外申請造成的。所以我們可以預先分配。即要求進程一次性請求完所有資源,若不能滿足,就阻塞這個進程。直到它所有的資源都滿足爲止。這就要求預知進程運行所需要的總資源數。
缺點:進程將會被延遲運行,資源被嚴重浪費。而且最重要的是,進程只有在運行的時候才知道需要哪些資源。 - 對於不可剝奪,最直接的方式就是剝奪阻塞進程的資源,但是代價太大,會導致進程無限期推遲運行。
- 對於環路,那麼破壞環路的條件。可以採用有序分配策略,
如果一個進程已經分配到了某種類型的資源,它接下來請求的資源,只能是那些排在這個資源之後的資源。(即按一定順序來申請資源)
死鎖的檢測
死鎖檢測算法主要是檢查系統中的進程是否有循環等待,將系統總的進程和資源的申請和分配描述成一幅有向圖,稱爲資源分配圖。通過檢查有向圖中是否有環來判斷死鎖的存在。
用方框代表資源,方框裏面的小圓圈代表的是資源數,P1,P2是進程。
從進程出的邊爲請求邊,從進程入的邊爲分配邊
那麼,採用拓撲排序的方式來判斷圖中是否有環。關於拓撲排序 看這個:數據結構——圖(9)——拓撲排序與DFS
死鎖定理:系統處於死鎖狀態等價於狀態S的資源分配圖是不可以被簡化的
死鎖的預防
銀行家算法(下篇詳細介紹)