一塊有特權的整體,還是許多被看守的伺服器?
走到這一級時,你對那條界線已經爛熟:程式碼要嘛在使用者模式執行,被圍在它自己的虛擬位址空間裡、無法直接碰硬體;要嘛在核心模式執行,在那裡它擁有整台機器。核心就是那個被信任、能在特權模式下執行、並把機器的資源分配給其他所有人的單一程式。本篇要問一個比這一切高一層的問題:核心本身是由許多部件組成的——一個排程器、一個記憶體管理器、檔案系統、幾十個裝置驅動程式、一個網路堆疊——那麼這些部件在哪裡執行?全部一起待在那塊受信任的整體裡,還是被切出去成為一個個分開、各受看守的部件?這一個選擇,劃分了史上出貨過的每一種核心設計。
把這股張力具象化會有幫助。在核心那唯一的位址空間裡,每個元件都能像呼叫一個普通函式那樣呼叫其他任何元件——檔案系統用一次直接呼叫就從磁碟驅動程式讀資料,不需要許可證、不需要複製。那快得驚人,但這也意味著最不起眼的驅動程式裡一個失控的指標,就能塗掉排程器的資料,整個系統隨之死亡。相反的立場說:別信任你自己的驅動程式。把每一個都放進它自己的使用者模式行程、給它自己的位址空間,這樣音效驅動程式裡的一個臭蟲就只能弄壞音效驅動程式。安全上升了,但現在檔案系統無法直接呼叫磁碟驅動程式——它們住在不同的位址空間裡,必須互傳訊息,而那比較慢。「靠共享信任換速度」對上「靠隔離換安全」:這就是整場辯論,而三大設計就是對它的三種不同回答。
單體式:一切都在同一個受信任的房間裡
單體式核心整套採用第一種答案:排程器、記憶體管理器、每一個檔案系統、網路堆疊、以及所有裝置驅動程式,全都連結成一個大型程式,完全在核心模式下執行、共享一個位址空間。Linux 就是典型範例,而它正是這個意義上的單體式——當你對一個檔案呼叫 read() 時,從系統呼叫入口、穿過虛擬檔案系統、進入特定的檔案系統、往下到區塊層與磁碟驅動程式,這一整條路徑是那一個房間裡的一連串普通函式呼叫。途中沒有任何東西跨越位址空間邊界;沒有任何東西在行程之間被複製。正是這種直接,使得單體式核心在常見路徑上通常是最快的設計,也使得 Linux 從一支手錶跑到一台超級電腦。
代價寫在硬幣的另一面。因為每個部件共享核心的位址空間,每個部件都被同等地信任、也同等地危險。一個有臭蟲的第三方驅動程式不會只弄垮它自己的元件——根本沒有一個被隔離的元件可垮——它可以用一次失控的寫入損壞它所能觸及的任何核心資料結構,而通常的結果就是核心恐慌(kernel panic):核心偵測到一個無法復原的不一致,於是停掉整台機器,而不冒著在已損壞狀態上繼續執行的風險。驅動程式的數量讓這件事很真切:絕大部分的核心程式碼、以及絕大部分的核心臭蟲,都住在裝置驅動程式裡,而在單體式中,它們每一個都帶著對一切的完全權力執行。
微核心:盡可能少地信任
微核心把相反的答案推到它的邏輯終點。它問:到底有什麼是最少、真正必須在特權核心模式下執行的?誠實的答案很小——你需要位址空間管理、基本排程、以及一個讓行程能彼此傳訊息的方法。單體式留在裡面的其他一切——檔案系統、網路堆疊、尤其是裝置驅動程式——都被驅逐出去,成為叫做伺服器的普通使用者模式行程。核心本身縮小到幾千行,它唯一的大任務就是可靠地在這些伺服器之間傳遞訊息。當一個應用程式想讀一個檔案,它送一則訊息給檔案系統伺服器,後者送一則訊息給磁碟驅動程式伺服器,而那個微小的核心只負責把這些訊息在位址空間的牆之間來回遞送。
回報是單體式無法匹敵的故障隔離。如果網路堆疊伺服器有臭蟲而當掉,它是獨自當掉——它的位址空間沒了,但核心、排程器、以及其他每一個伺服器都毫髮無傷,而一個監督者可以在系統持續執行的同時,乾脆把那個失敗的伺服器重新啟動。一個發瘋的驅動程式只能損壞它自己的行程、永遠碰不到核心,因為硬體的記憶體保護會在位址空間的牆前攔下它,就像攔下任何使用者程式一樣。這正是為什麼在「當機不可接受」之處微核心稱霸:飛機、汽車煞車控制器、以及安全飛地。高保證核心 seL4 就是一個微核心,它的核心擁有一份經機器檢查的數學證明,證明它免於整類整類的臭蟲——這份保證之所以可行,唯一的原因就是那段受信任的程式碼小到可以被完整地推理。
那麼為什麼你的筆電幾乎可以確定沒在跑一個純粹的微核心?因為那次檔案讀取跨越了好幾道位址空間的牆,而每一次跨越都有代價。檔案系統到驅動程式的那一跳,現在不是一次函式呼叫,而是行程間通訊:核心必須切換脈絡、複製或重新映射訊息、再切回來——鏈條裡每有一個伺服器就來一次。1990 年代早期的微核心正是因為這個理由而出了名地慢,而 seL4 一脈最大的成就,就是把一則訊息的成本壓低到足以具備競爭力。微核心的賭注是:隔離值得這筆 IPC 稅。對大多數桌機與伺服器而言,歷史判定這筆稅太高了——而這正是替那個折衷方案打開大門的東西。
混合式:務實的中間地帶,以及那個異類
混合式核心拒絕選邊站,把對效能關鍵的伺服器留在核心模式裡面,同時在表面上保持一種微核心風味的訊息傳遞結構。誠實地讀這個詞應是:它的結構鬆散地像個微核心,但為了速度,那些原本會是昂貴的使用者模式伺服器的部件——檔案系統與核心驅動程式——被拉回核心的位址空間,好讓它們的互動再次成為快速的函式呼叫,而非 IPC。蘋果的 XNU(macOS 與 iOS 內部的核心,建立在一個與 BSD 層融合的 Mach 微核心核之上)與 Windows NT 就是著名範例。你得到了微核心結構整潔的一部分、以及一個可以隔離跑少數東西的地方,但熱路徑的大宗仍留在核心內,理由與單體式相同:在每一次磁碟讀取時跨越位址空間的牆太貴了。
還有一個設計該放上這張地圖,正因為它把問題反轉了。外核心(exokernel)是一個研究性設計,它問的不是「核心該如何抽象化硬體?」而是「如果它幾乎不做抽象化呢?」它幾乎什麼都不做,只是安全地多工分配原始硬體——安全地分發磁碟區塊、記憶體分頁、與 CPU 時間——並把那些抽象(「檔案」或「行程」這些概念本身)往上推進每個程式各自連結的應用層函式庫。它的想法是:一個了解自己存取模式的應用程式,能比一個放諸四海皆準的核心政策更好地管理自己的資源。外核心從未成為主流,但那股精神——給應用程式直接、安全地存取硬體、跳過核心這個通用中間人——在今天的單核心應用(unikernel)與核心旁路網路中仍大聲迴響,所以值得把它當作羅盤的第四個方位記著。
讀懂這個選擇,以及貫穿這一級的路
退一步看,這四種設計排成單一軸上的四個點——你願意把多少完全特權託付給你自己的程式碼?外核心幾乎不把抽象化的職責託付給任何東西;微核心只信任一個微小的訊息傳遞核;混合式信任一個務實的中間地帶;單體式把整個核心當作一塊來信任。讀懂任何真實系統,意味著讀懂它的驅動程式在哪裡執行,因為驅動程式正是臭蟲集中之處。也要注意這個選擇並非純粹技術性的:Linus Torvalds 與 Andrew Tanenbaum 在 1992 年那場著名的「單體式對微核心」之爭,與其說是關於跑分,不如說同樣是關於工程價值觀——效能與務實,對上隔離與可證明性。今天兩種設計都出貨在數十億台裝置裡,這是誠實的判決:沒有單一贏家,只有不同賭注下的不同取捨。
這一級其他的一切,現在都有了歸宿。因為使用者與核心之間的那條界線是這四種設計共同的核心,下一篇會打開機器、看著它開機——一段開機序列如何把控制權交給核心——然後追蹤陷阱與中斷機制,也就是每一次進入核心的跨越實際上所經過的那道硬體門。看見那道門之後,第三篇會跟著一個系統呼叫,從你的 read() 一路進入核心、再回來,這個具體機制把一個應用程式接到它底下無論哪一種設計。
從那裡,這一級轉向內部、轉向單體式的機械裝置,因為那正是 Linux 給你讀的東西。第四篇解剖排程器、以及完全公平排程器如何分享 CPU;第五篇正面對上共享位址空間核心最深的問題——當數十個 CPU 在同一塊記憶體裡同時執行核心程式碼時,你要如何防止資料結構被撕裂?那正是核心自旋鎖與讀-複製-更新(RCU)掙得它們存在價值的地方。讓一幅圖貫穿全部五篇:使用者/核心的牆、核心內部的位址空間的牆、以及每一次跨越的代價。那唯一一股張力——為速度而信任,還是為安全而隔離——就是把整一級串起來的那條線。