一座塔,而非一層樓
在前幾篇導覽中,你已看清電腦究竟是什麼——一台 儲存程式機器,逐一擷取並執行指令——也見識了「事先編譯的程式碼」與「執行時逐句直譯的程式碼」之間的差別。現在我們從那顆處理器拉遠視角,問一個更大的問題:當你的程式要求印出一行字或開啟一個檔案時,究竟是誰真正在做事?誠實的答案是:沒有人是獨力完成的。現代電腦是一座層層相疊的塔,每一層都倚靠著它下方的那一層。
這座塔有個名字:抽象堆疊。把它想成大樓裡的樓層。每一層都把下方那層雜亂的管線藏了起來,並對上方那層提供一組乾淨、簡單的服務。頂樓的人從不會看到地下室如何灌漿——他們只是相信燈會亮。同理,你的應用程式相信「把這些位元組寫到螢幕上」會發生,卻完全不需要知道電子如何流經顯示器。
四層樓
由下往上看,一台典型機器的堆疊有四層樓值得命名。最底層是硬體:執行指令的 CPU、存放資料的記憶體,以及連接外在世界的 I/O 裝置(磁碟、螢幕、鍵盤、網路卡)。硬體快速、死板,且毫無保護——它樂於讓任何程式碼覆寫任何一個位元組,這正是我們不讓你的應用程式直接碰它的原因。
再上一層是核心——作業系統的核心部分。核心是唯一被允許直接與硬體對話的軟體,它的全部職責就是把硬體安全地分享給同時在跑的每一支程式。它決定下一個拿到 CPU 的是哪支程式、配發記憶體,並守護裝置。它是這棟大樓的房東:它擁有真正的資源,並依嚴格的規則出租。
核心之上是系統函式庫,你最先遇到的會是 C 標準函式庫(常稱為 libc)。函式庫是一捆現成的函式,你可以直接呼叫,而不必自己重寫。函式庫把核心那些原始、裸露的服務包裝成更友善的形狀——例如你第一支程式會用到的那個親切的 printf(),就是一個函式庫函式,而非硬體本身。最頂樓是應用程式:人們真正想執行的那支程式,包含你寫的程式碼。
+-------------------------------+ | Application (your code) | <- top floor +-------------------------------+ | Libraries (libc, ...) | <- friendlier functions +-------------------------------+ | Kernel (the OS core) | <- shares the hardware +===============================+ <- user / kernel boundary | Hardware (CPU, RAM, IO) | <- the metal +-------------------------------+
唯一真正要緊的那道牆
並非每一層之間的隔閡都一樣。你的應用程式與函式庫之間的界線是柔軟的——呼叫 printf() 只是一次平凡的函式呼叫,跳到同一個行程內的某段程式碼,本質上和呼叫一個你自己寫的函式沒有不同。但函式庫與核心之間的界線,卻是一道真實、由硬體強制的牆。這就是使用者空間與核心空間之分,跨越它並不是一次普通的跳躍。
你的程式碼跑在使用者模式中,CPU 拒絕讓它直接碰硬體或其他程式的記憶體。要做真正的工作——讀檔、送封包、要更多記憶體——它必須透過一次系統呼叫,請求核心代為執行。系統呼叫不只是另一次函式呼叫:它刻意陷入(trap)CPU 那具特權的核心模式,讓核心代你執行該操作,再帶著結果落回使用者模式。這是整篇導覽最關鍵的區別。
跟著「hello, world」下行再返回
讓我們用你下一篇就會真正執行的那支程式來把這件事具體化:一支印出一行字的 hello world 程式。當你呼叫 printf("hello, world\n") 時,看起來只是一個小小的動作,但這一行卻會穿過塔的每一層樓,再層層攀回。看著它一路旅行,是體會分界線究竟在哪的最佳方式。
- 應用程式層:你的程式碼呼叫 printf("hello, world\n")——這是一次進入 C 函式庫的平凡函式呼叫,仍然完全在使用者空間之內。
- 函式庫層:printf() 把文字格式化好,再把完成的位元組交給較底層的函式庫常式,由它請求核心真正將它們送出。
- 跨越那道牆:函式庫發出 write() 系統呼叫,陷入核心模式——這趟旅程中唯一真正的分界。
- 核心層:核心檢查你是否獲准,接著直接驅動終端機裝置把字元送上螢幕,與硬體直接對話。
- 層層攀回:write() 把寫出的位元組數回傳給函式庫,printf() 回到你的程式碼,執行繼續向前——回到它最初出發的使用者空間。
每一層都恰好做了自己份內的事,並信任它下方的那一層。你的程式碼從未指名任何一個硬體暫存器、從未過問你用的是哪種螢幕、甚至根本不知道有個核心模式存在。這就是抽象堆疊發揮作用的時刻:你寫下「印出這一行」,四層樓便通力合作讓它成真。
為何認得這些層次值得
這張地圖不是無關緊要的瑣事——它是你在整座階梯都會用到的方位感。當你之後學到堆積(heap)時,你是在向一個函式庫層與一個核心層要記憶體。當你遇到行程與執行緒時,你是在看核心如何分派 CPU 時間。當一支程式以記憶體區段錯誤(segfault)死去時,是硬體逮到它跨越了不被允許跨越的牆,再由核心將它終結。前方幾乎每一個進階主題,其實都是對某一層樓、或對其中兩層之間某道邊界的更近一步凝視。
在繼續前,要誠實提醒兩件事。第一,這幅四層圖景是一種乾淨的簡化:真實系統還會在這些樓層之間與之內加入裝置驅動程式、虛擬機、容器等更多層次。這四個名字是正確的起步框架,而非全部真相。第二,分層是一種取捨,而非白白的好處——每一層都增添一點成本、隱藏一點細節,這正是貼近底層硬體工作的系統程式設計師,有時會剝開一層、親眼看看底下究竟發生什麼的原因。