核心恐慌(Kernel Panic)
當作業系統核心遇到無法恢復的錯誤時,就會發生所謂的 kernel panic,這和 Go 或 Rust 語言中的 panic
概念類似。你有看過 Windows 的藍底白字畫面嗎?我們可以在自己的核心裡實作類似的機制來處理致命錯誤。
以下這個 PANIC
巨集(macro)就是我們的 kernel panic 實作:
#define PANIC(fmt, ...) \
do { \
printf("PANIC: %s:%d: " fmt "\n", __FILE__, __LINE__, ##__VA_ARGS__); \
while (1) {} \
} while (0)
這段巨集會印出發生 panic 的位置,然後進入一個無窮迴圈來停止後續執行。我們之所以把它寫成巨集(macro),是因為這樣才能正確顯示發生 panic 的原始檔名(__FILE__
)與行號(__LINE__
)。 如果你把它寫成一般函式,那麼 __FILE__
和 __LINE__
只會顯示該函式定義的地方,而不是你實際呼叫 PANIC
的位置。
這個巨集用了兩個常見技巧:
do
{ ... } while (0)
的用法 由於條件是while (0)
,這個區塊只會執行一次。這是 C 語言中定義多行巨集時的常見寫法。 如果你只單純用{ ... }
包起來,在某些情況下(例如搭配 if 使用)可能會造成語法邏輯錯誤。請 參考這篇 清楚的範例說明。 另外注意,每一行結尾的\
是為了讓 C 預處理器把整個巨集當成單一行處理,讓換行不會被當成巨集的結尾。##__VA_ARGS__
的技巧 這是 GCC 提供的語法擴充,用來撰寫接受不定參數的巨集(參考:GCC 官方說明)。 其中 ## 是為了當沒有傳入任何額外參數時,自動移除前面的逗號 ,,這樣像下面這樣只傳一個參數的寫法也能正常編譯:PANIC("booted!");
這兩個技巧讓 PANIC 巨集既好用又安全,在實作底層系統程式時非常實用。
Let's try it
Let's try using PANIC
. You can use it like printf
:
void kernel_main(void) {
memset(__bss, 0, (size_t) __bss_end - (size_t) __bss);
PANIC("booted!");
printf("unreachable here!\n");
}
在 QEMU 中試跑看看,確認是否有正確顯示發生 panic 的檔案名稱與行號,而且 PANIC
之後的程式碼沒有被執行(也就是 "unreachable here!"
沒有出現):
$ ./run.sh
PANIC: kernel.c:46: booted!
Windows 的藍底白字(Blue Screen)、Linux 的 kernel panic 畫面看起來雖然很可怕,但對你自己寫的 kernel 來說,這其實是個非常實用又貼心的功能!
它提供一種「優雅當機」的機制,能用人類可讀的錯誤訊息來提示錯誤發生的地方,比完全沒輸出、靜悄悄地掛掉要好太多了。