操作系统-线程
說明:文中內容大部分都是大部分都是《操作系統-精髓與設計原理 第八版》的原文,自己做了一些刪改,使其更易于理解。
本章講述一些與進程管理相關的高級概念,這些概念在很多現代操作系統中都可以找到。實際上,它包含了兩個獨立的概念:一個與資源所有權有關,一個與執行相關。這一區別使得許多操作系統中出現和發展了稱為線程(thread)的結構。
一 進程和線程
在迄今為止的討論中,進程具有如下兩個特點:
- 資源所有權
- 調度/執行
這兩個特點是獨立的,因此操作系統應能分別處理它們。很多操作系統,特別是近期開發的操作系統已在這樣做。為區分這兩個特點,我們通常將分派的單位稱為線程或輕量級進程(Light Weight Process,LWP),而將擁有資源所有權的單位稱為進程(process)或任務(task)。
1.1 多線程
多線程是指操作系統在單個進程內支持多個并發執行路徑的能力。每個進程中僅執行單個線程的傳統方法(此時還未提出線程的概念)稱為單線程方法(single-threaded approach)。
在多線程環境中,進程定義為資源分配單元和一個保護單元。與進程相關聯的有:
? ·容納進程映像的虛擬地址空間。
? ·對處理器、其他進程(用于進程間通信)、文件和I/O資源(設備和通道)的受保護訪問。
一個進程中可能有一個或多個線程,每個線程都有:
? 一個線程執行狀態(運行、就緒等)。
? 未運行時保存的線程上下文;線程可視為在進程內運行的一個獨立程序計數器。
? 一個執行棧。
? 每個線程用于局部變量的一些靜態存儲空間。
? 與進程內其他線程共享的內存和資源的訪問。
— 單線程多線程進程模型
圖4.2從進程管理的角度說明了線程和進程的區別。
在單線程進程模型中(無明確的線程概念),進程的表示包括其進程控制塊和用戶地址空間,以及在進程執行中管理調用/返回行為的用戶棧和內核棧。進程正運行時,處理器寄存器由該進程控制;進程未運行時,將保存這些處理器寄存器中的內容。
在多線程環境中 進程仍然只有一個與之關聯的進程控制塊和用戶地址空間,但每個線程現在會有許多單獨的棧和一個單獨的控制塊,控制塊中包含寄存器值、優先級和其他與線程相關的狀態信息。
因此,進程中的所有線程共享該進程的狀態和資源,所有線程都駐留在同一塊地址空間中,并可訪問相同的數據。當某個線程改變了內存中的一個數據項時,其他線程在訪問這一數據項時會看到這一變化。若一個線程以讀權限打開一個文件,那么同一進程中的其他線程也能從這個文件中讀取數據。
— 比較性能后會發現線程的如下優點:
1.在已有進程中創建一個新線程的時間,遠少于創建一個全新進程的時間。Mach開發人員的研究表明,線程創建要比在UNIX中創建進程快10倍。
2.終止線程要比終止進程所花的時間少。
3.同一進程內線程間切換的時間,要少于進程間切換的時間。
4.線程提高了不同執行程序間通信的效率。在多數操作系統中,獨立進程間的通信需要內核介入,以提供保護和通信所需的機制。但是,由于同一進程中的多個線程共享內存和文件,因此它們無須調用內核就可互相通信。
總結起來就是創建、終止、切換時間和通信效率的提高。
因此,若將一個應用程序或函數實現為一組相關聯的執行單元,則用一組線程要比用一組分離的進程更有效。此外,在單處理器上使用線程結構,但其可簡化邏輯上從事幾種不同工作的程序的結構。
— 這里給出在單用戶多處理系統中使用線程的4個例子:
- 前臺后臺工作
- 異步執行
- 執行速度:一個線程在讀取數據時被I/O操作阻塞,另一個線程仍然可以繼續運行。
- 模塊化程序結構
在支持線程的操作系統中,調度和分派是在線程基礎上完成的,因此大多數與執行相關的信息可以保存在線程級的數據結構中。但是,有些活動會影響進程中的所有線程,因此操作系統必須在進程級對它們進行管理。例如,掛起操作會把一個進程的地址空間換出內存,以便為其他進程的地址空間騰出位置。因為一個進程中的所有線程共享同一個地址空間,因此它們會同時被掛起。類似地,進程終止時會使得進程中的所有線程都終止。
1.2 線程功能
類似于進程,線程也具有執行狀態,且可彼此同步。下面依次介紹線程的這兩種功能。
— 線程狀態
和進程一樣,線程的主要狀態有運行態、就緒態和阻塞態。一般來說,掛起態對線程沒有意義,因此這類狀態僅適用于進程。特別地,如果一個進程被換出,由于所有線程都共享該進程的地址空間,因此所有線程都須被換出。
有4種與線程狀態改變相關的基本操作:
- 派生:典型情況下,在派生一個新進程時,同時也會為該進程派生一個線程。隨后,進程中的線程可在同一進程中派生另一個線程,并為新線程提供指令指針和參數;新線程擁有自己的寄存器上下文和棧空間,并放在就緒隊列中。
- 阻塞:線程需要等待一個事件時會被阻塞(保存線程的用戶寄存器、程序計數器和棧指針),處理器轉而執行另一個就緒線程。
- 解除阻塞:發生阻塞一個線程的事件時,會將該線程轉移到就緒隊列中。
- 結束:一個線程完成后,會釋放其寄存器上下文和棧。
— 線程同步
一個進程中的所有線程共享同一個地址空間和諸如打開的文件之類的其他資源。
一個線程對資源的任何修改都會影響同一進程中其他線程的環境,因此需要同步各種線程的活動,以便它們互不干擾且不破壞數據結構。例如,兩個線程都試圖同時往一個雙向鏈表中增加一個元素時,可能會丟失一個元素或破壞鏈表結構。
二 線程分類
2.1 用戶級和內核級線程
線程分為兩大類,即用戶級線程(User-Level Thread,ULT)和內核級線程(Kernel-Level Thread,KLT),后者又稱內核支持的線程或輕量級進程。
用戶級線程
在純ULT軟件中,管理線程的所有工作都由應用程序完成,內核意識不到線程的存在。圖4.5(a)說明了純ULT方法。任何應用程序都可使用線程庫設計成多線程程序。線程庫是管理用戶級線程的一個例程包,它含有創建和銷毀線程的代碼、在線程間傳遞消息和數據的代碼、調度線程執行的代碼,以及保存和恢復線程上下文的代碼。
默認情況下,應用程序從單個線程開始,并在該線程中開始運行。這個應用程序及其線程將分配給一個由內核管理的進程。應用程序在運行(進程處于運行態)的任何時刻,都可派生一個在相同進程中運行的新線程。線程派生是通過調用線程庫中的派生例程實現的。通過過程調用,控制權傳遞給派生例程。線程庫為新線程創建一個數據結構,然后使用某種調度算法,把控制權傳遞給該進程中處于就緒態的一個線程。當控制權傳遞給線程庫時,需要保存當前線程的上下文,然后在控制權從線程庫中傳遞給一個線程時,恢復那個線程的上下文。上下文實際上包括用戶寄存器的內容、程序計數器和棧指針。
上段描述的所有活動發生在用戶空間中和一個進程內,內核并不知道這些活動。內核繼續以進程為單位進行調度,并為進程指定一個執行狀態(就緒態、運行態、阻塞態等)。下面的例子將闡述線程調度和進程調度的關系。假設進程B在其線程2中執行,進程和作為進程一部分的兩個用戶級線程的狀態如圖4.6(a)所示。此時可能會發生如下情況之一:
- 在線程2中執行的應用程序代碼進行一個阻塞進程B的系統調用。例如,執行一次I/O調用。這會把控制權轉交給內核,隨后內核啟動I/O操作,把進程B置于阻塞態,并切換到另一個進程。在此期間,根據線程庫維護的數據結構,進程B的線程2仍處于運行狀態。注意,從在處理器上執行的角度來看,線程2實際上并不處于運行態,只是在線程庫看來它處于運行態。
- 時鐘中斷把控制權傳遞給內核,內核確定當前正運行的進程B已用完其時間片。內核把進程B置于就緒態并切換到另一個進程。同時,根據線程庫維護的數據結構,進程B的線程2仍處于運行態。
- 線程2運行到需要進程B的線程1執行某些動作的一個點。此時,線程2進入阻塞態,而線程1從就緒態轉換到運行態。進程自身保留在運行態。
在線程的調度過程中,可以認為進程一直是在運行之中的。因為在進程不在運行的時候,可以認為進程內部一切都停止了。當進程再次運行,進程內部才繼續進行。
— 使用ULT而非KLT的優點如下:
總結:減少模式切換開銷、自定義調度算法、操作系統平臺無關
— 使用ULT而非KLT的缺點如下:
總結:一個線程阻塞,則同一個進程中所有線程阻塞;無法使用多處理器技術
— 現在已有解決這兩個問題的方法
解決問題1
使用一種稱為“套管”(jacketing)的技術。“套管”的目標是把一個產生阻塞的系統調用轉化為一個非阻塞的系統調用。例如,替代直接調用一個系統I/0例程,讓線程調用一個應用級的I/O套管例程,這個套管例程中的代碼用于檢查并確定I/O設備是否忙。如果忙,該線程進入阻塞態并把控制權傳送給另一個線程。這個線程重新獲得控制權后,套管例程會再次檢查I/O設備。
這就有點像線程提示操作系統產生一個進程,這個新進程去執行I/O操作,執行完成后提示這個線程,在完成之前,這個線程都是阻塞,但是原來的進程就可以執行別的線程而不被阻塞。
解決問題2
把應用程序寫成一個多進程程序而非多線程程序。但是,這種方法消除了線程的主要優點:每次切換都變成進程間的切換而非線程間的切換,導致開銷過大。
內核級線程
在純KLT軟件中,管理線程的所有工作均由內核完成。應用級沒有線程管理代碼,只有一個到內核線程設施的應用編程接口(API)。Windows是這種方法的一個例子。
圖4.5(b)顯示了純KLT方法。內核為進程及進程內的每個線程維護上下文信息。調度由內核基于線程完成。
這種方法克服了ULT方法的兩個缺點: 首先,內核可以同時把同一個進程中的多個線程調度到多個處理器中;其次,進程中的一個線程被阻塞時,內核可以調度同一個進程中的另一個線程。KLT方法的另一個優點是,內核例程自身也可是多線程的。
與ULT方法相比,KLT方法的主要缺點是: 在把控制權從一個線程傳送到同一個進程內的另一個線程時,需要切換到內核模式。
為說明兩種方法的區別,表4.1給出了在單處理器VAX機上運行類UNIX操作系統的測量結果。表中進行了兩個基準測試,即Null Fork和Signal-Wait,前者測量創建、調度、執行和完成一個調用空過程的進程/線程的時間(即派生一個進程/線程的開銷),后者測量進程/線程給正在等待的進程/線程發信號,然后在某個條件上等待所需要的時間(即兩個進程/線程的同步時間)。可以看出,ULT和KLT之間、KLT和進程之間都有一個數量級以上的性能差距。
從表中可以看出,雖然使用KLT多線程技術與使用單線程的進程相比,速度明顯提升,但使用ULT與使用KLT相比,速度再次得以提升。速度的再次提升是否能實現,取決于應用程序的性質。應用程序中的大多數線程切換都需要內核模式訪問時,基于ULT的方案不見得會比基于KLT的方案好。
混合方法
有些操作系統提供混合ULT和KLT的方法【見圖4.5(c)】。在混合系統中,線程創建完全在用戶空間中完成,線程的調度和同步也在應用程序中進行。一個應用程序中的多個用戶級線程會被映射到一些(小于等于用戶級線程數)內核級線程上。為使整體性能最佳,程序員可為特定應用程序和處理器調節KLT的數量。
在混合方法中,同一個應用程序中的多個線程可在多個處理器上并行地運行,某個會引起阻塞的系統調用不會阻塞整個進程。設計正確時,這種方法可結合純ULT方法和純KLT方法的優點,并克服它們的缺點。
Solaris操作系統是使用混合方法的很好例子。最新版的Solaris將ULT/KLT關系限制為1:1。
三 多核和多線程
使用多核系統支持單個多線程應用程序的情況可能會出現在工作站、游戲機或正運行處理器密集型應用的個人計算機上。這時會帶來性能和應用程序設計上的一些問題。本節首先介紹在多核系統上運行的多線程應用程序性能,然后介紹采用多核系統性能設計應用程序的一個具體示例。
3.1多核系統上的軟件性能
多核組織結構帶來的性能提升,取決于應用程序有效利用并行資源的能力。我們首先介紹運行在多核系統上的單個應用程序。Amdahl定律聲稱
該定律假設程序執行時間的(1-f)分之一所涉及的代碼本質上是串行的,其余f分之一所涉及的代碼是無限并行的,并且沒有調度開銷。
該定律看上去使得多核組織結構的前景很迷人。然而,如圖4.7(a)所示,即使是一小部分串行代碼也會顯著影響性能。假設只有10%的代碼本質上是串行的(即f=0.9),那么在一個8處理器的多核系統上運行該程序也僅有4.7倍的性能提升。另外,多處理器任務調度和通信以及高速緩存一致性維護都會給軟件帶來額外的開銷。這就使得性能曲線達到峰值后便開始下降。圖4.7(b)是一個有代表性的例子。
盡管如此,軟件工程師們一直在努力解決這個問題,而且發現大量應用程序可以有效利用多核系統。研究了一系列數據庫應用程序,這些程序采取了很多措施來降低硬件組織結構、操作系統、中間件和數據庫應用軟件本身的串行部分比例。 從圖4.8中可以看出,數據庫管理系統和數據庫應用程序能有效地使用多核系統。還有許多不同類型的服務器程序能夠有效使用并行化的多核組織結構,因為服務器程序通常會并行地處理許多相對獨立的事務。
除通用服務器軟件外,其他類型的應用程序也可從多核系統中直接獲益,因為它們的吞吐量能隨著處理器核心的數量伸縮。【MCDO06】給出了如下示例:
- 原生多線程應用程序:多線程應用程序的特征是具有少數幾個高度線程化的進程。線程化應用程序的例子包括Lotus Domino和SiebelCRM(客戶關系管理)。
- 多進程應用程序: 多進程應用程序的特征是具有多個單線程的進程。多進程應用程序的例子包括Oracle數據庫、SAP和PeopleSoft。
- Java 應用程序: Java從根本上支持線程的概念。不僅Java語言本身能夠很方便地支持多線程應用程序開發,Java虛擬機也是一個多線程進程,它為Java應用程序提供調度機制和內存管理。能夠直接從多核系統資源中獲益的Java應用程序包括Sun公司的Java應用服務器、BEA公司的WebLogic、IBM公司的WebSphere和開源的Tomcat應用服務器。基于J2EE開發的所有應用程序也可直接從多核技術中獲益。
- 多實例應用程序: 即使個別應用程序未利用大量的線程來達到伸縮性,仍然可以通過并行運行多個應用程序的實例來從多核組織結構中獲益。多個應用程序實例需要一定程度上的隔離性時,可使用虛擬化技術(虛擬出支撐操作系統的硬件)為每個實例提供獨立、安全的環境。
3.2應用示例:
Valve 娛樂科技公司開發了眾多游戲和Source 游戲引擎。Source是玩家數量最多的游戲引擎之一。Valve公司在自己的游戲中使用Source引擎,并通過頒發許可證的方式來允許其他游戲開發者使用Source引擎。
近年來,Valve使用多線程技術重新編寫了Source引擎,以充分利用Intel和AMD多核處理器芯片的性能。改進后的Source引擎代碼為Valve公司的游戲如《半條命2》提供了更為強大的支持。
從Valve的角度來看,線程根據其粒度定義為如下幾種:
? 粗粒度線程:分配到各個處理器上的各個模塊(稱為系統)。在Source引擎中,一個處理器負責渲染,一個處理器負責AI(人工智能),一個處理器負責物理計算,以此類推。這種策略非常簡單。從本質上講,每個主要模塊都是單線程的,由一個時間軸線程來協調所有其他線程的同步。
? 細粒度線程:分布在多個處理器上的許多相似或相同的任務。例如,在數據數組上迭代的一個循環可分割為一些小循環,而小循環則在各個線程上并行執行。
? 混合線程:這涉及為某些系統選擇性使用細粒度線程,或為其他系統使用單個線程。
Valve公司發現,使用粗粒度線程策略時,在雙處理器上運行程序的性能是在單處理器上運行程序的性能的兩倍,但這種性能提升僅在某些專門設計的情形下才能達到。在真實的游戲環境中,性能約能提升為原來的1.2倍。Valve公司還發現有效地利用細粒度線程非常困難,因為每個工作單元耗費的時間是變化的,而且管理輸出和結果的時間軸所涉及的編程相當復雜。
Valve公司還發現,隨著8顆核心甚至16顆核心的多核系統的出現,混合線程方法最具應用前景,因此它可實現最佳的加速比。Valve識別了那些只在單處理器上運行時才非常有效率的系統。例如,混音系統幾乎不與用戶交互,它只處理自身的數據集,且不受窗口的幀配置限制。其他模塊(如場景渲染)可組織為許多線程,這類模塊既可以在單個處理器上運行,也可以在多個處理器上運行,因此能獲得更好的性能。
根據系統特性來選擇使用的單個線程或多個線程,可以有效的提高加速比。如果在可以使用多個線程的系統中使用了單個線程,性能自然會有所損失。而在應該使用單個線程的地方使用了多個線程來執行反而會使性能下降,也同時增加了編程的復雜性。
四 Linux的進程和線程管理
4.1 Linux進程
Linux中的進程或任務由一個task struct數據結構表示。task struct數據結構包含以下各類信息:
- 狀態:進程的執行狀態(執行態、就緒態、掛起態、停止態、僵死態)。這些狀態將在下面進一步講述。
- 調度信息:Linux調度進程所需要的信息。一個進程可能是普通的或實時的,并具有優先級。
- 實時進程在普通進程前調度,且在每類中使用相關的優先級。一個計數器會記錄允許進程執行的時間量。
- 標識符:每個進程都有唯一的一個進程標識符,以及用戶標識符和組標識符。組標識符用于給一組進程指定資源訪問特權。
- 進程間通信:Linux支持UNIXSVR4中的IPC機制,詳見第6章。
- 鏈接:每個進程都有一個到其父進程的鏈接及到其兄弟進程(與它有相同的父進程)的鏈接,以及到所有子進程的鏈接。
- 時間和計時器:包括進程創建的時刻和進程所消耗的處理器時間總量。一個進程可能還有一個或多個間隔計時器,進程通過系統調用來定義間隔計時器,計時器期滿時,會給進程發送一個信號。計時器可以只用一次或周期性地使用。
- 文件系統:包括指向被該進程打開的任何文件的指針和指向該進程當前目錄與根目錄的指針。
- 地址空間:定義分配給該進程的虛擬地址空間。
- 處理器專用上下文:構成該進程上下文的寄存器和棧信息。
圖4.15給出了Linux中一個進程的執行狀態,如下所示:
- 運行:這個狀態值對應于兩個狀態。一個運行進程要么正在執行,要么準備執行。
- 可中斷:這是一個阻塞態,此時進程正在等待一個事件(如一個I/O操作)的結束、一個可用的資源或另一個進程的信號。
- 不可中斷:這是另一個阻塞態。它與可中斷狀態的區別是,在不可中斷狀態下,進程正在等待一個硬件條件,因此不會接收任何信號。
- 停止:進程被中止,并且只能由來自另一個進程的主動動作恢復。例如,一個正在被調試的進程可被置于停止態。
- 僵死:進程已被終止,但由于某些原因,在進程表中仍然有其任務結構。
4.2 Linux線程
傳統UNIX系統支持在每個進程中只執行一個線程,但現代UNIX系統通常支持在一個進程中含有多個內核級線程。如同傳統UNIX系統那樣,老版本的Linux內核不支持多線程。多線程應用程序需要用一組用戶級程序庫來編寫,以便將所有線程映射到一個單獨的內核級進程中,最著名的是pthread(POSIX thread)庫。現代UNIX提供內核級線程。Linux提供一種不區分進程和線程的解決方案,這種解決方案使用一種類似于Solaris輕量級進程的方法,將用戶級線程映射到內核級進程。組成一個用戶級進程的多個用戶級線程則映射到共享同一個組ID的多個Linux內核級進程上。因此,這些進程可以共享文件和內存等資源,使得同一個組中的進程調度切換時不需要切換上下文。
在Linux中通過復制當前進程的屬性可創建一個新進程。新進程創建后,可以共享資源,如文件、信號處理程序和虛存。兩個進程共享相同的虛存時,可將它們視為一個進程中的線程。 然而,沒有給線程單獨定義數據結構,因此Linux中的進程和線程沒有區別。Linux用clone()命令代替通常的fork()命令來創建進程。該命令包含了下面所示的一組標識作為參數。傳統的fork()系統調用在Linux上是用所有克隆標志清零的clone()系統調用實現的。
克隆標識包括以下實例:
- CLONE_NEWPID:創造新的進程ID命名空間。
- CLONE PARENT:調用者及其創建的任務共享同一個父進程。
- CLONE_SYSVSEM:共享System V SEM_UNDO語義。
- CLONE_THREAD:將該進程插入父進程的同一個線程組。該標志為真時,它隱含設置了
CLONE_PARENT。 - CLONE_VM:共享地址空間(內存描述符和所有頁表)。
當Linux內核執行從一個進程到另一個進程的切換時,會檢查當前進程的頁目錄地址是否與將被調度的進程的相同。若相同,則它們共享同一個地址空間,所以此時上下文切換僅是從代碼的一處跳轉到代碼的另一處。
雖然屬于同一個進程組的克隆進程共享同一內存空間,但不能共享同一個用戶棧。所以clone()調用會為每個進程創建獨立的棧空間。
五 小結
某些操作系統區分進程和線程的概念,前者涉及資源的所有權,后者涉及程序的執行。這種方法可提高性能,方便編碼。在多線程系統中,可在一個進程內定義多個并發線程,實現方法是使用用戶級線程或內核級線程。用戶級線程對操作系統是未知的,它們由一個在進程的用戶空間中運行的線程庫創建并管理。用戶級線程非常高效,因為從一個線程切換到另一個線程不需要進行狀態切換,但一個進程中一次只有一個用戶級線程可以執行,如果一個線程發生阻塞,整個進程都會被阻塞。進程內包含的內核級線程是由內核維護的。由于內核認識它們,因而同一個進程中的多個線程可在多個處理器上并行執行,一個線程的阻塞不會阻塞整個進程,但從一個線程切換到另一個線程時需要進行模式轉換。
總結
- 上一篇: 操作系统-进程
- 下一篇: 操作系统-并发性:互斥与同步