進程和線程相關問題

操作系統基本特徵

1.併發:操作系統調控多個進程輪流使用計算機資源,不同進程來回切換的速度特別快,看起來就像是在“同時"運行一樣。
2.共享:操作系統使多個進程共享計算機資源,每個進程在運行的時候都認爲自己獲得了cpu,內存,I/O等資源。但實際上可能是不同進程在內存中有不同的獨立的一塊空間供其訪問,看上去就像是不同進程共享資源一樣。這種共享稱爲同時共享。不同進程之間除了有各自獨立的空間,可能有彼此公共的內存空間來用於進程間的交流。爲了使交流不出差錯,一般操作系統規定,在一個進程訪問公共空間直到訪問結束爲止,另一個進程不能訪問此空間。這種共享稱爲互斥共享。
3.虛擬:從前面兩個特徵能看出,操作系統爲每個進程或者用戶分配計算機資源,這讓每個進程或者用戶感覺自己好像獨立擁有一臺計算機。這種把一臺計算機虛擬成多臺計算機的特性稱爲虛擬。
4.異步:操作系統使程序併發運行,不同程序輪流使用cpu,進程A運行一會,進程B運行一會,進程C運行一會,進程A再運行一會……這樣每個進程在實際是“走走停停”,每次“走”多久,“停”多久都是由操作系統決定的。所以即使是同一個進程,同樣的運行環境,從“微觀”角度上看,它的運行過程可能每次都不一樣。但是從"宏觀"角度,即運行結果每次都是一樣的。

多道程序設計和分時系統

多道程序設計是爲了提高cpu的利用率而出現的,他在進程需要等待某個任務時(例如I/O)會切換到內存中的另一個進程,等到第一個程序完成等待再切換回來。而分時系統會主動在各個程序之間切換,不會等到進程運行結束在換到下一個進程。
舉個例子:進程A是聊天,進程B是處理文件,進程C是大數據計算。在多道程序設計會在A等待聊天輸入時切換到B進程處理文件,等到A開始輸入時,再切換回來運行A。而分時系統會在A等待聊天輸入時,切換到進程B,運行一會再切換到C,並在BC間來回切換運行,直到A開始輸入時,再切換到A。
簡單來說,多道程序設計是被動在進程之間切換,而分時系統不僅會被動切換還會主動切換進程。

進程,程序,線程相關問題

進程的組成:

  1. 程序代碼
  2. 程序數據
  3. 程序計數器
  4. 寄存器,堆,棧
  5. 系統資源(內存,文件,I/O等)

注:CPU資源不屬於系統資源,進程當然會佔用CPU資源,但是人們更關心進程所佔用的系統資源,所以沒有把CPU資源寫入進程的組成。

進程與程序的區別

  1. 進程是動態的,程序是靜態的。
  2. 進程是暫時的,程序是永久的。
  3. 進程與程序的組不同(比如進程含有輸入的數據)

進程的特點

  1. 動態性
  2. 併發性
  3. 獨立性
  4. 制約性

進程控制塊(PCB)

程序 = 算法 + 數據結構

操作系統也可以看作程序,程序控制塊就是操作系統的數據結構,主要表示進程的狀態,使進程成爲一個能獨立運行的基本單位。

一個PCB唯一標識一個進程,所以PCB含有一個進程的信息標識。PCB還存儲着CPU運行的狀態信息。PCB還存儲着進程的控制信息,這個信息是操作系統用於控制,調控進程以及進程之間運作,協調,交流而使用的信息。

所有進程可以按其進程狀態(就緒,阻塞)將其PCB組織起來。組織方式有鏈表和索引表。鏈表:桶一狀態的進程連接在同一個鏈表上,用鏈表的好處是面對頻繁的進程創建和刪除,鏈表操作起來更方便。索引表:同一狀態的進程在同一個索引表中,索引就是數組,在進程創建和刪除不那麼頻繁的時候,用索引表不失爲 一種高效的方法。

PID(process ID):進程在創建的時候由操作系統分配其一個ID,這個ID在進程結束之前是不變的。但是同樣的程序下次創建進程時操作系統會重新分配PID。在同一時刻,PID和進程是一一對應的關係。
PPID(parent PID):是進程的父進程的PID。

進程的狀態
進程主要有 3 種狀態,運行,就緒,阻塞。運行態就是可以轉爲阻塞或者就緒,就緒可以轉爲運行,阻塞可以轉爲就緒。 除了這三種,還有一種“掛起”狀態,掛起是在某個進程急需運行時而內存中的進程已經滿了,這時就要將內存中不急着運行的阻塞的進程轉到外存,將急着運行的進程放入內存。除了可以阻塞到掛起, 也可以就緒到掛起(很少出現)。運行可以轉變爲就緒掛起,就緒可以轉爲就緒掛起,阻塞可以轉爲阻塞掛起,阻塞掛起可以轉爲就緒掛起。
爲什麼有線程
線程產生的更根本原因就是效率。單單使用進程在某些方面效率低下,甚至無法完成需求。設想一個播放視頻的程序,如果用單一的進程實現大致是下面這樣。

main() {
	while (1) {
		read();			//讀取視頻文件
		decoding();		//視頻解碼
		play();			//播放
	}
}

如果一個視頻播放程序,每次播放前都是將一個視頻全部解碼之後再放入緩存中開始播放,那用戶就得在播放前等上一陣子,跟何況如果視頻太大,緩存可能放不下。所以一般是一點一點的讀取-解碼-播放。那麼問題來了,每次播放完了一段視頻之後,必須等一會read()和decoding()再播放,視頻播放出來的效果是斷斷續續的。所以人們就想辦法讓這三個功能同步執行,讀取文件的功能會讀完文件之後立馬去讀下一個文件,而不是等上一次播放結束後纔開始讀下一個文件;同樣的,視頻解碼也是馬不停蹄地在解碼。這樣才能實現流暢的播放。而這分開的三個功能就是三個線程。

什麼是線程
回到上面的例子,想一下,read 所讀取的文件需要提供給 decoding ,decoding 解碼的數據要提供給 play;再者,讀取文件的速度是很慢的,這就需要多創建幾個線程同時讀,這幾個read線程就需要訪問進程爲其打開的同一個視頻文件。可以看出,線程之間的資源共享就顯得很重要。事實也是如此,一個進程的多個線程是共享這個進程的資源的。這些資源有代碼,數據,內存,文件等等。

由以上討論可以看出,線程是功能執行單元,而進程更像是一個資源管理者。

類似於進程的PCB,線程也有自己的TCB(thread control process),但是由於線程間共享進程的資源,所以TCB中主要內容是進程的狀態信息。

線程的優缺點
優點就是高效
1.線程靈活,創建線程和刪除線程的系統開銷相比於進程來說要小很多。
2.線程之間能共享資源。
3.線程之間切換速度快於進程。

缺點就是容錯率低
正是因爲資共享,如果一個線程出錯,導致共享的資源出錯,那麼就會有可能引起別的線程出錯,甚至整個進程崩潰。

線程和進程的比較
1.進程是資源分配的基本單位,線程是CPU調度的基本單位。
2.各個進程獨享完整的資源平臺;而各個線程獨享必要的資源,寄存器,棧等。注意!雖然線程獨享一些資源,但是這些資源也是能被其他線程直接訪問的,且無法被阻止。
3.進程和線程同樣有三態。
4.線程的空間和時間消耗少;線程創建和結束都很快,因爲線程所使用的資源是交給進程管理了。線程之間的切換很快,應爲共享資源,再切換的時候不需要切換“頁表”。線程之間通信不需要經過內核,因爲資源共享。

PS:線程進程各有優劣,在具體使用時,要根據實際情況來選擇使用。

線程的實現

  • 用戶線程:操作系統 “看不見“ 的線程。這種線程用專門的 ”用戶線程庫“ 實現對線程的管理。操作系統只能看見進程的相關信息,看不見進程裏的線程信息。總的來說,線程的調度和管理由庫來管理,操作系統不直接參與。用戶線程模型
    優點:第一,線程切換是由線程庫來完成的,無需用戶態/內核態的切換,速度非常快。第二,由於線程是由線程庫實現的,所以可以在不支持線程技術的操作系統中。第三,每個線程的TCB由線程庫來維護。第四,不同進程可以由不同線程調度算法。
    缺點:第一,一個線程的阻塞會造成整個進程被阻塞。第二,一個線程除非主動交出CPU使用權,不然進程中的其他線程無法運行,因爲線程庫沒有打斷線程運行的特權(操作系統有,通過時鐘中斷實現)。第三,由於操作系統分配時間片給單個進程,所以每個線程分得的時間相比於內核線程會較少,執行較慢。

  • 內核線程:操作系統 “看得見” 的線程。線程控制塊放在內核中,線程的控制和調度都是由內核實現的。
    內核線程模型
    優點:第一,一個線程的阻塞不會影響其他內核線程的運行。第二,時間片是分配給線程的,多線程進程會獲得更多的CPU。
    缺點:進程的創建,終止,切換都是由系統調用實現的,開銷很大。

內核線程和用戶線程之間有三種對應關係

  • 多個用戶線程對應一個內核線程
  • 一個用戶線程對飲過一個內核線程
  • 多個用戶線程對應多個內核線程

輕量級進程
由內核支持的用戶線程,一個進程可有一個或多個輕量級進程,每個量級進程由一個單獨的內核線程來支持。
輕量級線程模型
管理起來複雜,但是更靈活。

上下文切換

在進程切換時需要進行上下文切換,上下文切換的目的是爲了使進程再次運行時能夠恢復到上次運行的狀態。上下文切換必須快速,因爲整個操作非常頻繁。

上下文有哪些?

  • 寄存器(PC,SP,……),CPU狀態,……

上下文切換如何實現?
因爲需要速度塊,所以是由彙編實現的 。

操作系統爲活躍的進程準備了進程控制塊,並且將PCB放在一個合適的隊列裏,方便進程之間的切換。

  • 就緒隊列
  • 等待I/O隊列
  • 殭屍隊列

進程的創建,加載,等待,終止

創建 :新進程的創建是由已存在的進程通過fork()函數創建的,這個已存在的進程稱爲新進程的父進程。fork()函數將父進程的PCB複製到內存中(除了PID不復制),做爲子進程的PCB。
加載 :子進程被創建之後,需要將自己需要的資源加載到自己的PCB中,這時子進程調用exec()函數來加載自己需要的資源。
等待 :父進程有個wait()函數等待子進程結束並將其PCB回收,因爲子進程難以自己回收自己,所以需要其他進程的幫助。如果子進程結束之前父進程就已經結束了,那麼子進程會進入殭屍隊列等待被回收,此時init進程會定時掃描進程控制塊,檢查有無處於殭屍狀態的進程並回收。
終止 :進程結束時,調用exit()函數終止進入殭屍狀態,等待父進程來回收。

進程間通信

進程之間可能會有合作的關係,合作關係的進程可能就需要使用一段相同的地址空間或者共享一部分相同的磁盤空間,那麼在兩個進程對這些共享資源操作的過程中就有可能出現不協調而出錯的可能。
打個比方:有兩個計算機科學家(A,B)住在一起,A早上起來發現冰箱沒麪包了,於是出門買麪包。A剛出門,B也起牀了,B發現冰箱裏沒有面包了,於是B也出門買麪包。最後的結果是A和B都買了麪包。這肯定不是AB希望的結果。
計算機的進程之間的合作也會出現類似的情況。這種情況的出現稱爲競爭條件的出現。

競爭條件兩個或多個進程讀寫某些共享數據,而最後的結果取決於進程運行的精確時序。(全文背誦!)

那麼如何避免這種情況的發生呢?

先分析一下出現這種情況的原因。

  • A和B都可以對冰箱進行 ”檢查冰箱—決定是否買麪包“ 的操作,所以纔有可能出現兩人都買的情況。
  • A出門買麪包到回來這段時間內,冰箱仍然是空的且B並不知道A是否去買麪包了

解法0
看第一個原因。那可否讓A和B中只有一個人擁有 ”檢查冰箱—決定是否買麪包“ 的能力呢?這樣就不會出現買多了的情況。想一想,顯然是不可以的!A和B是合作關係,如果這個活全部都交給A或者B了,那麼還談何合作?況且如果A沒有起牀,B就有可能要餓一上午!

解法1
看第二個原因。只要A在買之前,在冰箱上貼一個標籤()告訴B自己去買麪包了,B就不會去買了。那這個方法行不行呢?看上去好像是沒有問題的,但是雖然現實中沒問題,但是放在計算機裏就不一定了!!

現在回到計算機中,我們用程序來描述一下A和B的行爲。

//計算機科學家A
if (冰箱上沒有B的標籤) {
	if (冰箱是空的) {
		貼上標籤A;
		去買麪包;
		撕標籤;
	}	
}

//計算機科學家B
if (冰箱上沒有A的標籤) {
	if (冰箱是空的) {
		貼上標籤B;
		去買麪包;
		撕標籤;
	}
}

我們知道進程是併發執行的,如果A崗判斷完冰箱是空的但是還沒貼標籤,操作系統調度B開始運行,這時由於A還沒貼標籤,B會去買麪包。此時已經出錯了!A是先去檢查冰箱的,但是B仍然去買了麪包,而且繼續觀察程序,會發現A也會去買麪包! 所以貼標籤並不能解決這個問題。原因是,檢查冰箱和貼標籤這兩個動作不是原子性的,可能會被打斷。

解法2
那想辦法讓這兩個操作結合起來變爲原子性嗎?這樣當然是最好的,但是暫時合不起來。只能想想別的合作機制。之前的方法會發現,冰箱沒有標籤的狀態有可能會被AB同時發現,進而導致後面的問題。如果冰箱的標籤狀態只有兩個,狀態0表示A可以去買,B不可以買;狀態1表示B可以買,A不可以。那麼就不會出現A,B發現同一狀態而做同樣的事情的可能了。這麼說有點奇怪,看一下程序就明白了

//計算機科學家A
while (冰箱狀態爲1) 
	等待;
if (冰箱爲空) {
	買麪包;
	冰箱狀態改爲1;
}

//計算機科學家B
while (冰箱狀態爲0)
	等待;
if (冰箱爲空) {
	買麪包;
	冰箱狀態改爲0;
}

以上面這種嚴格輪流工作的機制,就一定不會出現AB同時去買麪包的情況了。但是這個方式合適嗎?仔細想想能發現,這個方法有個很不好的地方,如果A買了麪包之後將狀態改爲1,當A下次再去買麪包時,就必須得等B買一次麪包回來把狀態改爲0,不然A會永遠的等待。這種等待是佔用CPU資源的,稱爲忙等待

討論到這裏發現,多買麪包的問題總是得不到完美的解決,每次看似解決了一個問題,馬上另外一個問題就冒出來了。所以我們得制定幾項原則,可行的解決方案應該要滿足以下這幾項原則。

  1. 進程A,B不會同時進入臨界區
  2. 不對CPU速度和數量做任何假設
  3. 臨界區外運行的進程不得阻塞其他進程
  4. 不得使進程無限制的等待進入臨界區

簡單解釋一下,我們把對共享內存進行訪問的程序片段稱爲臨界區,上面的例子裏檢查冰箱並買麪包的程序片段就是臨界區。不對CPU速度和數量做任何假設,我的理解就是,我們尋找的解決方法是在軟件層面的,那麼我們的方法應該是對任何硬件平臺適用的,所以不對CPU速度和數量做任何假設。

有了以上的原則約束再來尋找解決辦法

解法3:屏蔽中斷
在程序進入臨界區之前就屏蔽所有的中斷信號,這樣就不會在運行的時候,因爲系統調用而被打斷。雖然這個方法確實能解決問題,而且也滿足上面的四項原則,但是這種方法還是不被接受的。如果臨界區的操作很少,那麼這麼做幾乎不會對系統造成太大影響,但是臨界區操作很多的話,那在操作過程中,整個計算機就失去了人機交互功能,一切外界的中斷(鼠標,鍵盤,磁盤)都不會被系統接受,甚至操作系統都被終止。

解法4:Peterson解法
這是一個可以接受的解法!且是一個好方法!
Peterson解法(下文簡稱P法)是上文所討論的解法2的升級版本。解法二中因爲嚴格的輪換工作制度,導致當一個進程不想工作之後,另一個進程就一直需要等待。那麼很容易想到的解決方法就是讓每個進程公開自己想不想工作。這樣就不會因爲一個進程不想工作而導致另一個進程無法工作。程序的實現就是加一個數組來記錄每個人是否想工作。

int turn;			//用turn表示冰箱狀態,0代表A工作,1代表B工作
int want[2];		//記錄AB是否想工作,值爲0是不想,值爲1是想
//計算機科學家A
{
	want[0] = 1;		//記錄自己想工作
	turn = 0;			//A進入工作狀態
	while(turn == 1 && want[1] == 1);  //如果B想工作且B進入了工作狀態,那就等B工作
	臨界區;
	want[0] = 0;		//A結束了臨界區的工作,並記錄自己不想工作。
}

計算機科學家B的程序也是類似的。
P法滿足了四項原則,至於證明的話用反證法,模擬一下程序的執行過程就能證明。而且P法沒有什麼缺點,唯一美中不足的就是,P法仍然有忙等待,這是一種對cpu資源的浪費。

以上的解法都是在軟件層面,下面介紹一種硬件方面的解決方案。

解法5 :TSL和XCHG

TSL RX , LOCK (TSL是指令名,RX和LOCK是指令參數)
在一些計算機中,特別是設計爲多處理器的計算機都有TSL指令:測試並枷鎖(test and set lock)。該操作一共有兩個步驟

  1. 將內存字lock讀到寄存器RX中
  2. 在該內存地址上存一個非0值

這兩個二步驟是不可分割的原子操作,在兩個步驟執行完成之前CPU不會被調度執行別的進程。而且執行該指令的CPU會鎖住內存總線,禁止其他CPU訪問該內存字。

具體使用方法如下,還是用上文中的例子

//計算機科學家A
start:
	TSL(rx, lock);		//把鎖的值賦給rx, 並將鎖的值設爲非1
	if (rx == 0) {		//檢查鎖的值是不是0
		買麪包;			//如果是0,則可以買麪包
		lock = 0;		//臨界區操作結束後,將鎖的值設爲0
	} else {
		goto start;		//如果不是0,則循環
	}

TSL本身是彙編語言,整個程序應該也寫成彙編,寫成C語言的形式是爲了方便理解,邏輯上兩者都是差不多的。

還有一種指令是XCHG RX,LOCK。同樣是一個原子操作,但和TSL不同,XCHG直接交換rx和lock的值,使用方法如下

//計算機科學家A
start:
	rx = 1;				//將rx的值設爲1
	XCHG(rx, lock);		//交換rx和lock的值,現在lock值爲1
	if (rx == 0) {		//檢查鎖的值是不是0
		買麪包;			//如果是0,則可以買麪包
		lock = 0;		//臨界區操作結束後,將鎖的值設爲0
	} else {
		goto start;		//如果不是0,則循環
	}

不論是XCHG還是TSL(硬件層)還是peterson解法(軟件層),他們都是正確的,但是都有忙等待的缺點,這不僅浪費了CPU時間,甚至有可能造成預想不到的後果,比如優先級反轉問題。爲了找尋更好的辦法,計算機科學家Dijkstra發明了信號量

信號量(semaphore)

在討論信號量之前,想一想解決忙等待的方法有啥?容易想到用阻塞來代替忙等待。《現代操作系統》一書上有“生產者消費者問題”,百度上也能搜到。這個問題就是使用阻塞代替忙等,但是它的阻塞和阻塞喚醒機制有些問題,信號量的出現恰好解決了這個問題。

信號量是Dijkstra在1965年提出的一種方法,它使用一個整型變量來累計喚醒次數,供以後使用。在他的建議中引入了一個新的變量類型,稱作信號量(semaphore)。一個信號量的取值可以爲0(表示沒有保存下來的喚醒操作)或者正值(表示有一個或多個喚醒操作)。對於信號量的操作有down和up(原名是P和V,寫成down和up便於理解)。down操作是一個進程在需要某種資源(一般用信號量表示資源的狀態)時進行的要進行的操作。down會檢查某一個信號量的值,如果值大於0,則信號量減一,該進程繼續;如果信號量等於0,該進程睡眠(此時down操作並未結束,因爲還沒有完成對信號量的減一操作)。up操作對某信號量的值加一,如果有一個或多個進程因爲down操作睡眠在此信號量上,則up喚醒其中一個(具體喚醒哪一個由系統決定)來完成自己未完成的down操作。
down和up操作都屬於原子操作,不會被中斷,如果有多個CPU,則每個信號量需要有一個鎖變量進行保護,通過TSL和XCHG指令來確保同一時刻只有一個CPU在對信號量操作。

下文的內容基於 “生產者和消費者問題”

生產者和消費者之間需要進行互斥,生產者和生產者之間需要互斥,消費者和消費者之間需要互斥。這時只需要一個信號量就可以實現:

typedef int semaphore;
semaphore mutex = 1;  					//用於互斥的信號量,值爲0或1

void producer {
	int item;
	while (1) {
		item = produce_item();
		down(mutex);					//檢查mutex是否爲0,爲0則睡眠,爲1,則減一併繼續
		insert_item(item);				//生產操作
		up(mutex);						//使mutex加一,如果有在mutex上睡眠的進程,則換醒
	}
}

void consumer {
	int item;
	while (1) {
		down(mutex);					//檢查mutex是否爲0,爲0則睡眠,爲1,則減一併繼續
		item = remove_item();			//消費操作1(在臨界區)
		up(mutex);						//使mutex加一,如果有在mutex上睡眠的進程,則換醒
		consume_item(item);				//消費操作2(不在臨界區)
	}
}

生產者和消費者除了存在互斥問題,還有一個重要的問題是同步問題。什麼是同步,同步就是線程之間的運行存在一定程度的先後順序。比如在生產者和消費者問題中,緩衝區如果滿了,生產者就必須等消費者從緩衝區中取出一個item之後才能再度運行;同樣的如果緩衝區空了,消費者就必須等待生產者向緩衝區中加入一個item之後才能運行。所以在這個問題中有兩個需要同步的地方。那麼需要兩個信號量就可以實現同步:

#define N 100;							//緩衝區最大容量爲100
typedef int semaphore;
semaphore mutex = 1;  					//用於互斥的信號量,值爲0或1
semaphore full = 0;						//用於同步的信號量,代表緩衝區中item數量
semaphore empty = N;					//用於同步的信號量,代表緩衝區中空位數量

void producer {
	int item;
	while (1) {
		item = produce_item();
		down(&empty);					//檢查緩衝區中是否有空位,沒有則睡眠,有則減一併繼續
		down(&mutex);					//檢查mutex是否爲0,爲0則睡眠,爲1,則減一併繼續
		insert_item(item);				//生產操作
		up(&mutex);						//使mutex加一,如果有在mutex上睡眠的進程,則換醒
		up(&full);						//使緩衝區中item數加一,如果有在full上睡眠的進程,則換醒
	}
}

void consumer {
	int item;
	while (1) {
		down(&full);					//檢查緩衝區中是否有item,沒有則睡眠,有則減一併繼續
		down(&mutex);					//檢查mutex是否爲0,爲0則睡眠,爲1,則減一併繼續
		item = remove_item();			//消費操作1(在臨界區)
		up(&mutex);						//使mutex加一,如果有在mutex上睡眠的進程,則換醒
		up(&empty);						//使緩衝區中空位數加一,如果有在empty上睡眠的進程,則換醒
		consume_item(item);				//消費操作2(不在臨界區)
	}
}

同步的問題也解決了,信號量既能解決互斥也能解決同步!!!

互斥量

互斥量可以看作是信號量的簡化版本,比如上文中的mutex就類似於互斥量。如果不需要信號量的計數能力,就可以用互斥量。互斥量實現時既容易又有效,這使得互斥量在實現用戶空間線程包時非常有用。

《現代操作系統》p75有互斥量在用戶線程包中的應用以及優點,講的很清楚。

管程

有了信號量和互斥量基本可以說解決問題了,但是這樣真的結束了嗎?顯然沒有,信號量的使用是比較複雜的,上文中的代碼雖然看起來不難,但是輪到你自己寫代碼時,你就會發現各種問題。比如上文中的生產者和消費者在進入臨界區之前都要進行兩個 down 操作,第一個down用於同步,第二個用於互斥,如果這兩個操作順序交換一下,會發生什麼結果呢?比方說生產者,假設它先進行down(&mutex),在down(&empty)時它睡眠了,這時它還沒有釋放mutex,這就使得沒有任何進程能進入臨界區,沒有進程能執行up操作喚醒它,那這個生產者永遠睡在了那裏。一個簡單的例子可以發現信號量的使用必須非常謹慎。

所以爲了更易於編寫出正確的程序,Brinch Hansen和Hoare提出了管程。在後文能發現,他們兩提出的管程略有不同。

首先,何爲管程?管程是由局部於自己的若干公共變量及其說明和所有訪問這些公共變量的過程所組成的軟件模塊。 簡單的說管程是一種對於同步操作更高級的封裝。傳統同步方法需要獨立管理,管程可以對他們進行統一方式的管理。

管程有一個關鍵特性,任意時刻管程中只能有一個活躍進程,這使得管程能有效地完成互斥。這個特性是由編譯器負責的。

光是互斥還不夠,管程還需要實現同步,所以解決的方法是引入條件變量,以及兩個相關的操作:wait和signal。當一個管程過程發現它無法繼續運行時(例如消費者發現緩衝區爲空),它會在某個條件變量上執行wait(如empty)操作。該操作使得進程自身阻塞,並且將另外一個之前等待在管程之外的進程(不是被阻塞的進程可能是就緒的進程)調入管程。signal就比較簡單了,比方說生產者,可以喚醒正在阻塞中的消費者,只要通過對消費者阻塞在的條件變量(empty)執行signal操作就ok。wait和signal的實現大致如下:

Class condition{				//condition就是條件變量
	int numwaiting = 0;			//用來記錄睡眠中的進程數量
	waitqueue q;				//如果有多個進程睡眠了,就會進入睡眠隊列中
}

condition::Wait(lock) {
	numwaiting++;				
	add this thread to q;
	release(lock);				//進程釋放鎖,不然下面調入的新進程就不能進入管程了
	schedule();					//調入新進程, 原進程此時被阻塞了
	require(lock);				//調入進程結束了,原進程要再次獲得鎖
}

condition::Signal() {
	if (numwaiting > 0) {
		remove a thread from q;
		wakeup();					//喚醒一個被阻塞的進程
		numwaiting--;
	}
}

現在有一個問題,signal喚醒進程之後,如果不做任何事情,就會有兩個進程在管程之中,這與管程的特性不符。對於這個問題Hansen和Hoare提出了不同的方案。Hansen建議在signal之後原進程立馬退出管程。Hoare建議signal之後,原進程掛起,等喚醒的進程結束返回之後,再繼續運行原進程。一般採用Hansen的方法,因爲概念上更簡單,且容易實現。
至於管程的在生產者於消費者問題中的運用,清華大學陳渝老師講的很好
清華大學->操作系統->管程

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