清華大學操做系統公開課(八)-同步互斥

1.什麼是互斥

在計算機執行過程當中,對於多個任務,它們共享着一個資源,要求對該資源的存取過程是排他的。c++

2.爲何要有互斥

不考慮SMP狀況,僅分析單CPU狀況,由於SMP只不過是更復雜的一種狀況,原理相似。
有以下代碼片斷,其中share_data是一個全局變量。編程

int share_data = 0;

void foo(void)
{
	share_data++;		//寫共享資源
	show share_data;	//讀共享資源
}

2.1線程間

若是兩個線程都執行上述片斷,執行過程以下圖所示:

首先是Thread A開始執行,此時share_data爲0,執行完①後,若是執行流程不被打斷,則繼續往下走,最終show share_data獲得的數據應該是1。可是當Thread A執行完步驟①後,CPU發生了調度,切到Thread B上執行②,而share_data對於兩個線程是共享的,因此Thread B此刻看到的share_data是通過了一次自增的,爲1。最終執行完流程③,④。Thread A看到的結果是share_data自增了兩次,與預期不符。併發

2.2線程與中斷間

還一種狀況是代碼片斷分別在一個線程和中斷中:
性能

過程和上面相似,只不過執行流程是被中斷打斷的。測試

3.併發和競態

以上多個執行流在一個時間段併發(Concurrency)執行,而且圍繞共享資源進行訪問致使了競態(Race Conditions)。就是一種你爭我奪的狀態。而這個共享的資源就是他們爭奪的對象,能夠是(全局的變量,同一個硬件資源,文件系統上的一個文件等)。優化

與併發經常一塊兒提到的另外一個詞叫並行,並行指同一個時刻,同時進行,例如SMP上多個CPU執行多個線程這種狀況。ui

3.1如何解決競態

以上看到了競態帶來了咱們預期以外的效果,按照咱們程序的設計,對於一個功能模塊,一樣的輸入應該獲得一致的輸出才行。考慮生活中一個這樣的問題:A和B住一塊兒,冰箱裏面沒麪包了就要去買,可是咱們要避免兩我的重複買麪包這種狀況。如何解決?操作系統

  • 方案一
    打開冰箱,發現沒麪包了,留張紙條貼在冰箱上,買回麪包放進去,撕去紙條。用代碼表示:
do
{
	if (noNote)
	{
		if (noBread)
		{
			leave Note;
			buy bread;
			remove Note;
		}
	}
}while (1);

能解決問題嗎?不能。假設A看到 if (noNote) ---> if (noBread),恰好有別的事打斷了他,A出去了,這時B進來,也看到了if (noNote) ---> if (noBread),而後恰巧B也被打斷了,A此時進來,因而他繼續前面被打斷的工做,留下紙條,買回麪包,撕去紙條,走了。而後B回來了,繼續前面被打斷的工做,留下紙條,買回麪包,撕去紙條,走了。這時會發現A和B都買了麪包。線程

  • 方案二
    以上問題看上去好像是沒提早貼好紙條致使,那若是先貼紙條呢
do{
	leave Note;
	if (noNote)
	{
		if (noBread)
		{
			buy bread;
			remove Note;
		}
	}
}while(1);

分析這個過程看獲得,雙方留下紙條後,進去檢查if (noNote)都是不成立的,這樣,兩我的都不會去買麪包,若是這種巧合一直按照這種順序發生,那麼永遠不會有人買麪包。設計

  • 方案三
    標籤加以區別:
do{
	leave my Note;
	if (no Other's Note)
	{
		if (noBread)
		{
			buy Bread;
		}
		remove my Note;
	}
}while(1);

顯而易見,依舊是不行的。

  • 方案四
do{
	leave Note1;
	while (is Note2 Exist)					
	{										
		do nothing;							
	}
	
	if (noBread)
	{
		buy Bread;
	}
	remove Note1;
}while(1);
do{
	leave Note2;
	if (noNote1)
	{
		if(noBread)
		{
			buy Bread;
		}
	}
	remove Note2;
}while(1);

這個方案可行嗎?可行。可是明顯看的出和前面3個方案的不一樣,這裏用了兩段不一樣的代碼。此方案有以下缺點:

  • 一樣是2我的買麪包,上面三種方案,購買流程一套代碼便可實現,而此狀況須要2套代碼。
  • 若是處理線程數超過2個,處理邏輯的複雜度會呈指數級增加。
  • 而且第一段處理有一個死循環,若是下面一段的處理比較耗時,則上一段的死循環會持續好久,致使比較高的CPU佔用。
  • 方案五
    既然方案四已經否認了,綜合前三個解決方法來看,能夠看出,主要是由於放下紙條和檢查紙條這個過程被打斷了,2個動做能夠拆分爲2次完成。若是經過一些手段把這兩個動做綁定在一塊兒,看看會是怎麼樣。
do
{
	while(check_and_leave());	//執行不可打斷

	if (noBread)
	{
		buy Bread;
	}

	remove Note;
}while(1);

check_and_leave()裏面的邏輯是:

if (noNote)
{
	leave Note;
	return FALSE;
}
else
{
	return TRUE;
}

這樣就能夠解決競態問題了。只要實現check_and_leave()執行動做的不可打斷就能夠了。

4.臨界區

4.1概念

enter section;
	critical section;
exit section;
	remainder section;

臨界區(critical section):

  • 任務執行時,須要互斥的一段區域。

進入臨界區(enter section):

  • 進入臨界區須要先檢查是否有人已進入臨界區。
  • 若是可進入臨界區,設置進入標誌。

退出臨界區(exit section):

  • 清除進入臨界區標誌。

4.2訪問規則

  • 空閒則進入
    沒有任務進入了臨界區,任何任務均可以進入。
  • 繁忙則等待
    有任務進入臨界區,其餘任務都得等待。
  • 有限等待
    未進入臨界區的任務不能無限等待。
  • 讓權等待(可選)
    未進入臨界區的任務,應釋放CPU使用權(如切換到阻塞態)。

4.3如何實現臨界區的訪問

4.3.1禁止中斷

禁止中斷至關因而硬件方法實現,在第三節的分析可知,臨界區出現了競態主要是由於任務調度引發,或者中斷引發。而任務調度也是由中斷方法實現(如時間片,超時則引起調度,由時鐘中斷致使),因此禁止了中斷,能夠實現臨界區的訪問。

local_irq_save(unsigned int flags);
critical section;
local_irq_restore(unsigned int flags);

進入臨界區(enter section):

  • 禁止全部中斷,保存CPSR。

退出臨界區(exit section):

  • 使能中斷,恢復CPSR。

缺點:

  • 禁止中斷後,執行的任務將沒法響應任何外部的輸入,例如信號,中斷,一直執行下去。
  • 若是臨界區執行耗時較長,那麼對系統的性能有很大的影響。
  • 對SMP狀況是沒法處理的,由於禁中斷只會禁止當前程序運行的CPU上的中斷。

由於這些侷限性,要當心使用禁用條件中斷。

4.3.2軟件方法

4.3.3高級抽象

基於硬件原子性操做的高級抽象方法。硬件提供了一些原語,像中斷禁用、原子操做指令等,操做系統在此基礎上提供更高級的編程抽象來簡化並行編程,例如鎖、信號量,這些方式是從硬件原語中構建的。

那麼如何實現這種抽象呢?

Test And Set

bool Test_And_Set(bool* flag)
{
	bool rv = *flag;
	*flag = TRUE;
	return rv;
}

這是一條機器指令,這條機器指令完成了一般操做的讀寫兩條機器指令的工做,完成了三件事情:

  • 從內存中讀取值
  • 測試該值是否爲1(而後返回真或假)
  • 內存值設置爲1

Exchange

void Exchange(bool *a, bool *b)
{
	bool tmp = *a;
	*a = *b;
	*b = tmp;
}

雖然這兩個指令看起來由幾條小指令組成,可是它已經被封裝成了一條機器指令,這就意味着它在執行的時候不會被打斷,不容許出現中斷或切換,這就是機器指令的語義保證。在此基礎上完成互斥。

使用Test_And_Set實現自旋鎖

這裏看到若是鎖狀態value爲1,表示臨界區如今被人佔用,已經上鎖了,這時調用Lock::Acquire()不能獲得鎖,那麼它會在這裏死循環,不斷測試value的值。這種空轉全用來消耗任務的時間片,顯然是能夠有優化空間的。例如發現獲取鎖失敗,則將任務置爲睡眠狀態,掛入到等待隊列。

忙等和非忙等各有各的使用場景,若是臨界區比較簡單,執行不會消耗多少時間,那麼用忙等更合適,由於非忙等是須要進行任務調度的,調度出去,而後再調度進來,這個調度的開銷不能白白浪費在適合忙等的臨界區。

使用exchange實現鎖:

class Lock{
	int value = 0;
}

Lock::Acquire(){
	int key = 1;
	while(1 == key){
		exchange(lock, key);
	}
}

Lock::Reease(){
	value = 0;
}

不論是Test_And_Set仍是Exchage他們的終極目的其實都是在一個機器指令裏面對一個變量實現:

  • 讀取當前值
  • 將變量設置爲1
相關文章
相關標籤/搜索