自我啟動的難題
一路爬上這道梯子,你此刻已經認識核心是那位握有萬能鑰匙的大樓管理員,也知道一個程式唯有被載入記憶體並開始執行,才成為一個行程。但這引出一個確實奇怪的問題:當你按下電源鍵時,記憶體是空的,什麼都沒在跑。記憶體裡並沒有一個核心可以去載入核心。那麼,是誰把那最初的第一個程式從磁碟上抬起來的呢?這就是自我啟動(bootstrapping)的難題,這名字來自一個荒謬的畫面:抓著自己的鞋帶把自己提起來。
訣竅是從某個根本不需要被載入的東西開始。一個小程式被永久烙進主機板上的一塊晶片裡——也就是韌體(firmware)——這樣電力一到,CPU 就能直接從那塊晶片開始執行它。韌體之於電腦,就像建築物的緊急照明之於停電:它不等任何其他東西就緒,因為它天生就被接線成最先到場的那一個。整個開機序列因此是一場接力賽,每一位跑者都恰好夠力去喚醒下一位、稍大一些的跑者,並把棒子交出去。
第一棒:韌體與 UEFI
在現代個人電腦上,那個韌體是 UEFI(更老的機器用一種較簡單、叫 BIOS 的韌體)。電力一到,CPU 就跳到韌體晶片內一個固定的位址並開始執行。它的工作是對硬體做一次快速點名——記憶體可用嗎?有鍵盤嗎?接了哪些磁碟?——這套自我測試能在任何東西打算使用之前,就抓出一條壞掉的記憶體。把 UEFI 想成那位比所有人都早到的夜班警衛:他開燈、繞行整棟大樓確認每一道門,做完這些,才為一天真正的工作打開大門。
硬體一旦過關,UEFI 就得找一個程式來啟動。較老的 BIOS 韌體做法很單純:讀取磁碟最開頭的那個磁區——也就是啟動區塊(boot block)——然後執行擺在那裡的任何程式碼,是個粗暴而簡單的慣例。UEFI 文明得多:它看得懂一個小型檔案系統,會把開機程式當成已知目錄裡的一般檔案去尋找,這管理起來容易許多。無論哪一種,韌體的最後一個動作,都是載入下一棒、然後跳進去。
第二棒:開機載入器與載入核心
韌體所啟動的那個程式,就是開機載入器(bootloader)——在 Linux 上,最常見的是 GRUB。為什麼非得有獨立的一棒?因為韌體刻意對作業系統一無所知;開機載入器才是那位真正懂得核心住在哪裡、如何把它從磁碟讀出來、如何正確啟動它的專家。它是跑道(韌體)與飛機(核心)之間的機場地勤:它呈現你有時會看到的開機選單、挑選要啟動哪一個核心、把那個核心當成檔案找到,再把它複製進記憶體。
載入核心不只是複製而已。磁碟上的核心映像為了省空間是壓縮過的,所以開機載入器(或核心最前端的一小段引導碼)會把它解壓縮進記憶體、排成正確的版面。開機載入器還會交給核心一包它自己不容易查出的資訊——有多少記憶體、韌體在哪裡找到那些裝置,以及一行開機參數,告訴核心諸如「哪一顆磁碟存放著根檔案系統」之類的事。接著就是交棒的那一刻:開機載入器跳到核心的進入點。從這一道指令起,核心接管一切,在完整的核心模式下執行,而韌體與開機載入器已徹底跑完它們那一棒。
第三棒:核心醒來,接著掛載根目錄
現在核心第一次執行,它自己也有一段細緻的啟動過程。它建立起本階梯後續每篇指南都仰賴的那些結構:它建好自己的記憶體映射,好讓它能安全地配置記憶體;它安裝那張讓 CPU 為每一個中斷與例外找到處理常式的表;並且偵測與初始化裝置。唯有打好這些地基,這台機器才成為一個讓一般行程能安全棲身的地方。
在這裡,一個熟悉的雞生蛋難題回來了。核心需要掛載根檔案系統——讓磁碟的目錄樹出現在「/」這個檔案層級的最頂端——這樣它才能讀到真正的程式。但要讀某一顆特定的磁碟,它可能需要一個驅動程式,而那個驅動程式本身卻住在那顆磁碟上。逃生口是 initramfs:一個微小而完整的檔案系統,由開機載入器和核心一起載入記憶體。核心先掛載這個位於記憶體中的檔案系統、執行它所含的那幾個驅動程式以抵達真正的磁碟、把真正的根掛載上去,然後從那個暫時的檔案系統退下來。它就是你拋下去、好爬上真正樓梯的那道繩梯。
棒子落地:第一個行程
真正的根掛載完成後,核心做出它在開機過程中的最後一個動作,而那是個漂亮的動作。它啟動那最初的第一個使用者模式程式——init 行程,傳統上被賦予行程編號 1。這正是這台機器不再只是一個核心、而成為一個你可以使用的執行中系統的時刻。你之後會執行的所有其他行程——你的命令列殼層、你的瀏覽器、每一個背景服務——都是這一個的後裔,由它、或由它的子代透過你先前見過的「fork 再 exec」模式所建立。init 行程是整棵行程樹的根,是其餘一切唯一的祖先。
在大多數 Linux 系統上,扮演 init 這個角色的程式是 systemd。它的工作是讓系統的其餘部分活起來:它讀取自己的設定,啟動那些讓機器真正好用的服務——網路、日誌系統、登入畫面——而且很聰明地,它能平行啟動彼此獨立的服務,而不是排成緩慢的一列,這正是現代機器開機迅速的部分原因。以下就是這整場接力,從頭到尾。
- 通電。CPU 開始直接從主機板上一塊晶片執行韌體(UEFI 或 BIOS),此時記憶體仍是空的。
- 韌體對硬體做自我測試,接著找到並載入開機載入器(從一個開機檔或啟動區塊),然後跳進它。
- 開機載入器(例如 GRUB)在磁碟上定位核心,連同 initramfs 一起解壓縮進記憶體,交付開機參數,然後跳到核心。
- 核心初始化自己——記憶體映射、中斷表、裝置驅動程式——接著掛載 initramfs、抵達真正的磁碟,並把真正的根檔案系統掛載上去。
- 核心啟動第一個使用者行程 init(行程編號 1,通常是 systemd),由它啟動其他所有服務。系統此刻已為你準備就緒。
請留意整個序列反覆出現的形狀:小而受信任的,啟動大而有彈性的;固定的韌體,伸手去取一個可替換的核心;而每一棒都乾淨俐落地交給下一棒。如今坐鎮中央的這同一個核心,將在本階梯其餘篇章裡,揭示它如何回應一次系統呼叫、如何應付一個中斷、如何分發記憶體、如何保護共享資料——但這一切都從這裡開始,從那個不需要任何其他程式來啟動它的程式開始。開機序列,就是自我啟動難題的答案,用機器碼寫成。第四步裡甚至還藏著一個耐人尋味的核心結構抉擇:單核心會在一個龐大的特權程式裡做完幾乎所有那些初始化,而微核心則會把其中大半推到獨立的使用者模式服務去——這正是本階梯最後一篇指南要完整探討的取捨。