作業系統除錯 (Operating-System Debugging)
本系列 文章內容參考自經典教材 Operating System Concepts, 10th Edition (Silberschatz, Galvin, Gagne)。本文對應章節:Section 2.10 Operating-System Debugging。
廣義而言,除錯(Debugging) 是在系統中尋找並修復錯誤的活動,涵蓋硬體與軟體兩個層面。值得注意的是,效能問題也被視為一種 bug,因此除錯也包含效能調校(Performance Tuning),即透過消除處理瓶頸來提升整體效能。本章將探討 OS 如何支援三種核心除錯活動:失敗分析、效能監控,以及動態核心追蹤。
2.10.1 失敗分析 (Failure Analysis)
Process 層級的失敗:Core Dump
當一個 Process 發生錯誤而失敗時,OS 通常會做兩件事:
- 寫入 log 檔案:將錯誤資訊記錄到 log file,用於通知系統管理員或使用者問題的發生
- 產生 core dump:拍下 Process 在失敗當下的完整記憶體快照(a capture of the memory of the process),儲存到檔案中供後續分析
「core」這個詞來自早期電腦使用磁蕊記憶體(Magnetic Core Memory) 的時代,當時記憶體就被稱為「core」。雖然現代早已改用 DRAM,這個名稱卻沿用至今。
core dump 檔案可以用偵錯工具(Debugger) 開啟。Debugger 允許程式設計師探索 Process 在失敗當下的程式碼與記憶體狀態,例如查看哪個變數持有非法值、哪個指標已超出有效範圍,是定位 bug 根本原因的重要工具。
Kernel 層級的失敗:Crash Dump
除錯 user-level Process 的程式碼本身已是一大挑戰,而除錯 OS kernel 則更為複雜,原因有三:
- 規模與複雜度:kernel 程式碼量龐大、子系統高度交織
- 直接控制硬體:kernel 中的錯誤可能直接導致整台機器失去回應
- 缺乏 user-level 工具:在 user-level 可用的偵錯工具在 kernel 層級幾乎派不上用場
kernel 中的失敗稱為 crash(崩潰)。當 crash 發生時,OS 同樣會將錯誤資訊記錄到 log 檔案,並將記憶體狀態儲存到 crash dump。
然而,有一個關鍵問題:如果 crash 發生在 file-system 的程式碼裡,kernel 還能把 crash dump 寫到檔案系統嗎?
答案是不行。一旦 file-system 的程式碼本身已發生嚴重錯誤,再嘗試透過它寫入檔案,不僅可能寫入失敗,還可能造成資料損毀。
因此,kernel crash 的標準做法是:
- 寫入專用磁碟區域:crash 發生時,OS 將整個記憶體內容(或至少是 kernel 擁有的部分)直接寫入磁碟上一個不屬於任何檔案系統的保留區域(reserved disk section),完全繞過 file system。
- 系統重新啟動後再整理:系統重啟後,一個獨立的 process 讀取該保留區的原始資料,將其整理成標準的 crash dump 檔案,寫入正常的 file system 供後續分析。
2.10.2 效能監控與調校 (Performance Monitoring and Tuning)
效能調校(Performance Tuning)的目標是消除處理瓶頸(Bottleneck),而要找出瓶頸在哪裡,必須先能夠量測系統行為。OS 因此需要提供一套機制來收集並展示系統的各種量測數據。
效能監控工具主要分成兩種技術路線:計數器(Counters) 與追蹤(Tracing),兩者收集資訊的方式根本不同。
計數器工具 (Counter-Based Tools)
OS 透過一系列計數器持 續追蹤系統活動,例如:已執行的 system call 次數、網路裝置的傳輸次數、磁碟的讀寫次數等。計數器工具的特性是隨時可查、開銷固定低廉,適合快速掌握系統整體概況。
Linux 上常用的計數器工具分成兩類:
Process 層級(Per-Process):
| 工具 | 說明 |
|---|---|
ps | 回報單一 Process 或一組 Process 的資訊(狀態、CPU 使用、記憶體等) |
top | 即時顯示目前所有 Process 的動態統計數據,按 CPU 使用率排序 |
系統層級(System-Wide):
| 工具 | 說明 |
|---|---|
vmstat | 回報記憶體使用統計,包含虛擬記憶體的 page in/out 狀況 |
netstat | 回報網路介面的統計數據,例如封包的傳送與接收次數 |
iostat | 回報磁碟的 I/O 使用量,包含讀寫速率與等待時間 |
/proc 虛擬檔案系統
Linux 上大多數計數器工具,其資料來源都是 /proc 檔案系統。/proc 是一個 「虛擬」(Pseudo)檔案系統,它只存在於 kernel 記憶體中,並不對應到任何實際的磁碟儲存區,其主要用途是讓 user-space 程式查詢各 Process 以及 kernel 本身的統計數據。
要理解 /proc 存在的意義,必須先思考一個問題:在沒有 /proc 的情況下,user-space 程式要如何查詢 kernel 的內部狀態?
答案是:只能靠 system call。
- 要查詢 CPU 資訊,就得有一個
get_cpu_info()system call; - 要查詢記憶體使用量,就得有
get_mem_stats(); - 要列出所有 Process,就得有
get_process_list()。每新增一種監控需求,就要在 kernel 增加一個 system call
而 system call 一旦加入就很難修改(它是 kernel 與 user-space 之間的公開合約,不能隨意變動)。這條路很快就會讓 kernel ABI 膨脹成難以維護的怪物。
/proc 用了一個更聰明的解法:把 kernel 狀態偽裝成一個可瀏覽的目錄樹。這個設計的核心依據是 UNIX 的哲學:「一切皆檔案(Everything is a file)」。UNIX 上的所有工具(cat、grep、awk、shell 腳本)都已經知道如何讀取檔案,所有程式語言都有標準的檔案 I/O 介面(open/read/close)。只要 kernel 狀態能以檔案的形式呈現,整個 user-space 生態系就能不需要任何修改地直接使用:
# 不需要 任何特殊 API,cat 就能查 CPU 資訊
cat /proc/cpuinfo
# grep 直接從 /proc/meminfo 抓空閒記憶體
grep MemFree /proc/meminfo
# shell 腳本直接讀取某個 process 的狀態
cat /proc/1234/status
為什麼是「目錄層級」,而不是單一的大檔案?
目錄結構帶來的是可探索性(Discoverability)。每個 PID 都有自己的子目錄,目錄裡的每個檔案是該 Process 的一種屬性:
/proc/1234/
status ← 基本狀態(名稱、PID、狀態、記憶體)
maps ← 記憶體映射區域
fd/ ← 目 前開啟的所有 file descriptor
exe ← 指向執行檔的 symlink
cmdline ← 完整的命令列參數
...
只要 ls /proc/1234/,就能一眼看出 kernel 對這個 Process 暴露了哪些資訊,完全不需要查文件。系統層級的 kernel 統計數據則放在 /proc/ 的頂層或子目錄下,例如 /proc/meminfo(記憶體使用)、/proc/cpuinfo(CPU 資訊)、/proc/net/tcp(TCP 連線表)等。
「檔案」的內容實際上從哪裡來?
實際上 /proc 裡的「檔案」完全不存在於磁碟上。當 ps、top 或任何程式對 /proc 下的路徑呼叫 open() 和 read() 時,kernel 的虛擬檔案系統層會攔截這個請求,即時呼叫對應的 kernel 函式來生成內容,然後把結果當作檔案資料回傳給呼叫者。整個過程完全在記憶體中完成,沒有任何磁碟存取。
這個設計也讓 kernel 開發者能夠輕鬆新增監控點:只要在 /proc 下掛載一個新的「檔案」(對應一個 kernel 函式),現有的所有工具立刻就能讀取它,完全不需要修改任何 system call 介面。
/proc 與 Device Files 的共同哲學/proc 和 /dev 下的裝置檔案(device files)體現的是同一個設計哲學。/dev/null、/dev/random 看起來是檔案,但對它們的讀寫實際上觸發的是 kernel 的裝置驅動程式。/proc 把這個概念延伸到 kernel 狀態查詢:用「讀取檔案」這個統一的操作模型,取代各式各樣零散的 system call。
在 Windows 系統上,工作管理員(Windows Task Manager) 提供相同功能的圖形介面,涵蓋目前執行中的應用程式與 Process 資訊、CPU 與記憶體使用量,以及網路連線統計數據(對應教科書 Figure 2.19)。
2.10.3 追蹤工具 (Tracing)
計數器工具是查詢 kernel 當下維護的靜態數值(例如「目前的 CPU 使用率是多少?」);而追蹤工具則是針對特定事件收集資料,例如「某次 system call 從呼叫到返回,中間經過了哪些步驟?」兩者的差別在於:計數器回答「有多少」,追蹤工具回答「如何發生」。
Linux 上常見的追蹤工具同樣分兩層:
Process 層級(Per-Process):
| 工具 | 說明 |
|---|---|
strace | 追蹤一個 Process 所呼叫的所有 system call,顯示每次呼叫的參數與回傳值 |
gdb | 原始碼層 級的偵錯工具(source-level debugger),可設定中斷點、單步執行、檢視變數 |
系統層級(System-Wide):
| 工具 | 說明 |
|---|---|
perf | Linux 效能分析工具集合,涵蓋 CPU 效能計數器(硬體 PMU)、追蹤點、取樣分析 |
tcpdump | 網路封包擷取工具,記錄通過網路介面的封包內容供後續分析 |
"Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it."
這句話提醒我們:程式碼的可讀性與可除錯性,本身就是設計品質的重要指標。讓 OS 更容易理解、更容易除錯、更容易在執行中調校,是作業系統研究與工程實踐中一個持續進行中的重要領域。