作業系統中的程序與執行緒
簡介
在傳統的作業系統中,程序擁有獨立的記憶體地址空間和一個用於控制的執行緒。但是,現在的情況更多的情況下要求在同一地址空間下擁有多個執行緒併發執行。因此執行緒被引入作業系統。
為什麼需要執行緒?
如果非要說是為什麼需要執行緒,還不如說為什麼需要程序中還有其它程序。這些程序中包含的其它迷你程序就是執行緒。
執行緒之所以說是迷你程序,是因為執行緒和程序有很多相似之處,比如執行緒和程序的狀態都有執行,就緒,阻塞狀態。這幾種狀態理解起來非常簡單,當程序所需的資源沒有到位時會是阻塞狀態,當程序所需的資源到位時但CPU沒有到位時是就緒狀態,當程序既有所需的資源,又有CPU時,就為執行狀態。
下面我們來看一個具體的例子:
就拿我寫部落格的LiveWriter來說,LiveWriter需要監聽我打字輸入的狀態,還需要每隔5分鐘對草稿進行自動儲存。假設如果這個程序只有一個執行緒的話,那麼當對草稿進行儲存時,因為此時需要訪問硬碟,而訪問硬碟的時間執行緒是阻塞狀態的,這時我的任何輸入都會沒有響應,這種使用者體驗是無法接受的,或許我們可以通過鍵盤或者滑鼠的輸入去中斷儲存草稿的過程,但這種方案也並不討好。而使用多執行緒,每個執行緒僅僅需要處理自己那一部分應該完成的任務,而不用去關心和其它執行緒的衝突。因此簡化了程式設計模型。如圖1所示。
圖1.兩條執行緒滿足各自的功能
更具體的說,執行緒的好處如下:
1.在很多程式中,需要多個執行緒互相同步或互斥的並行完成工作,而將這些工作分解到不同的執行緒中去無疑簡化了程式設計模型。
2.因為執行緒相比程序來說,更加的輕量,所以執行緒的建立和銷燬的代價變得更小。
3.執行緒提高了效能,雖然執行緒巨集觀上是並行的,但微觀上卻是序列。從CPU角度執行緒並無法提升效能,但如果某些執行緒涉及到等待資源(比如IO,等待輸入)時,多執行緒允許程序中的其它執行緒繼續執行而不是整個程序被阻塞,因此提高了CPU的利用率,從這個角度會提升效能。
4.在多CPU或多核的情況下,使用執行緒不僅僅在巨集觀上並行,在微觀上也是並行的。
這裡值得注意的是,上面的兩個執行緒如果改成兩個程序,那麼達不到所要的效果,因為程序有自己獨立的記憶體地址空間,而執行緒共享程序的記憶體地址空間。
經典執行緒模型
另一個看程序和執行緒的角度是程序模型基於兩類不同的概念:資源的組織和執行。在過去沒有執行緒的作業系統中,資源的組織和執行都是由程序完成的。但區分這兩者很多時候需要加以區分,這也是為什麼需要引入執行緒。
程序是用於組織資源的單位,程序將相關的資源組織在一起,這些資源包括:記憶體地址空間,程式,資料等,將這些以程序的形式組織起來可以使得作業系統管理這些資源更為容易。
而執行緒,是每一個程序中執行的一個條線。執行緒雖然共享程序中的大多數資源,但執行緒也需要自己的一些資源,比如:用於標識下一條執行指令的程式計數器,一些容納區域性變數的暫存器,以及用於表示執行的歷史的棧。
總而言之:程序是組織資源的最小單位,而執行緒是安排CPU執行的最小單位。
其實在一個程序中多個執行緒並行和在作業系統中多個程序並行非常類似,只是執行緒共享的是地址空間,而程序共享的是實體記憶體,印表機,鍵盤等資源……
每一個程序和執行緒所獨自佔有的資源如表1所示。
程序佔有的資源 | 執行緒佔有的資源 |
地址空間 全域性變數 開啟的檔案 子程序 訊號量 賬戶資訊 |
棧 暫存器 狀態 程式計數器 |
表1.程序和執行緒所獨佔的資源
其中,執行緒可以共享程序獨佔的資源。
我們常用的術語“多執行緒”一般指的是在同一個程序中多個執行緒的併發執行。如圖2所示。
圖2.沒有多執行緒的系統一個程序只能由一個執行緒
在多執行緒的程序中,每個執行緒輪流使用CPU,因此實際上執行緒並不是並行的,但從巨集觀上看,是並行的。
在多執行緒模型中,每一個程序初始建立時只有一個執行緒。這個執行緒可以通過呼叫系統的庫函式去建立其它執行緒。執行緒建立的執行緒並必須要為其指定地址,因為新的執行緒自動在建立它的地址空間內工作。雖然一個執行緒可以建立另一個執行緒,但通常來講,執行緒之間是並列的,並不存在層級關係。
當一個程序完成其工作後,可以通過呼叫系統庫函式進行銷燬。
作業系統實現執行緒的幾種模式
在作業系統中,執行緒可以實現在使用者模式下,也可以實現在核心模式下,也可以兩者結合實現。
執行緒實現在使用者空間下
當執行緒在使用者空間下實現時,作業系統對執行緒的存在一無所知,作業系統只能看到程序,而不能看到執行緒。所有的執行緒都是在使用者空間實現。在作業系統看來,每一個程序只有一個執行緒。過去的作業系統大部分是這種實現方式,這種方式的好處之一就是即使作業系統不支援執行緒,也可以通過庫函式來支援執行緒。
在這種模式下,每一個程序中都維護著一個執行緒表來追蹤本程序中的執行緒,這個表中包含表1中每個執行緒獨佔的資源,比如棧,暫存器,狀態等,如圖3所示。
圖3.在使用者空間中實現執行緒
這種模式當一個執行緒完成了其工作或等待需要被阻塞時,其呼叫系統過程阻塞自身,然後將CPU交由其它執行緒。
這種的模式的好處,首先,是在使用者空間下進行程序切換的速度要遠快於在作業系統核心中實現。其次,在使用者空間下實現執行緒使得程式設計師可以實現自己的執行緒排程演算法。比如程序可以實現垃圾回收器來回收執行緒。還有,當執行緒數量過多時,由於在使用者空間維護執行緒表,不會佔用大量的作業系統空間。
有好處就有壞處,這種模式最致命的缺點也是由於作業系統不知道執行緒的存在,因此當一個程序中的某一個執行緒進行系統呼叫時,比如缺頁中斷而導致執行緒阻塞,此時作業系統會阻塞整個程序,即使這個程序中其它執行緒還在工作。還有一個問題是假如程序中一個執行緒長時間不釋放CPU,因為使用者空間並沒有時鐘中斷機制,會導致此程序中的其它執行緒得不到CPU而持續等待。
執行緒實現在作業系統核心中
在這種模式下,作業系統知道執行緒的存在。此時執行緒表存在作業系統核心中,如圖4所示。
圖4.執行緒在作業系統核心中實現
在這種模式下,所有可能阻塞執行緒的呼叫都以系統呼叫(System Call)的方式實現,相比在使用者空間下實現執行緒造成阻塞的執行時呼叫(System runtime call)成本會高出很多。當一個執行緒阻塞時,作業系統可以選擇將CPU交給同一程序中的其它執行緒,或是其它程序中的執行緒,而在使用者空間下實現執行緒時,排程只能在本程序中執行,直到作業系統剝奪了當前程序的CPU。
因為在核心模式下實現程序的成本更高,一個比較好的做法是另執行緒回收利用,當一個執行緒需要被銷燬時,僅僅是修改標記位,而不是直接銷燬其內容,當一個新的執行緒需要被建立時,也同樣修改被“銷燬”的執行緒其標記位即可。
這種模式下同樣還是有一些弊端,比如接收系統訊號的單位是程序,而不是執行緒,那麼由程序中的哪一個執行緒接收系統訊號呢?如果使用了表來記錄,那麼多個執行緒註冊則通過哪一個執行緒處理系統訊號?
混合模式
還有一種實現方式是將上面兩種模式進行混合,使用者空間中程序管理自己的執行緒,作業系統核心中有一部分核心級別的執行緒,如圖5所示。
圖5.混合模式
在這種模式下,作業系統只能看到核心執行緒。使用者空間執行緒基於作業系統執行緒執行。因此,程式設計師可以決定使用多少使用者空間執行緒以及作業系統執行緒,這無疑具有更大的靈活性。而使用者空間執行緒的排程和前面所說的在使用者空間下執行實現執行緒是一樣的,同樣可以自定義實現。