doraemon

doraemon

let's go write some rusty code and revolutionize the world!

CPU和内存的虛擬化

為什麼要虛擬化 CPU 和內存#

虛擬化內存使應用程式編程變得簡單,操作系統給程式提供了一種假象,程式仿佛擁有一個很大的獨立地址空間,而不需要考慮實際小而擁擠的物理內存是如何分配的。其次可以保護其他程式的內存不被惡意程式修改。

虛擬化 CPU 使得操作系統可以使用調度算法,保證在多程式執行時,CPU 不會被某一個程式單獨佔用。

虛擬內存的 3 個目標:透明,效率和保護。

僵屍狀態#

一個進程可以處於已退出但尚未清理的最終(final)狀態(在基於 UNIX 的系統中,這稱為僵屍狀態)。這個最終狀態非常有用,因為它允許其他進程(通常是創建進程的父進程)檢查進的返回代碼,並查看剛剛完成的進程是否成功執行(通常,在基於 UNIX 的系統中,程成功完成任務時返回零,否則返回非零)。

shell 的調用過程,fork,exec#

shell 也是一個用戶程式,它首先顯示一個提示符(prompt),然後等待用戶輸入。你可以向它輸入一個命令(一個可執行程式的名稱及需要的參數),大多數情況下,shell 可以在文件系統中找到這個可執行程式,調用 fork () 創建新進程,並調用 exec () 的某個變體來執行這個可執行程式,調用 wait () 等待該命令完成。子進程執行結束後,shell 從 wait () 返回並再次輸出一個提示符,等待用戶輸入下一條命令。

fork 和 exec 的分離,讓 shell 可以方便地實現很多有用的功能。比如:

wc p3.c > newfile.txt

在上面的例子中,wc 的輸出結果被重定向(redirect) 到文件 newfile.bxt 中(通過 newfile.at 之前的大於號來指明重定向)。shell 實現結果重定向的方式也很簡單,當完成子進程的創建後,shell 在調用 exec () 之前先關閉了標準輸出(slandard output),打開了文件 newfile.txt。這樣,即將運行的程式 wc 的輸出結果就被發送到該文件,而不是打印在屏幕上。

CPU 虛擬化的關鍵底層機制,受限直接執行#

系統調用的過程,trap 指令,陷阱表#

要執行系統調用,程式必須執行特殊的陷阱(trap)指令。該指令同時跳入內核並將特權級別提升到內核模式。一旦進入內核,系統就可以執行任何需要的特權操作(如果允許),從而為調用進程執行所需的工作。完成後,操作系統調用一個特殊的從陷阱返回(return-from-trap)指令,如你期望的那樣,該指令返回到發起調用的用戶程式中,同時將特權級別降低,回到用戶模式。

還有一個重要的細節沒討論:陷阱 trap 如何知道在 OS 內運行哪些程式?

顯然,發起調用的過程不能指定要跳轉到的地址(就像你在進行過程調用時一樣),這樣做讓程式可以跳轉到內核中的任意位置,這顯然是一個糟糕的主意。實際上,這種能力很可能讓一個狡猾的程式員令內核運行任意程式序列。因此內核必須謹慎地控制在陷阱上執行的程式。

內核通過在啟動時設置陷阱表(trap table)來實現。當機器啟動時,它在特權(內核)模式下執行,因此可以根據需要自由配置機器硬件。操作系統做的第一件事,就是告訴硬件在發生某些異常事件時要運行哪些程式。

例如,當發生硬碟中斷,發生鍵盤中斷或程式進行系統調用時,應該運行哪些程式?操作系統通常通過某種特殊的指令,通知硬件這些陷阱處理程式的位置。一旦硬件被通知,它就會記住這些處理程式的位置,直到下一次重新啟動機器,並且硬件知道在發生系統調用和其他異常事件時要做什麼(即跳轉到哪段程式)。

最後再插一句:能夠執行指令來告訴硬件陷阱表的位置是一個非常強大的功能。因此,你可能已經猜到,這也是一項特權(privileged)操作。如果你試圖在用戶模式下執行這個指令,硬件不會允許。

協作調度系統中#

協作調度系統中,OS 通過等待系統調用,或某種非法操作發生,從而重新獲得 CPU 的控制權

非協作調度系統中#

非協作調度系統中,OS 通過等待時鐘中斷,系統調用,或某種非法操作發生,從而重新獲得 CPU 的控制權,由操作系統的調度算法決定接下來運行什麼程式。

虛擬內存#

基於硬件的動態重定位#

每個 CPU 需要兩個寄存器,基址寄存器和界限寄存器。編寫和編譯程式時,假設地址空間從 0 開始,當程式真正執行時,操作系統會決定其在物理內存中的實際加載地址,並將起始地址記錄在基址寄存器中。當進程運行時,該進程產生的所有內存引用,都會被硬件處理為物理地址 = 虛擬地址 + 基址。這種方法存在內存碎片 (已分配的空間程式內部卻未使用)。

分段,泛化的基址 / 界限#

典型的地址空間有 3 個不同的邏輯段,程式碼,堆棧和堆,將這 3 個部分分成 3 個段放在物理內存中,每個段中有一組基址 / 界限寄存器,硬件在轉換虛擬地址時使用段寄存器。

例如一個段寄存器前 2 位標識虛擬地址所屬的段,剩餘 12 位表示段內偏移量,此時物理地址 = 偏移量 + 段對應的基址,界限寄存器的值用於檢驗是否發生段溢出。每個段中除了基址 / 界限寄存器外,硬件還需要知道段增長的方向 (比如堆棧可能是反向增長)。

分段帶來的問題是,物理內存很快充滿了許多空閒空間的小洞,因而很難分配給新的段,或擴大已有的段。這種問題被稱為外部碎片 (external fragmentation)。

分頁#

分頁,即將一個進程的地址空間分割成固定大小的單元,每個單元稱為一頁。相應的,把物理內存分割成頁框,每個這樣的頁框包含一個虛擬內存頁。

為了記錄地址空間的每個虛擬頁放在物理內存中的位置,操作系統通常為每個進程保存一個數據結構,稱為頁表 (page table)。

頁表的主要作用是為地址空間的每個虛擬頁保存地址轉換,從而讓我們知道每個頁在物理內存中的位置。頁表是每個進程一個 (per-process) 的數據結構。

為了轉換該過程生成的虛擬地址,我們必須首先將它分成兩個組件:虛擬頁面號(virtual page number,VPN)和頁內的偏移量(offset)。假設進程的虛擬地址空間是 64 字節,我們的虛擬地址總共需要 6 位($2^6=64$)。若頁大小設置為 16 字節,則頁內的偏移量需要佔 4 位 ($2^4=16$),剩餘的兩位劃分給 VPN ($2^2$ 可以表示 4 頁,$4*16=64$) 。

在進行地址轉換時,操作系統查詢頁表將 VPN 轉換為 PFN (物理頁號),偏移量保持不變。

頁表就是一種數據結構,用於將虛擬地址映射到物理地址,因此,任何數據結構都可以採用。最簡單的形式稱為線性頁表(linear page table),就是一個數組。操作系統通過虛擬頁號(VPN)檢素該數組,並在該索引處查找頁表項 (PTE),以便找到期望的物理頁號(PFN)。

頁表項 PTE 中有有效位,保護位,存在位等等,當然還有最重要的物理頁號 (PFN)。

分頁的問題#

線性頁表占用的內存很大,因為不論虛擬頁 VPN 是否被映射到物理地址都在頁表中存儲了。

此外,對於每個內存引用(無論是取指令還是顯式加載或存儲),分頁都需要我們執行一個額外的內在引用,以便首先從頁表中獲取地址轉換。額外的內存引用開銷很大,在這種情況下,可能會使進程減慢兩倍或更多。

所以,有兩個必須解決的實際問題。如果不仔細設計硬件和軟件,頁表會導致系統運行速度過慢,並占用太多內存。

快速地址轉換 - TLB#

TLB (Translation Look-aside Buffer) 是基於硬件的緩存,可以解決地址轉換慢的問題,硬件轉換虛擬地址時首先查看 TLB 是否命中,未命中再去訪問內存中的頁表。

TLB 中包含的虛擬到物理的地址映射只對當前進程有效,對其他進程是沒有意義的。所以在發生進程上下文切換時,硬件或操作系統(或二者)必須注意確保即將運行的進程不要誤讀了之前進程的地址映射。

  1. 簡單的清空 TLB。
  2. 在 TLB 中添加一個地址空間標識符 (標識屬於哪個進程)。

多級頁表#

頁表占用的內存很大,可以用一種簡單的方法減小頁表大小,即使用更大的頁 (VPN 數量變少,頁內偏移量增大),但更大的頁意味著會有較多的內存碎片。另一個個辦法是使用分段和分頁雜交的方式。

我們說頁表是一個數據結構,我們完全可以使用哈希表或者樹的方式來存儲有效的的頁表項 (PTE)。當然使用二叉樹的代價比較大,在查找,過程中會多次加載內存。

現代操作系統常使用多級頁表,可以看成是一種簡單的哈希表 (事實上線性表數組也是一種哈希表),將普通的線性頁表分成頁大小的單元,然後將這些單元放進一個新的線性表,取名為頁目錄。這是一種以時間換空間的方法,若使用二級頁表,當 TLB 未命中時,需要先從內存查詢頁目錄,再從頁目錄對應的線性表查詢需要的 PTE。得到 PTE 共加載內存 2 次,通過 PTE 得到物理地址後再訪問數據還需要加載一次內存,成本相比於簡單線性表更高,好在我們還有 TLB。

載入中......
此文章數據所有權由區塊鏈加密技術和智能合約保障僅歸創作者所有。