《作業系統導論》二、 虛擬化(1)
關鍵問題:如何將資源虛擬化
我們將在本書中回答一個核心問題:作業系統如何將資源虛擬化?這是關鍵問題。
為什麼作業系統這樣做? 這不是主要問題,因為答案應該很明顯:它讓系統更易於使用。
因此我們關注如何虛擬化:作業系統通過哪些機制和策略來實現虛擬化?作業系統如何有效地實現虛擬化?需要哪些硬體支援?
4 - 程序
關鍵問題:如何提供有許多CPU的假象?
雖然只有少量的物理的CPU可用,但是作業系統如何提供幾乎有無數個CPU可用的假象?
作業系統通過虛擬化CPU提供這種假象。通過讓一個程序只執行一個時間片,然後切換到其他程序,作業系統提供了存在多個虛擬CPU的假象。這就是時分共享(time sharing)CPU技術,允許使用者如願執行多個併發程序。潛在的開銷就是效能損失,因為如果CPU必須共享,每個程序的執行就會慢一點。
機制與策略
機制是一些低階方法或協議,實現了所需的功能。
策略是在作業系統內做出某種決定的演算法。
分離策略和機制
在許多作業系統中,一個通用的設計正規化是將高階策略與低階機制分可。你可以將機制堪稱系統的"如何(how)"問題功能答案。作業系統如何執行上下文切換?策略為"哪個(which)"問題提供答案。
例如,作業系統現在應該執行哪個程序?
++將兩者分開可以輕鬆改變策略,而不必重新考慮機制,因此這是一種模組化(modularity)的形式,一種通用的軟體設計原則++
系統系統的基本的抽象 — 程序。
程序就是執行中的程式。
作業系統如何啟動並執行一個程式?程序建立實際如何進行?
作業系統執行程式必須做的第一件事是將程式碼和所有靜態資料(例如初始化變數)載入到記憶體中,載入到程序的地址空間中。
將程式碼和靜態資料載入到記憶體後,作業系統在執行此程序之間還需:為程式的執行時棧(run-time stack或stack)分配一些記憶體,C程式使用棧存放區域性變數、函式引數和返回地址。
作業系統也可能為程式的堆(heap)分配一些記憶體。在C程式中,堆用於顯式請求的動態分配資料。malloc()/free();資料結構(如連結串列、散列表、樹和其他有趣的資料結構)需要堆。起初堆很小,隨著程式執行,通過malloc()庫API請求共呢個多記憶體。
作業系統還將執行一些其他初始化任務,特別是與輸入/輸出(I/O)相關的任務。例如UNIX系統中,預設情況下每個程序都有3個開啟的檔案描述符,用於標準輸入、輸出和錯誤。
通過上述工作,OS終於為程式執行搭好了舞臺。然後執行最後一項任務:啟動程式,在入口處執行,即main()。通過跳轉到main()例程,OS將CPU的控制權轉移到建立的程序中,從而程式開始執行。
5 - 程序API
關鍵問題:如何建立並控制程序?
作業系統應該提供怎樣的程序來建立及控制介面?如何設計這些接口才能既方便又實用?
- fork()
子程序並不是完全拷貝了父程序。具體來說,雖然它擁有自己的地址空間(即擁有自己的私有記憶體)、暫存器、程式計數器等,但是它從fork()返回的值是不同的。父程序獲得的返回值是新建立子程序的PID,而子程序獲得的返回值是0.
CPU排程程式(scheduler)決定了某個時刻哪個程序被執行,由於CPU排程程式非常複雜,所以我們不能假設哪個程序會先執行。 事實表明,這種不確定性(non-determinism)會導致一些很有趣的問題,特別是在多執行緒程式(multi-threaded program)。 - wait()
用於父程序等待子程序執行完畢。 - exec()
這個系統呼叫可以讓子程序執行與父程序不同的程式。呼叫fork(),這只是在你想執行相同程式的拷貝時有用。
給定可執行程式的名稱(如wc)及需要的引數後,exec()會從可執行程式中載入程式碼和靜態資料,並用它覆寫自己的程式碼端(以及靜態資料),堆、棧及其他記憶體也會被重新初始化。然後作業系統就執行該程式,將引數從高argv傳遞給該程序。因此,它並沒有建立新程序,而是直接將當前執行的程式替換為不同的執行程式。對exec()的成功呼叫永遠不會返回。
分離fork()及exec()的作用
給shell在fork之後exec之前執行程式碼的機會,這些程式碼可以在執行新程式前改變環境,從而讓一系列有趣的功能很容易實現。
6 - 進位制:受限直接執行
在構建虛擬化時存在的挑戰
- 如何在不增加系統開銷的情況下實現虛擬化?
- 有效地執行如何有效地執行程序,同時保留對CPU的控制?
控制權對於作業系統尤為重要,因為作業系統負責資源管理。如果沒有控制權,一個程序可以簡單地無限制執行並接管機器,或訪問沒有許可權的資訊。
關鍵問題:如何高效、可控地虛擬化CPU
作業系統必須以高效能的方式虛擬化CPU,同時保持對系統的控制。為此,需要硬體和作業系統支援。作業系統通常會明智地利用硬體支援,以便高效地實現其工作。
受限直接執行
直接執行:只需直接在CPU上執行程式即可,當OS希望啟動程式執行時,它會在程序列表中為其建立一個程序條目,為其分配一些記憶體,將程式程式碼(從磁碟)載入到記憶體中,找到入口點,跳轉到哪裡,並開始執行使用者程式碼,並在稍後回到核心。
存在問題:
問題1:如果我們只執行一個程式,作業系統怎麼能確保程式不做任何我們不希望它做的事,同時仍然高效地執行它?
增加受限制操作
關鍵問題:如何執行受限制的操作
一個程序必須能夠執行I/O和其他一些受限制的操作,又不能讓程序完全控制系統。作業系統和硬體如何寫作實現這一點?
採用受保護的控制權轉移
硬體通過提供不同的執行模式來協助作業系統。在使用者模式(user mode)下,應用程式不能完全訪問硬體資源。在核心模式(kernel mode)下,作業系統可以訪問機器的全部資源。還提供了陷入(trap)核心和從陷阱返回(return-from-trap)到使用者模式程式的特別說明,以及一些指令,讓作業系統高速硬體陷阱表(trap table)在記憶體中的位置。
執行陷阱時,硬體需要小心,因為它必須確保儲存足夠的呼叫者暫存器,以便在作業系統發出從陷阱返回指令時能夠正確返回。
補充:為什麼系統呼叫看起來像過程呼叫
你可能知道,為什麼對系統呼叫的呼叫(如open() read())看起來完全就像C中的典型過程呼叫。也就是說,如果它看起來像一個過程呼叫,系統如何知道這是一個系統呼叫,並做所有正確的事情?原因很簡單:它是一個過程呼叫,但隱藏在過程呼叫內部的是著名的陷阱指令。更具體地說,當你呼叫open()時,你正在執行對C庫的過程呼叫。其中,無論時對於open()還是提供的其他系統呼叫,庫都使用與核心一致的呼叫約定來將引數放在眾所周知的位置(例如,在棧中或特定的暫存器中),將系統呼叫號也放入一個眾所周知的位置(同樣,放在棧或暫存器中),然後執行上述的陷阱指令。庫中陷阱之後的程式碼準備好返回值,並將控制權返回給發出系統呼叫的程式。因此,C庫中進行系統呼叫的部分是用於彙編手工編碼的,因為它們需要仔細遵守約定,以便正確處理引數和返回值,以及執行硬體特定的陷阱指令。現在你知道為什麼你自己不必寫彙編來陷入作業系統了,因為有人已經為你寫了這些彙編。
陷阱如何知道在OS內執行哪些程式碼
顯然,發起呼叫的過程不能指定要跳轉到的地址(就像你在進行過程呼叫時一樣),這樣做讓程式可以跳轉到核心中的任意位置,這顯然是一個糟糕的主意。
核心通過在啟動時設定陷阱表(trap table)來實現。當機器啟動時,它在特權(核心)模式下執行,因此可以根據需要自由配置機器硬體。作業系統做的第一件事,就是告訴硬體在發生某些異常事件時要執行哪些程式碼。例如,當發生硬碟終端,發生鍵盤中斷或程式進行系統呼叫時,應該執行哪些程式碼?作業系統通常通過某種特殊的指令,通知硬體這些陷阱處理程式的位置。
問題2:當我們執行一個程序時,作業系統如何讓它停下來並卻換到另一個程序,從而實現虛擬化CPU所需的時分共享?
在程序之間切換
關鍵問題:如何獲取CPU的控制權
作業系統如何獲得CPU的控制權(regain control),以便它可以在程序之間切換?
- 協作方式:等待系統呼叫
過去某些系統採用的一種方式,在這種風格下,作業系統相信系統的程序會合理執行。執行事件過長的程序被假定會定期放棄CPU,以便作業系統可以決定執行其他任務。
大多數程序通過進行系統呼叫,將CPU的控制權轉移給作業系統。提示:處理應用程式的不正當行為
作業系統通常必須處理不正當行為,這些程式通過設計(惡意)或不小心(錯誤),嘗試做某些不應該做的事情。在現代系統中,作業系統試圖處理這種不當行為的方式是簡單地終止犯罪者。
如果應用程式執行了某些非法操作,也會將控制轉移給作業系統(如除0操作)。
這種方式存在問題:如果某個程序進入無限迴圈,並且從不進行系統呼叫,會發生什麼情況? - 非協作方式:作業系統進行控制
事實證明,沒有硬體的額外幫助,如果程序拒絕進行系統呼叫(也不出錯),從而控制權將無法交還給作業系統,那麼作業系統無法做任何事情。只能重啟計算機。關鍵問題:如何在沒有寫作的情況下獲得控制權?
即使程序不協作,作業系統如何獲得CPU的控制權?作業系統可以做什麼來確保流氓程序不會佔用機器?
答案:時鐘中斷
時鐘裝置可以程式設計為每隔幾毫秒產生一次中斷。產生中斷時,當前正在執行的程序停止,作業系統中預先配置的中斷處理程式會執行。此時,作業系統重新獲得CPU的控制權。
請注意,在此協議中,有兩種型別的暫存器儲存/恢復。第一種是發生時鐘中斷的時候。在這種情況下,執行程序的使用者暫存器由硬體隱式儲存,使用該程序的核心棧。第二種是當作業系統決定從A切換到B。這種情況下,核心暫存器被軟體(即OS)明確地儲存,但這次被儲存在該程序的程序結構的內從中。後一個操作讓系統從好像剛剛由A陷入核心,變成好像剛剛由B陷入核心。