我們知道,在 OS(operating system)中有一個 kernel mode, user mode 的概念,其用處在於限定 instruction 的執行權限。處於 user mode 的 instruction,不可以直接執行訪問 hardware 等敏感操作;而處於 kernel mode 的 instruction 則可以。

如果不深究細節,似乎 user/kernel mode 是非常顯然的模式,不就類似於調用某個 HTTP API 嘛,沒啥了不起。但如果深究細節,問一下 user/kernel mode 的切換到底發生了什麼事情,為什麼要如此設計這樣的切換流程,那麼,很多東西就變得不再平凡。

看到 kernel/user mode,可能最直觀的想法就是:OS 提供了一堆可供 user 使用的 kernel 函數,如 funcKernel() ,這些函數可以被 user mode 的任何方法 funcUser() 調用。而這些 funcKernel() 函數的實現,是用處於 kernel mode 的方法來實現的。

但,這樣的敍述是有問題的,因為沒有精準推敲所使用的術語。當我們討論 user/kernel mode 的執行時,我們討論的是 instruction,而不是方法。方法是可以被拆分為多條 instruction 的,而 instruction 可以被拆分嗎?

也即是,按照上述符號, funcKernel 和 funcUser 不再是一個個的方法了,而是一條條 atomic 的 instruction(讓我們將其對應的符號切換為 instrKernel 和 instrUser ),請問,可以讓一條 instruction 的執行被拆分為多條其它 instruction 的執行嗎?

顯然,從 CPU 的角度講,instruction 已經是最小執行單位了,是不可以被拆分的。

此時,我們終於看到了 devil in the detail:如何讓不可拆分的 instruction instrKernel 和 instrUser 來實現類似於 funcKernel 和 funcUser 之間的嵌套調用?

一種最直觀而粗糙的想法,當然就是強行模仿 method 之間的調用方式,即:再引入一層虛擬的 instruction set,類似於 virtual machine。任何 instrUser 都是 virtual instruction,而 instrKernel 才是真正的 hardware instruction。這可以算是 virtual machine 的解決思路,我們暫時不予討論。

那如果不使用這樣的 virtual instruction set 層,又該如何解決呢?此時,無論是 OS kernel 的 instruction,還是 user application 所使用的 instruction,對於 CPU 來講都是無差別對待的 hardware instruction。例如,你不能説 user application 的 mov/add 操作,就不同於 OS kernel 的 mov/add 操作。它們都是 CPU 的真實 instruction,當然是無差別同等對待。

那 CPU 應該如何區分 instruction 的「出處」呢?如何知道在執行一條 instruction 的時候,是來自於 instrKernel 還是來自於 instrUser 呢?

一個直觀的解決方案,當然就是引入 state 來區分 instruction。CPU ring 即是這樣的 state variable,只不過它不是被放於 instruction 中的,而是放於 CPU 中。即:不是在每一條 instruction 中放入一個 state 來説明自己的出處,而是直接將權限 state 放於 CPU 中,讓 CPU 根據自己的這個 ring variable 來判斷執行權限。

緊隨而來的問題是如何改變這個 state variable 呢?即:如何切換 kernel/user mode?

如果直接將這個「改變操作」放於一條常規的 instruction 中,那麼執行這條 instruction 時,CPU ring 應該是什麼狀態呢?

  • 如果是 user mode,那豈不是 user application 可以隨時改變 CPU 的權限,然後讓 user application 繼續自己指定的、後續任意 instruction 的執行?此時,權限控制不就沒有意義了嗎?
  • 如果是 kernel mode,那麼,user mode 調用它本來就是為了改變 CPU ring 的,現在連改變的 instruction 也是沒有權限執行的了,那它還怎麼改變自己的權限呢?

如此,將這個「改變操作」直接放於一條常規的 instruction 中肯定是不合適的。那麼,對於 CPU 來講還有什麼異常的 instruction 嗎?那就只剩 exception 了。

當 user mode 調用 system call 時,其對應的操作是拋出一個 exception(稱為 trap)。拋出的 exception,會有事先註冊到 trap table 的 exception handler 來捕獲、處理這個 exception。

此時,這個 trap handler 就可以改變 CPU ring,並執行這個 trap handler 所指定的後續 instructions。等到 trap handler 所指定的 instructions 全部被執行完後,它再將 CPU ring 改回 user mode 狀態。

乍一看,整個過程似乎有些神奇和晦澀?但仔細來看的話,其實這樣的通過「exception + trap handler」的方式所實現的,正是最開始我們提到的類似於 funcKernel 和 funcUser 之間的嵌套調用!

如上所述,雖然 instrUser 不能被拆分,但通過拋出 exception 的方式,這個 instruction 的執行流程被強制性轉換到了 trap handler,這不就是函數中調用子函數的執行流程跳轉嗎?!也即是:通過拋出 exception 來實現 instruction 級別的流程控制。而所有的這些 system call 的 trap handler,不就等價於一開始的 instrKernel 嗎?

此時,我們終於可以鬆一口氣,引入 CPU ring、system call 對應 exception handler、trap handler 改變 CPU ring 這一些列晦澀的騷操作,不過是為了實現 instruction 級別的「子函數調用」罷了,通過拋出 exception 的方式來實現 instruction 級別的流程跳轉,並沒有什麼玄幻的黑魔法。接下來,在子函數中來控制權限、改變 CPU ring,不過是自然而然的操作。

討論至此,文章似乎應該直接結束。但真正的思考者,總是不會滿足於對問題的解決,還會去重新回顧整個問題的來龍去脈,梳理其中的元問題和元認知。從我們得到的答案來看,似乎這些想法和操作都相當自然、直觀啊,那為什麼一開始的時候會困惑呢?一開始的時候,為什麼不會按照如此“自然”的思路來理解 kernel/user mode 的細節呢?

我想,這是因為我們太習慣於待在 high level 了,以至於不會很自然地切換到 low level 的視角來思考問題。通過「暴露有限的 API 來控制權限」是一種常用的手段,但習慣於 high level 視角的我們會習慣於「調用 API 就意味着 caller 本身可以被拆分為多步」,而如果 caller function 本身不能被拆分了,那麼這個經典的解決方案就不起作用了。而以 low level 的視角來講,「是否能調用 function」並不是等價於「function 是否能被拆分」,而是等價於「流程控制」,即:「執行流」能否從一個地方被轉移到另一個地方。

所以,為什麼我們無法一開始就理解這樣顯然的解決方案?因為解決方案的思路雖然是平凡的,但解決方案的實現卻是以 low level 的視角來實現的。CPU 雖然無法做到 high level 視角下的 caller function 拆分,但卻可以通過拋出 exception 來做流程控制,從而實現子函數調用。其實本來,所謂函數調用,不就是流程控制的切換嗎?

當然,我們還可以再進一步追問,如果是流程控制,為什麼不能直接使用 jump 操作,為什麼要使用如此曲折的拋出 exception 的方式呢?事實上,exception handler 本身也就是 jump 的一種實現方式,只不過,通過註冊到 trap table 來管理各種 handler 的結構要更為清晰一些。並且,對於那些無效的 exception,可直接根據無法查詢到相應的 trap handler 來放棄做處理。