操作系統中最核心的概念就是進程:這是對正在運行程序的一個抽象。
操作系統的其他所有內容都是圍繞著進程的概念展開的。
進程是操作系統提供的最古老的也是最重要的抽象概念之一。
即使可以利用的CPU只有一個,但它們也支持(偽)并發操作的能力。
它們將一個單獨的CPU變換成多個虛擬的CPU。
沒有進程的抽象,現代計算將不復存在。
進程
所有現代計算機經常會在同一時間做許多件事。
在任何多道程序設計系統中,CPU由一個進程快速切換至另一個進程,使每個進程各運行幾十或幾百個毫秒。
嚴格的說,在某個一瞬間,CPU只能運行一個進程。
但在1秒鐘期間,它可能運行多個進程,這樣就產生并行的錯覺。
人們很難對多個并行活動進行跟蹤。
因此,經過多年的努力,操作系統的設計者發展了用于描述并行的一種概念模型(順序進程),使得并行更容易處理。
進程模型
在進程模型中,計算機上所有可運行的軟件,通常也包括操作系統,被組織成若干順序進程,簡稱進程。
一個進程就是一個正在執行程序的實例,包括程序計數器,寄存器和變量的當前值。
從概念上說,每個進程擁有它自己的虛擬CPU。
當然,實際上真正的CPU在各進程之間來回切換。
但為了理解這種系統,考慮在(偽)并行情況下運行的進程集,要比我們試圖跟蹤CPU如何在程序間來回切換簡單得多。
一個進程是某種類型的一個活動,它有程序,輸入,輸出以及狀態。
單個處理器可以被若干進程共享,它使用某種調度算法決定何時停止一個進程的工作,并轉而為另一個進程提供服務。
值得注意的是,如果一個程序運行了兩遍,則算作兩個進程。
線程
在傳統的操作系統中,每個進程有一個地址空間和一個控制線程。
不過,經常存在在同一個地址空間中準并行運行多個控制線程的情形,這些線程就像(差不多)分離的進程(共享地址空間除外)。
人們需要多線程的主要原因是,在許多應用中同時發生著多種活動。
其中某些活動隨著時間的推移會被阻塞。
通過將這些應用程序分解成可以準并行運行的多個順序線程,程序設計模型會變得更簡單。
準確的說,正是之前關于進程模型的討論,有了這樣的抽象,我們才不必要考慮中斷,定時器和上下文切換,而只需考慮并行進程。
類似的,只是在有了多線程概念之后,我們才加入了一種新的元素:并行實體共享同一個地址空間和所有可用數據的能力。
對于某些應用而言,這種能力是必需的,而這正是多進程模型(它們具有不同地址空間)所無法表達的。
第二個關于需要多線程的理由是,由于線程比進程更輕量級,所以它們比進程更容易(即更快)創建,也更容易撤銷。
在許多系統中,創建一個線程較創建一個進程要快10-100倍。
在有大量線程需要動態和快速修改時,具有這一特性是很有用的。
需要多線程的第三個原因涉及性能方面的討論。
若多個線程都是CPU密集型的,那么并不能獲得性能上的增強,但是如果存在著大量的計算和大量的I/O處理,擁有多個線程允許這些活動彼此重疊進行,從而會加快應用程序執行的速度。
經典的線程模型
線程給進程模型增加了一項內容,即在同一個進程環境中,允許彼此之間有較大獨立性的多個線程執行。
在同一個進程中并行運行多個線程,是對在同一臺計算機上并行運行多個進程的模擬。
在前一種情形下,多個線程共享同一個地址空間和其他資源。
在后一種情形下,多個進程共享物理內存,磁盤,打印機和其他資源。
由于線程具有進程的某些性質,所以有時被稱為輕量級進程。
多線程這個術語,也用來描述在同一個進程中允許多個線程的情形。
一些CPU已經有直接硬件支持多線程,并允許線程切換在納秒級完成。
進程用于把資源集中到一起,而線程則是在CPU上被調度執行的實體。
當多線程進程在單CPU系統中運行時,線程輪流運行。
進程中的不同線程不像不同進程之間那樣存在很大的獨立性。
同一個進程中的所有的線程都有完全一樣的地址空間,這意味著它們也共享同樣的全局變量。
由于各個線程都可以訪問進程地址空間中的每一個內存地址,所以一個線程可以讀,寫或甚至清除另一個線程的堆棧。
線程之間是沒有保護的,這與不同進程是有差別的。
不同進程會來自不同的用戶,它們彼此之間可能有敵意,一個進程總是由某個用戶所擁有,該用戶創建多個線程應該是為了它們之間的合作而不是彼此間的爭斗。
除了共享地址空間之外,同一個進程中的所有線程還共享同一個打開文件集,子進程,報警以及相關信號等。
對于三個沒有關系的線程,應該放到不同的進程中。
認識到每個線程都有其自己的堆棧很重要,每個線程的堆棧有一幀,供各個被調用但是還沒有從中返回的過程使用。
在該幀中存放了相應過程的局部變量以及過程調用完成之后使用的返回地址。
通常每個線程會調用不同的過程,從而有一個各自不同的執行歷史。
這就是為什么每個線程需要有自己的堆棧的原因。
在多線程情況下,進程通常會從當前的單個線程開始。
這個線程有能力通過調用一個庫函數(如thread_create)創建新的線程。
thread_create的參數專門指定了新線程要運行的過程名。
這里沒有必要對新線程的地址空間加以規定,因為新線程會自動在創建線程的地址空間中運行。
當一個線程完成工作后,可以通過調用一個庫過程(thread_exit)退出。
該線程接著消失,不再可調度。
在某些線程系統中,通過調用一個過程,例如thread_join,一個線程可以等待一個(特定)線程退出。
這個過程阻塞調用線程知道那個(特定)線程退出。
另一個常見的線程調用是thread_yield,它允許線程自動放棄CPU從而讓另一個線程運行。
這樣一個調用是很重要的,因為不同于進程,(線程庫)無法利用時鐘中斷強制線程讓出CPU。
所以設法使線程行為“高尚”起來,并且隨著時間的推移自動交出CPU,以便讓其他線程有機會運行,就變得非常重要。
用戶級線程和內核級線程
為實現可移植的線程程序,IEEE在IEEE標準1003.1c中定義了線程的標準。
它定義的線程包叫做Pthread,大部分UNIX系統都支持該標準。
有兩種主要的方法實現線程包:在用戶空間中和在內核中。
這兩種方法互有利弊,不過混合實現方式也是可能的。
第一種方法是把整個線程包放在用戶空間中,內核對線程包一無所知。
從內核角度考慮,就是按正常的方式管理,即單線程進程。
用戶級線程包可以在不支持線程的操作系統上實現。
線程在一個運行時系統的頂部運行,這個運行時系統是一個管理線程的過程的集合。
這時,保存線程狀態的過程和調度程度都只是本地過程,所以啟動它們比進行內核調用效率更高。
另一方面,不需要陷阱,不需要上下文切換,也不需要對內存高速緩存進行刷新,這就使得線程調度非常便捷。
用戶級線程還有另一個優點,它允許每個進程有自己定制的調度算法。
盡管用戶級線程包有更好的性能,但它也存在一些明顯的問題。
其中第一個問題是如何實現阻塞系統調用。
另一個問題是,如果一個線程開始運行,那么在該進程中的其他線程就不能運行,除非第一個線程自動放棄CPU。
人們已經研究了各種試圖將用戶級線程的優點和內核級線程的優點結合起來的方法。
一種方法是使用內核級線程,然后講用戶級線程與某些或者全部內核線程多路復用起來。
采用這種方法,內核只識別內核級線程,并對其進行調度。
其中一些內核級線程會被多個用戶級線程多路復用。