你已經懂的那顆螺絲,再轉一圈
你花了整門課看著同一個想法不斷重演:拿一個真實、稀缺、難搞的資源,發給每個使用者一份乾淨私密的錯覺。核心給每個行程一個虛擬位址空間,讓它以為自己擁有全部的記憶體;它透過一次脈絡切換給每個執行緒一片 CPU,讓每個都以為自己獨自執行。機器虛擬化就是同一顆螺絲再多轉一整圈。我們不是騙一個行程以為自己擁有整台機器,而是騙一整個作業系統以為自己擁有裸機——自己的 CPU、自己的 RAM、自己的磁碟——而事實上它只是一個房客,與另外好幾個作業系統共用一台真實的電腦。
執行這個更大錯覺的軟體就是 hypervisor,也叫做虛擬機器監督器(virtual machine monitor,VMM)——同一樣東西的兩個名字,後者比較老、也比較精確。每個被騙的作業系統,連同它自己的核心與自己的程式,就是一個跑在虛擬機器(VM)裡面的客體(guest)。真正在執行這場欺騙的硬體與軟體則是宿主(host)。所以這一級恰好在你一直以來研究的核心底下加了一層:你以前想的是 硬體 → 核心 → 行程,現在你想的是 硬體 → hypervisor →(客體核心 → 客體行程),好幾個客體並排在一起。記住這幅圖;這一級的每一篇,都是「這一層新東西如何完成它的把戲」的一個細節。
這為什麼會難?兩個核心都想要 ring 0
問題的核心結在這裡。回想 OS 那一級:硬體強制執行特權環——一般程式碼跑在沒有特權的使用者模式(x86 上是 ring 3),只有核心跑在最高特權的 ring 0,在那裡它可以動分頁表、遮罩中斷、執行那些控制機器的危險指令。那個模型假設了一台機器一個核心。但一個客體是一整個作業系統:它的核心當初是預期自己擁有 ring 0 而寫的。現在我們有了兩個核心——客體的、和宿主的 hypervisor——而硬體只有一個 ring 0。它們不能都坐在裡面,因為那樣客體就能伸手越過那道帘子、奪走真正的機器,整個錯覺就垮了。
所以 hypervisor 必須把 ring 0 留給自己,並把客體核心降級到較低的特權。客體核心不知道自己被降級了,於是一如既往地不斷試圖執行特權指令——「關閉中斷」、「載入這個分頁表基底」——彷彿它還擁有機器。經典的答案是 trap-and-emulate(陷入並模擬):當被降級的客體企圖執行一個特權指令時,硬體拒絕並陷入(trap)——這正是一個系統呼叫背後那同一個陷入機制,一次跳進更高特權 hypervisor 的控制轉移。接著 hypervisor 看看客體本來想做什麼,在一份安全的私有機器狀態副本上替它執行它意圖的效果,然後把控制權交還,彷彿什麼異常都沒發生。客體永遠不會知道它沒碰到真實硬體。trap-and-emulate 的完整機制是下一篇;在這裡我們只需要知道它存在,以及那條說「它何時就足夠」的法則。
hypervisor 可以住的兩個地方:type-1 與 type-2
現在來到實務上的岔路。hypervisor 需要 ring 0,但它和裸機之間擺著什麼,容許兩種設計,而 type-1 對 type-2 的區別,就只是你選了哪一種。一個 type-1(或稱「裸機」)hypervisor 本身就是那個貼著金屬的東西:它最先開機、從一開始就擁有 ring 0、而它自己就是一個小而專門的核心,唯一的工作就是承載客體。它底下沒有一個通用作業系統。想想 Xen、VMware ESXi 或 Microsoft Hyper-V 這類產品——雲端供應商在伺服器上跑的那種東西,機器上的每一個作業系統都是客體,沒有一個你會登入進去的「正常」OS。
一個 type-2(或稱「託管」)hypervisor 是相反的安排:一個正常的作業系統——Linux、macOS、Windows——照常最先開機、擁有金屬,而 hypervisor 作為宿主 OS 之上的一支程式執行,把磁碟、網路、排程這些粗活交給宿主核心去扛。想想 VirtualBox 或 VMware Workstation:你在筆電上啟動的一支應用程式,它開出一個視窗,裡面跑著另一整個 OS。最乾淨的現代例子是 Linux 的 KVM,它透過一個驅動程式把普通的 Linux 核心本身變成一個 hypervisor,於是同一台機器既是你每天用的桌機、又同時是一個 VM 宿主。這兩類之間的界線可以變模糊——KVM 常被叫做「偏 type-1」,因為 hypervisor 的邏輯就住在宿主核心裡面——但定義這個分野的問題很鋒利:hypervisor 是在裸機上開機(type-1),還是跑在一個既存的通用 OS 之上(type-2)?
TYPE-1 (bare-metal) TYPE-2 (hosted)
---------------------- ----------------------
[ guest OS ] [ guest OS ] [ guest OS ] [ apps ]
-------------------------- ----------------+-----
| hypervisor | | hypervisor (a program) |
| (small kernel, ring 0) | --------------------------
-------------------------- | host OS kernel |
| bare hardware | --------------------------
-------------------------- | bare hardware |
--------------------------VMM 必須偽造什麼,一個器官一個器官地看
把監督器必須偽造什麼講具體,會很有幫助,因為一台真實電腦不是一個資源、而是好幾個,而客體把它們全都預期了。有三個大器官。第一是 CPU:客體必須看起來在自己的處理器上以全速跑自己的指令,包括我們看到它其實無法執行的那些特權指令——那就是 trap-and-emulate 的問題,光靠它本身在硬體幫忙之前太慢。第二是記憶體:客體核心建起它自己的分頁表,相信它那些「實體」位址是真的 RAM,但它們不是——它們本身又是虛擬的,必須有第二層轉譯,把客體心目中的實體記憶體映射到宿主真正的實體 RAM 上。第三是裝置:磁碟、網路卡、計時器——客體預期能跟一些對它而言並不真正存在的硬體對話。
每個器官都是接下來的一篇,所以就讓這成為你這一級的地圖。對 CPU 來說,純 trap-and-emulate 代價太高,於是晶片廠加進了硬體輔助虛擬化——Intel 的 VT-x、AMD 的 AMD-V——它給 CPU 一個獨立的客體模式,讓客體核心能在「看起來像 ring 0」的東西裡執行、卻不擁有真正那個,只在一個特殊的 VM exit 時才掉進 hypervisor;那是第 2 篇。對記憶體來說,那第二層轉譯不是靠軟體的影子分頁表、就是靠硬體的巢狀分頁(Intel EPT、AMD NPT);那是第 3 篇。對裝置來說,與其逐位元模擬一張真正的卡,客體與宿主可以協議出一個快速的合作介面——半虛擬化與 virtio 標準——那是第 4 篇。而第 5 篇橫跨到一個更輕量的表親,容器,在那裡客體根本不拿到自己的核心。
誠實的侷限,以及為何有人費這個事
現在來到誠實的部分,因為虛擬化確實強大、但它不是免費的,假裝不是這樣會養出壞的直覺。每一次陷入 hypervisor 都花掉真實的時間——一次控制轉移、狀態的存與還、監督器自己的工作——所以一個成天就只是猛敲特權指令或硬體的工作負載,在 VM 裡可能明顯比在裸機上慢。這一級的整條弧線,就是一場把那筆額外開銷縮小的追逐:硬體客體模式以避免陷入、巢狀分頁以避免軟體走分頁表、半虛擬裝置以避免忠實地模擬晶片。有了這些,一個守規矩的客體在一般工作上能跑到接近原生速度——但是「接近」,不是「相等」,而那道差距,恰恰在那些必須一再越進監督器的特權、重 I/O、或重中斷的程式碼上會拉大。
那為什麼還要付出任何代價?因為這個錯覺替你買到一些裸機給不了的東西。隔離:一個當掉、或被攻破的客體,被封死在它的 VM 裡,碰不到宿主、也碰不到鄰居——這遠比行程邊界更強,因為連客體的核心都被關住了。整併:一台大的實體伺服器可以同時承載許多客體,每個都以為自己有一整台電腦,於是閒置的容量被共享、而不是被浪費——這是所有雲端運算底下的經濟引擎。彈性:因為一個 VM 就只是 hypervisor 在管理的狀態,你可以對它拍快照、複製它、甚至把一個執行中的客體搬到另一台實體宿主而幾乎沒有頓挫。如果一個 OS 被焊死在一塊金屬上,這些都辦不到。這就是這整一級的交易:一筆可衡量、且不斷縮小的效能成本,換來隔離、整併、和一台你能抱起來就走的機器。