好像是dekker算法的小歷史

實在是無奈,OS老師一定要求自學這篇博文的內容。

大神,我就在此處全篇引用你的代碼啦,包涵。

可能內容有點冗雜,可以分版本閱讀。

以下正文

………………………………………………………………………………

對於每份代碼,要求對代碼寫註釋,寫原理,還寫缺陷。

對於圖示的一些說明:

其中的interrupt是中斷的意思,是從一個程序切換到另一個程序去執行。

其中的critical section,是臨界區的意思。

版本一:

int flag = 0;//全局變量,表示臨界區是否被使用
void process() {
	...
	while(flag != 0) {//表示當前有程序訪問臨界區,則當前程序等待
		/*do nothing*/
	}
	flag = 1;//標記當前程序進入臨界區
	/*訪問臨界區*/
	flag = 0;//訪問完成之後,釋放臨界區資源
	...
}

版本一的原理:

從臨界區角度出發,考慮到臨界區只有兩種狀態(佔用,空閒),用一個全局變量,表示這兩種狀態(對應程序的第一行)。

當臨界區處於佔用狀態時,讓試圖進入臨界區的程序等到(對應程序中的while循環)。等待到臨界區空閒,則按下面步驟執行。

當臨界區處於空閒狀態時,讓試圖進入臨界區的程序直接進入,進入之前將臨界區狀態標記爲佔用(對應程序中flag = 1),在臨界區內執行程序,

執行完成之後,釋放臨界區,使臨界區處於空閒狀態(對應程序中的flag = 0),讓其他程序訪問臨界區。

版本一的缺陷:

由於中斷執行的不確定性,可能會發生如下情況

很顯然,當程序0判斷臨界區是否爲空閒時,發現空閒同時發生中斷,執行程序1並又判斷臨界區是否爲空閒,此時當然也判斷爲空閒;

之後就是兩個程序先後將臨界區標記爲佔用,兩個程序又先後進入臨界區執行若干週期。

這個不滿足忙則等待,也就是不能控制只用一個程序佔用臨界區執行。版本一因此也會發生重大的錯誤。

版本二:

int number = 0;//全局變量,表示當前那個程序佔用臨界區
void process0(){
	...
	while(number != 0) {//對於0號程序而言,如果發現其他程序佔用臨界區,則等待
		/*do nothing*/
	}
	number = 0;//將臨界區標記爲0號程序使用
	/*訪問臨界區*/
	number = 1;//將臨界區標記其他程序使用
	... 
}
void process1(){//對於此處的代碼,只是對象發生改變,思想不變。
	...
       while(number != 1){
		 /*do nothing*/
	}
 	number = 1;
	/*訪問臨界區*/
	number = 0;
	...
}

版本二原理:

依舊是從臨界區角度出發,並細化了版本一中全局變量的含義,即由原來單一的臨界區資源是否被佔用,進一步表示爲那個程序佔用臨界區(對應程序第一行)。

當程序0試圖進入臨界區時,發現已經有其他程序編號佔用臨界區,則等待(對應程序中的while循環)至其他程序退出臨界區,將臨界區分配給它之後,按以下步驟;

當程序0試圖進入臨界區時,發現當前臨界區正被其佔用,則將臨界區標記爲自己的程序編號,並訪問臨界區,訪問完成之後,將臨界區分配給其他程序。

版本二缺陷:

我們現在考慮這樣一個情況,比如說當前number的值爲0,程序0中需要訪問臨界區的代碼相對整個程序而言,位置比較靠後,而程序1中需要訪問的代碼卻相對靠前;

也就是當程序1試圖進入臨界區時,程序1發現其他程序正在佔用臨界區,則只能等待;

事實是卻是這樣的,程序0還未進入臨界區,

也就是臨界區空閒,但是對於程序1而言是被佔用的。

這就違背了空閒則入,這勢必會影響整個系統的效率。

如果存在極端情況,比如說number值一直是0,然而程序0一直不訪問臨界區,哈哈哈哈(原諒我笑一下,C語言中的死循環聽過麼?)。那麼程序1真的要等到海枯石爛了。

這就不能做到有限等待啦。(用戶體驗那麼糟糕的系統,如果有一定是我寫出來的)

版本三:

int flag[2] = {0, 0};//全局變量,表示每一個程序是否佔用臨界區
void process0(){
	...
	while(flag[1] != 0) {//當程序0試圖進入臨界區時,判斷程序1是否佔用臨界區;如果佔用,則等待
		/*do nothing*/
	}
	flag[0] = 1;//程序0標記佔用臨界區,之後訪問
	/*訪問臨界區*/
	flag[0] = 0;//程序0標記釋放臨界區。
	...
}
void process1(){//思想相同,對象不同
	...
	while(flag[0] != 0) {
		/*do nothing*/
	}
	flag[1] = 1;
	/*訪問臨界區*/
	flag[1] = 0;
	...
}

版本三的原理:

從上面兩個版本看來,臨界區自顧自己的感受,不考慮程序本身處境。

這次從程序的角度看,程序是否佔用臨界區狀態也是兩種(佔用,釋放),每個程序各有標識(對應程序第一行)。

當程序0試圖進入臨界區的時候,判斷是否有其他程序正在佔用臨界區,如果佔用則等待,直到其他程序釋放臨界區(對應程序中的while循環)。釋放之後,按以下步驟。

當程序0判斷髮現其他程序並未佔用臨界區,則將自己的狀態標記爲佔有臨界區,之後訪問臨界區,之後釋放臨界區資源。

版本三的缺陷:

同樣的,對於先判斷臨界區是否被佔用,後改變狀態的思路來說,都有一個致命的缺陷。那就是在中斷的幫助下,繞過等待,各自進入同時(宏觀上)臨界區。

同版本一,由於中斷的不確定性,兩個程序有在臨界區會師成功,儘管這是我們所不想看到的,但是還是發生了。

先判斷後標記的思路,經過兩次嘗試,都有一個共同且致命的bug,不滿足忙則等待。

版本四:

int flag[2] = {0, 0};//全局變量,表示某個程序是否佔用臨界區
void process0(){
	...
	flag[0] = 1;//表明程序0試圖進入臨界區的意圖
	while(flag[1] != 0) {//判斷是否有其他程序使用,如果有則等待
		/*do nothing*/
	}
	/*訪問臨界區*/
	flag[0] = 0;//釋放臨界區
	... 
}
void process1(){//思路相同,對象不同
	...
	flag[1] = 1;
	while(flag[0] != 0) {
		/*do nothing*/ 
	}
	/*訪問臨界區*/
	flag[1] = 0;
	...
}

版本四的原理:

依舊從程序角度出發,因爲版本三的思路不正確,此處改用,先標記後判斷的方法(有點類似版本二),還是保留版本三的數據結構。

當程序0試圖進入程序時,先表明意圖(對應程序中的flag[0] = 1;)

然後判斷其他程序是否佔用臨界區(對應程序中的wheil循環),如果有其他程序佔用臨界區,則等待至其釋放臨界區。釋放之後按以下步驟。

然後訪問臨界區,訪問完成之後釋放臨界區資源。

版本四的缺陷:

此處你即將瞭解到,中斷是如何一次又一次巧妙的阻礙我們走向成功的(真是個小妖精)。

如果中斷髮生的恰到時候,那麼flag[0]和flag[1]都將被標記爲1,也就是都表示對方在佔用臨界區,哈哈哈(C語言的死循環)

這樣的一個壞結果是,發生死鎖現象。這個結果的確挺糟糕的。

版本五:

int flag[2] = {0, 0};//全局變量,表示某個程序是否佔用臨界區
void process0(){
	...
	flag[0] = 1;//先表明意圖,希望進入臨界區
	while(flag[1] != 0) {//判斷是否有其他程序佔用臨界區
		flag[0] = 0;//如果有其他程序佔用臨界區,則退讓並等待。
		/*程序暫停一個隨機時間*/
		flag[0] = 1;//在隨機時間之後,重新表明進入臨界區的想法
	}
	/*訪問臨界區*/
	flag[0] = 0; //釋放臨界區
	...
}
void process1(){//此處依舊不做多餘描述
	...
	flag[1] = 1;
	while(flag[0] != 0){
		flag[1] = 0;
		/*程序暫停一個隨機時間*/
		flag[1] = 1;
	}
	/*訪問臨界區*/
	flag[1] = 0;
	... 
}

版本五的原理:

在版本四的基礎上又做了改進,具體如下。

當其他程序佔用時,將自己的意圖暫時收起來,避免了版本四的死鎖現象。也就是從程序角度上看是對程序增加了退讓等待的屬性。

程序0先表明希望進入程序的意圖,

判斷是否有其他程序佔用臨界區,若有,則退讓並等待一些時間(while循環中的flag[0] = 0;),之後重新表明意圖(while循環中的flag[0] = 1;),直到其他程序釋放臨界區。釋放之後,按以下步驟。

訪問臨界區,訪問完之後釋放臨界區。

版本五的缺陷:

雖然避免了死鎖的出現,但是如果程序之間退讓過於默契,那麼仍然會出現程序之間相互等待的情況,也會導致系統性能的降低,甚至出現長時間的等待。

版本六(dekker's algorithm):

int flag[2] = {0, 0};//全局變量,表示某個程序是否佔用臨界區
int number = 0;//全局變量,表示當前臨界區被那個程序佔用
void process0(){
	...
	flag[0] = 1;//表明試圖佔用臨界區
	while(flag[1] != 0) {//判斷其他程序,是否佔用
		flag[0] = 0;//若佔用,則退讓
		if(number != 0) {//通過臨界區判斷,當前是否空閒
			while(1) {//臨界區並非空閒,則當前程序等待
				/*do nothing*/
			}
		}
		flag[0] = 1;//申請佔用
	}
	/*訪問臨界區*/
	number = 1, flag[0] = 0;//將臨界區狀態分配給其他程序,當前程序釋放臨界區
	... 
}
void process1(){//由於代碼思想相同,此處不做贅述
	...
	flag[1] = 1;
	while(flag[0] != 0) {
		flag[1] = 0;
		if(number != 1) {
			while(1) {
				/*do nothing*/
			}
		}
		flag[1] = 1;
	}
	/*訪問臨界區*/
	number = 0, flag[1] = 0;
	... 
}

版本六的原理:

單從程序看來也是行不通的,版本六則將臨界區和程序兩者結合起來。通過程序主動申請佔用臨界區,發現對方佔用時主動退讓,同時利用臨界區只能有一個程序被佔用的特點來決定兩個同時退讓的程序更優先佔用臨界區,避免了死鎖和長時間等待的出現。

當程序0試圖進入臨界區時(對應程序中第5行),

判斷其他程序是否進入臨界區(對應程序中第6行),若有,則退讓(對應第7行),

通過臨界區判斷(對應第8行),臨界區被誰佔用,若有其他程序佔用,則等待。等待結束,則按以下驟。

重新標記臨界區被程序0佔用,由於此時程序1釋放臨界區(分兩種情況考慮,一:在版本五的情況下,兩者相互退讓,程序1釋放臨界區;二:程序1正常訪問臨界區後退出)

所以程序0可以訪問臨界區,之後釋放臨界區。

版本六的缺陷:

該版本是解決了前五個版本的所有問題,但是從原理分析這個來看,考慮情況極多,代碼也相對上面各版本複雜。

同時while循環的引入,會佔用CPU資源,出現忙等待現象。

該算法對於多處理機就顯得不適用。

綜述:

無論是版本一還是版本三,通過判斷臨界區是否被佔用還是程序是否佔用臨界區,都不能達到互斥的目的;

對於版本二和版本四,通過預先判斷臨界區被哪個程序佔用還是判斷哪個程序正在佔用臨界區,都可能發生死鎖或長時間等待。

對於版本五,理所當然的放棄了對版本一和版本三的治療,轉而對版本四做了改進。意識到版本四的互不相讓,帶來的死鎖,也就有了相互之間的退讓的版本五。

可惜過猶不及,退讓的太默契也不是好事。這個時候,再次從臨界區這個被佔用者角度出發,利用其唯一佔用性,調解過於退讓的局面出現。

dekker算法做到了合適的調解了程序之間的關係,即不但能達到互斥,也能在相互退讓時給出決策,避免死鎖。


囉裏囉嗦的寫了那麼多字,有不當之處,請指出,給初學者一個學習的機會。

發佈了48 篇原創文章 · 獲贊 10 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章