作業系統,本來就是個幻術大師
到目前為止,你已經一遍又一遍看著核心做同一件事:遞給每個程式一個讓人信以為真的謊。一個行程以為自己獨佔了 CPU,但其實是排程器把一顆處理器切給了好多個行程。一個程式以為自己獨佔了一大片私有的記憶體,但其實是虛擬記憶體靠著一張分頁表在哄它,而好幾個程式其實共用著同一塊 RAM。一個檔案看起來是一條整整齊齊的位元組流,底下卻是一大攤散亂的磁碟區塊。這座階梯的每一階,說穿了,講的都是同一門手藝:在比較雜亂的硬體上,端出一個乾淨的軟體幻覺。
虛擬化(virtualization)把同一個想法拿來,往上瞄準了一層樓。我們不再是給一個行程「它有自己記憶體」的幻覺,而是給一整套作業系統「它有自己一整台電腦」的幻覺——CPU、記憶體、磁碟、網路卡,全套都有。這個純粹用軟體蓋出來的、一台完整機器的幻覺,就叫虛擬機(virtual machine,VM)。裡頭跑著一套真實、未經改動的作業系統——我們叫它客體(guest)——它快快樂樂地開機、排程、分頁,從不懷疑它看見的那些「硬體」,本身不過是跑在一台真實機器(主機,host)上的另一個程式。
認識超管理器:給核心們用的核心
是誰在維持這場宏大的幻覺?是一層新的軟體,叫超管理器(hypervisor),它還有個比較老、比較直白的名字叫虛擬機監視器(virtual machine monitor,VMM)。最乾淨的想像方式是:超管理器之於整套作業系統,就像一般核心之於一般行程。核心把一顆 CPU 多工分給許多行程、並讓它們彼此隔離;超管理器則把一台實體機器多工分給許多客體作業系統、並讓「它們」彼此隔離。它,說得一字不差,就是一個核心,只不過它的「行程」本身是一整套一整套完整的核心。
回想一下雙模式運作:CPU 要嘛跑在有特權的核心模式、要嘛跑在受限的使用者模式,而少數被嚴加看管的特權指令(停掉 CPU、改寫分頁表、跟裝置說話)只有在核心模式下才被允許。虛擬化把這件事漂亮地複雜化了。客體作業系統以為自己身在核心模式,於是去執行那些特權指令——但絕不能讓它真的碰到實體硬體,否則它可能搞砸主機與其他客體。所以超管理器坐在那個貨真價實、最有特權的位子上,讓客體以為自己有特權,然後悄悄攔下每一個危險的動作。下一篇導覽會把這個攔截到底怎麼運作完整攤開來,它的名字叫陷阱與模擬(trap-and-emulate)。
兩種立足點:裸機與寄宿
超管理器有兩種口味,差別很簡單,就在於它站在什麼上面。第一型超管理器——叫裸機型(bare-metal)——直接跑在硬體上,底下什麼都沒有;它「就是」那台機器上最底層的軟體,客體作業系統們疊在它上頭。根本沒有一套獨立的主機作業系統;超管理器自己扮演了那個角色。這就是嚴肅的資料中心裡跑的東西(VMware ESXi、Xen、Microsoft Hyper-V,以及 Linux 裡的 KVM 核心),因為最貼近金屬,意味著最少的額外負擔、與最緊的掌控。
第二型超管理器——叫寄宿型(hosted)——是當作一支普通的應用程式,跑在一套先開機的正常作業系統「裡面」。當你在筆電上啟動 VirtualBox、VMware Workstation 或 Parallels,你日常用的作業系統(Windows 或 macOS)就是主機,而超管理器只是它排程的另一個程式,就像瀏覽器那樣。那個程式接著變出一台虛擬機,一套客體作業系統就在裡頭開機。這份方便的代價,是多了一層:客體的請求得先穿過超管理器、「然後」再穿過主機作業系統,才能抵達硬體,所以第二型的擺設通常比裸機型稍微慢一點點。回報則是:你可以在你的 Mac 上跑一套 Linux 客體,同時你的 Mac 還繼續做著其他一切。
TYPE 1 (bare-metal) TYPE 2 (hosted)
+---------+ +---------+ +---------+ +---------+
| guest | | guest | | guest | | guest |
| OS A | | OS B | | OS A | | OS B |
+---------+ +---------+ +---------+ +---------+
| HYPERVISOR | | HYPERVISOR | <- an app
+--------------------+ +--------------------+
| HARDWARE | | HOST OS (Win/mac) |
+--------------------+ +--------------------+
| HARDWARE |
+--------------------+為什麼要費這勁?為幻覺辯護
這一整套機制聽起來代價不小,所以直白地問一句標題的問題很合理:到底為什麼要虛擬化?最老的理由是整併(consolidation)。一台只拿來跑一支應用程式的實體伺服器,通常只用到了它一小部分的能耐就閒著了——大量的 CPU、RAM 與磁碟全擱著沒用。把十台客體虛擬機塞進一台強壯的機器,你就把那些浪費收了回來,砍掉了機器的數量、電費,以及機房的地板空間。光是這一個經濟上的論點,就是為什麼早在誰開口說出「雲端」這個詞之前,虛擬化就已經悄悄接管了資料中心。
但整併只是起點,因為虛擬機是軟體,而軟體能被複製、暫停、搬移,方式是真實硬體永遠做不到的。隔離(isolation):一個客體裡的當機、資安入侵或失控的程式,會被封死在裡頭,沒辦法越界去碰它的鄰居或主機。快照(snapshot):你可以把一台虛擬機的整個狀態凍結成一個檔案,然後在幾秒內回滾——這對測試有風險的改動是天大的恩賜。封裝(encapsulation):一整台執行中的伺服器,變成一個你能複製或寄送的、可搬移的檔案。還有老舊系統支援(legacy support):在它的硬體早已化為塵土之後很久,你還能把一套古老的作業系統養在虛擬機裡繼續活著。
最有魔力的那份回報,值得擁有自己的名字:即時遷移(live migration)。正因為一台虛擬機不過就是「狀態」——它的記憶體、它的暫存器、它的虛擬裝置——超管理器可以在客體還在執行的同時,把那份狀態透過網路複製到「第二台」實體機器,然後切換得如此平順,客體根本沒察覺自己換了身體。雲端業者就是這樣在沒人的網站眨一下眼的情況下,把一台伺服器淨空來做維護。我們會在後面的導覽回頭講這個機制;現在,只要驚嘆一下:把硬體變成軟體,竟讓一台執行中的電腦成了一樣你可以「瞬間傳送」的東西。
一個更輕的對手:容器
一台完整的虛擬機很強大,但很重:每個客體都拖著它自己一整套作業系統,有自己的核心、自己的開機流程、自己好幾 GB 的東西。如果你只想在一個乾淨、隔離的盒子裡跑「一支應用程式」,為了這個還得開起一整套第二作業系統,感覺就像為了放一台腳踏車而去租了一整棟房子。這份不舒服催生了一個更輕的想法:容器(container),一種作業系統層級的虛擬化,本階後面的導覽會深入探索它。
這裡有個關鍵的差別,也是這篇導覽最重要、最該帶走的一件事。虛擬機在「超管理器上跑一整套客體作業系統」,所以每個客體都帶著它自己的核心。容器則「完全不」帶自己的核心——一台機器上的每個容器都共用那同一個主機核心,主機只不過是用兩個你很快會認識的核心功能,把每個容器圈進它自己私有的系統視角:命名空間(把其他行程、檔案樹與網路介面藏起來)與控制群組(限制每個容器最多能用多少 CPU 與記憶體)。正因為沒有第二套作業系統要開機,一個容器幾毫秒就啟動了,重量是好幾 MB,而不是好幾 GB。
接下來的路,以及一個誠實的提醒
你現在握著整個這一階的地圖了。第二篇導覽會把陷阱與模擬攤開來,並解釋 x86 架構在歷史上為什麼難虛擬化——有些特權指令以前會默默失敗、而不是觸發陷阱,逼得人們得用二進位轉譯之類的巧計,直到晶片廠商加進了硬體協助(Intel 的 VT-x、AMD 的對等技術)。第三篇導覽會把三大資源一個一個地虛擬化:CPU、記憶體(用影子分頁表與巢狀分頁表,去虛擬化你早已認識的那張分頁表),以及輸入輸出(用客體能高效溝通的 virtio 裝置)。第四、第五篇導覽接著潛入容器——命名空間、控制群組、疊加映像檔——並把它們與虛擬機端端正正地放上天平相較。
在你繼續往上爬之前,先除掉一個很誘人的誤解。虛擬化並不會讓你的軟體變快——恰恰相反。每一個幻覺都要付出代價:超管理器必須攔截並處理客體的特權動作,所以一個被虛擬化的工作負載,幾乎總是比同樣的程式碼直接跑在裸機上要慢一些。虛擬化替你買來的不是速度,而是彈性、隔離與密度——許多台彼此獨立的機器,安全地共用一台。這正是你在虛擬記憶體那裡碰過的同一筆誠實的交易,它也從來沒讓任何程式變快過;它讓的是程式在吃緊的資源下「還跑得起來」。付出那一點點額外負擔,換來一個運算的世界,在那裡,電腦成了你可以隨意召喚、複製、搬移的軟體。