深入理解計算機系統 CSAPP


Computer Systems A Programmer's perspective

關於進程與線程的相關知識

image

進程

像hello這樣的程序在現代系統上運行時,操作系統會提供一種假象,就好像系統上只有這個程序在運行。程序看上去是獨占地使用處理器、主存和I/O設備。處理器看上去就像在不間斷地一條接一條地執行程序中的指令,即該程序的代碼和數據是系統內存中唯一的對象。這些假象是通過進程的概念來實現的,進程是計算機科學中最重要和最成功的概念之一。

進程是操作系統對一個正在運行的程序的一種抽象。在一個系統上可以同時運行多個進程,而每個進程都好像在獨占地使用硬件。而並發運行,則是說一個進程的指令和另一個進程的指令是交錯執行的。在大多數系統中,需要運行的進程數是多於可以運行它們的CPU個數的。傳統系統在一個時刻只能執行一個程序,而先進的多核處理器同時能夠執行多個程序。無論是在單核還是多核系統中,一個CPU看上去都像是在並發地執行多個進程,這是通過處理器在進程間切換來實現的。操作系統實現這種交錯執行的機制稱為上下文切換。為了簡化討論,我們只考慮包含一個 CPU的單處理器系統的情況。

操作系統保持跟蹤進程運行所需的所有狀態信息。這種狀態,也就是上下文,包括許多信息,比如PC(程序計數器)寄存器文件的當前值,以及主存的內容。在任何一個時刻,單處理器系統都只能執行一個進程的代碼。當操作系統決定要把控制權從當前進程轉移到某個新進程時,就會進行上下文切換,即保存當前進程的上下文、恢復新進程的上下文,然后將控制權傳遞到新進程。新進程就會從它上次停止的地方開始。圖1-12展示了示例hello程序運行場景的基本理念。

示例場景中有兩個並發的進程: shell進程和 hello進程。最開始,只有shell 進程在運行,即等待命令行上的輸入。當我們讓它運行hello程序時,shell通過調用一個專門的函數,即系統調用,來執行我們的請求,系統調用會將控制權傳遞給操作系統。操作系統保存shell進程的上下文,創建一個新的hello進程及其上下文,然后將控制權傳給新的hello進程。hello進程終止后,操作系統恢復shell 進程的上下文,並將控制權傳回給它,shell進程會繼續等待下一個命令行輸入。

如圖1-12所示,從一個進程到另一個進程的轉換是由操作系統內核(kernel)管理的。內核是操作系統代碼常駐主存的部分。當應用程序需要操作系統的某些操作時,比如讀寫文件,它就執行一條特殊的系統調用(system call)指令,將控制權傳遞給內核。然后內核執行被請求的操作並返回應用程序。注意,內核不是一個獨立的進程。相反,它是系統管理全部進程所用代碼和數據結構的集合。

image

異常是允許操作系統內核提供進程( process)概念的基本構造塊,進程是計算機科學中最深刻、最成功的概念之一。

在現代系統上運行一個程序時,我們會得到一個假象,就好像我們的程序是系統中當前運行的唯一的程序一樣。我們的程序好像是獨占地使用處理器和內存。處理器就好像是無間斷地一條接一條地執行我們程序中的指令。最后,我們程序中的代碼和數據好像是系統內存中唯一的對象。這些假象都是通過進程的概念提供給我們的。

進程的經典定義就是一個執行中程序的實例。系統中的每個程序都運行在某個進程的上下文(context)中。上下文是由程序正確運行所需的狀態組成的。這個狀態包括存放在內存中的程序的代碼和數據,它的棧、通用目的寄存器的內容、程序計數器、環境變量以及打開文件描述符的集合。

每次用戶通過向shell輸人一個可執行目標文件的名字,運行程序時,shell就會創建一個新的進程,然后在這個新進程的上下文中運行這個可執行目標文件。應用程序也能夠創建新進程,並且在這個新進程的上下文中運行它們自己的代碼或其他應用程序。

關於操作系統如何實現進程的細節的討論超出了本書的范圍。反之,我們將關注進程提供給應用程序的關鍵抽象:

  • 一個獨立的邏輯控制流,它提供一個假象,好像我們的程序獨占地使用處理器。
  • 一個私有的地址空間,它提供一個假象,好像我們的程序獨占地使用內存系統。讓我們更深入地看看這些抽象。

邏輯控制流

即使在系統中通常有許多其他程序在運行,進程也可以向每個程序提供一種假象,好像它在獨占地使用處理器。如果想用調試器單步執行程序,我們會看到一系列的程序計數器(PC)的值,這些值唯一地對應於包含在程序的可執行目標文件中的指令,或是包含在運行時動態鏈接到程序的共享對象中的指令。這個PC值的序列叫做邏輯控制流,或者簡稱邏輯流。

image-20220219152306719

虛擬內存

虛擬內存是一個抽象概念,它為每個進程提供了一個假象,即每個進程都在獨占地使用主存。每個進程看到的內存都是一致的,稱為虛擬地址空間。圖1-13所示的是Linux進程的。

虛擬地址空間(其他Unix系統的設計也與此類似)。在Linux中,地址空間最上面的區域是保留給操作系統中的代碼和數據的,這對所有進程來說都是一樣。地址空間的底部區域存放用戶進程定義的代碼和數據。請注意,圖中的地址是從下往上增大的。

image

每個進程看到的虛擬地址空間由大量准確定義的區構成,每個區都有專門的功能。在本書的后續章節你將學到更多有關這些區的知識,但是先簡單了解每一個區是非常有益的。我們從最低的地址開始,逐步向上介紹。

  • 程序代碼和數據。對所有的進程來說,代碼是從同一固定地址開始,緊接着的是和.C全局變量相對應的數據位置。代碼和數據區是直接按照可執行目標文件的內容初始化的,在示例中就是可執行文件 hello。在第9章我們研究鏈接和加載時,你會學習更多有關地址空間的內容。

  • 。代碼和數據區后緊隨着的是運行時堆。代碼和數據區在進程一開始運行時就被指定了大小,與此不同,當調用像malloc和 free這樣的C標准庫函數時,堆可以在運行時動態地擴展和收縮。在第9章學習管理虛擬內存時,我們將更詳細地研究堆。

  • 共享庫。大約在地址空間的中間部分是一塊用來存放像C標准庫和數學庫這樣的共享庫的代碼和數據的區域。共享庫的概念非常強大,也相當難懂。在第﹖章介紹動態鏈接時,將學習共享庫是如何工作的。

  • 。位於用戶虛擬地址空間頂部的是用戶棧,編譯器用它來實現函數調用。和堆一樣,用戶棧在程序執行期間可以動態地擴展和收縮。特別地,每次我們調用一個函數時,棧就會增長;從一個函數返回時,棧就會收縮。在第3章中將學習編譯器是如何使用棧的。

  • 內核虛擬內存。地址空間頂部的區域是為內核保留的。不允許應用程序讀寫這個區域的內容或者直接調用內核代碼定義的函數。相反,它們必須調用內核來執行這些操作。

虛擬內存的運作需要硬件和操作系統軟件之間精密復雜的交互,包括對處理器生成的每個地址的硬件翻譯。基本思想是把一個進程虛擬內存的內容存儲在磁盤上,然后用主存作為磁盤的高速緩存。第9章將解釋它如何工作,以及為什么對現代系統的運行如此重要。

用戶模式和內核模式

為了使操作系統內核提供一個無懈可擊的進程抽象,處理器必須提供一種機制,限制一個應用可以執行的指令以及它可以訪問的地址空間范圍。
處理器通常是用某個控制寄存器中的一個模式位(mode bit)來提供這種功能的,該寄存器描述了進程當前享有的特權。當設置了模式位時,進程就運行在內核模式中(有時叫做超級用戶模式)。一個運行在內核模式的進程可以執行指令集中的任何指令,並且可以訪問系統中的任何內存位置。

沒有設置模式位時,進程就運行在用戶模式中。用戶模式中的進程不允許執行特權指令( privileged instruction),比如停止處理器、改變模式位,或者發起一個I/O操作。也不允許用戶模式中的進程直接引用地址空間中內核區內的代碼和數據。任何這樣的嘗試都會導致致命的保護故障。反之,用戶程序必須通過系統調用接口間接地訪問內核代碼和數據。

運行應用程序代碼的進程初始時是在用戶模式中的。進程從用戶模式變為內核模式的唯一方法是通過諸如中斷、故障或者陷入系統調用這樣的異常。當異常發生時,控制傳遞到異常處理程序,處理器將模式從用戶模式變為內核模式。處理程序運行在內核模式中,當它返回到應用程序代碼時,處理器就把模式從內核模式改回到用戶模式。

Linux提供了一種聰明的機制,叫做/proc文件系統,它允許用戶模式進程訪問內核數據結構的內容。/proc文件系統將許多內核數據結構的內容輸出為一個用戶程序可以讀的文本文件的層次結構。比如,你可以使用/proc文件系統找出一般的系統屬性,比如CPU類型(/proc/cpuinfo),或者某個特殊的進程使用的內存段(/proc/<process-id>/maps)。2.6版本的 Linux內核引入/sys文件系統,它輸出關於系統總線和設備的額外的低層信息。

上下文切換

操作系統內核使用一種稱為上下文切換(context switch)的較高層形式的異常控制流來實現多任務。上下文切換機制是建立在8.1節中已經討論過的那些較低層異常機制之上的。

內核為每個進程維持一個上下文(context)。上下文就是內核重新啟動一個被搶占的進程所需的狀態。它由一些對象的值組成,這些對象包括通用目的寄存器浮點寄存器程序計數器用戶棧狀態寄存器內核棧各種內核數據結構,比如描述地址空間的頁表包含有關當前進程信息的進程表,以及包含進程已打開文件的信息的文件表

在進程執行的某些時刻,內核可以決定搶占當前進程,並重新開始一個先前被搶占了的進程。這種決策就叫做調度(scheduling),是由內核中稱為調度器(scheduler)的代碼處理的。當內核選擇一個新的進程運行時,我們說內核調度了這個進程。在內核調度了一個新的進程運行后,它就搶占當前進程,並使用一種稱為上下文切換的機制來將控制轉移到新的進程

上下文切換

1)保存當前進程的上下文

2)恢復某個先前被搶占的進程被保存的上下文

3)將控制傳遞給這個新恢復的進程。

當內核代表用戶執行系統調用時,可能會發生上下文切換。如果系統調用因為等待某個事件發生而阻塞,那么內核可以讓當前進程休眠,切換到另一個進程。比如,如果一個read系統調用需要訪問磁盤,內核可以選擇執行上下文切換,運行另外一個進程,而不是等待數據從磁盤到達。另一個示例是sleep系統調用,它顯式地請求讓調用進程休眠。一般而言,即使系統調用沒有阻塞,內核也可以決定執行上下文切換,而不是將控制返回給調用進程。

中斷也可能引發上下文切換。比如,所有的系統都有某種產生周期性定時器中斷的機制,通常為每1毫秒或每10毫秒。每次發生定時器中斷時,內核就能判定當前進程已經運行了足夠長的時間,並切換到一個新的進程。

圖8-14展示了一對進程A和B之間上下文切換的示例。在這個例子中,進程A初始運行在用戶模式中,直到它通過執行系統調用read陷入到內核。內核中的陷阱處理程序請求來自磁盤控制器的DMA傳輸,並且安排在磁盤控制器完成從磁盤到內存的數據傳輸后,磁盤中斷處理器。

image

進程的優劣

對於在父、子進程間共享狀態信息,進程有一個非常清晰的模型:共享文件表,但是不共享用戶地址空間。進程有獨立的地址空間既是優點也是缺點。這樣一來,一個進程不可能不小心覆蓋另一個進程的虛擬內存,這就消除了許多令人迷惑的錯誤——這是一個明顯的優點。

另一方面,獨立的地址空間使得進程共享狀態信息變得更加困難。為了共享信息,它們必須使用顯式的IPC(進程間通信)機制。基於進程的設計的另一個缺點是,它們往往比較慢,因為進程控制和IPC的開銷很高。

線程

到目前為止,我們已經看到了兩種創建並發邏輯流的方法。在第一種方法中,我們為每個流使用了單獨的進程。內核會自動調度每個進程,而每個進程有它自己的私有地址空間,這使得流共享數據很困難。在第二種方法中,我們創建自己的邏輯流,並利用I/O多路復用來顯式地調度流。因為只有一個進程,所有的流共享整個地址空間。本節介紹第三種方法—基於線程,它是這兩種方法的混合。

線程(thread)就是運行在進程上下文中的邏輯流。在本書里迄今為止,程序都是由每個進程中一個線程組成的。但是現代系統也允許我們編寫一個進程里同時運行多個線程的程序。線程由內核自動調度。每個線程都有它自己的線程上下文(thread context),包括一個唯一的整數線程ID(Thread ID,TID)棧指針、程序計數器、通用目的寄存器條件碼所有的運行在一個進程里的線程共享該進程的整個虛擬地址空間

基於線程的邏輯流結合了基於進程和基於I/O多路復用的流的特性。同進程一樣,線程由內核自動調度,並且內核通過一個整數ID來識別線程。同基於I/O多路復用的流一樣,多個線程運行在單一進程的上下文中,因此共享這個進程虛擬地址空間的所有內容,包括它的代碼、數據、堆、共享庫和打開的文件。

線程執行模型

多線程的執行模型在某些方面和多進程的執行模型是相似的。思考圖12-12中的示例。每個進程開始生命周期時都是單一線程,這個線程稱為主線程(main thread)。在某一時刻,主線程創建一個對等線程(peer thread),從這個時間點開始,兩個線程就並發地運行。最后,因為主線程執行一個慢速系統調用,例如read或者sleep,或者因為被系統的間隔計時器中斷,控制就會通過上下文切換傳遞到對等線程。對等線程會執行一段時間,然后控制傳遞回主線程,依次類推。

在一些重要的方面,線程執行是不同於進程的。因為一個線程的上下文要比一個進程的上下文小得多,線程的上下文切換要比進程的上下文切換快得多。另一個不同就是線程不像進程那樣,不是按照嚴格的父子層次來組織的。和一個進程相關的線程組成一個對等(線程)池,獨立於其他線程創建的線程。主線程和其他線程的區別僅在於它總是進程中第一個運行的線程。對等(線程)池概念的主要影響是,一個線程可以殺死它的任何對等線程,或者等待它的任意對等線程終止。另外,每個對等線程都能讀寫相同的共享數據。

線程內存模型

一組並發線程運行在一個進程的上下文中。每個線程都有它自己獨立的線程上下文,包括線程ID、棧、棧指針、程序計數器、條件碼和通用目的寄存器值。每個線程和其他線程一起共享進程上下文的剩余部分。這包括整個用戶虛擬地址空間,它是由只讀文本(代碼)、讀/′寫數據、堆以及所有的共享庫代碼和數據區域組成的。線程也共享相同的打開文件的集合。

從實際操作的角度來說,讓一個線程去讀或寫另一個線程的寄存器值是不可能的。另一方面,任何線程都可以訪問共享虛擬內存的任意位置。如果某個線程修改了一個內存位置,那么其他每個線程最終都能在它讀這個位置時發現這個變化。因此,寄存器是從不共享的,而虛擬內存總是共享的。

各自獨立的線程棧的內存模型不是那么整齊清楚的。這些棧被保存在虛擬地址空間的棧區域中,並且通常是被相應的線程獨立地訪問的。我們說通常而不是總是,是因為不同的線程棧是不對其他線程設防的。所以,如果一個線程以某種方式得到一個指向其他線程棧的指針,那么它就可以讀寫這個棧的任何部分。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM