| RFC-0082:在 Fuchsia 上執行未修改的 Linux 程式 | |
|---|---|
| 狀態 | 已接受 |
| 區域 |
|
| 說明 | 本文建議在 Fuchsia 上執行未修改 Linux 程式的機制。 |
| 問題 | |
| Gerrit 變更 | |
| 作者 | |
| 審查人員 | |
| 提交日期 (年-月-日) | 2021-02-11 |
| 審查日期 (年-月-日) | 2021-03-25 |
摘要
本文提議在 Fuchsia 上執行未修改 Linux 程式的機制。這些程式會在使用者空間程序中執行,而該程序的系統介面與 Linux ABI 相容。我們不會使用 Linux 核心實作這個介面,而是會在名為 starnix 的 Fuchsia 使用者空間程式中實作介面。在很大程度上,starnix 會做為相容性層,將 Linux 用戶端程式的要求轉換為適當的 Fuchsia 子系統。為了支援 Linux 系統介面所隱含的所有功能,許多子系統都需要詳細說明。
提振精神
如要在 Fuchsia 上執行軟體,必須從來源重新編譯軟體,以 Fuchsia 為目標。為減少在 Fuchsia 上執行時所需的來源修改量,Fuchsia 提供 POSIX 相容層「POSIX Lite」,這個軟體可以將其設為目標。POSIX Lite 是以用戶端程式庫的形式,疊加在基礎的 Fuchsia 系統 ABI 上。
不過,POSIX Lite 並非 POSIX 的完整實作。舉例來說,POSIX Lite 不包含 POSIX 中會產生可變動全域狀態的部分 (例如 kill 函式),因為 Fuchsia 是以物件能力規範為基礎設計,會避開可變動全域狀態,以提供強大的安全保障。因此,使用 POSIX Lite 的軟體必須經過修改,才能直接使用 Fuchsia 系統介面處理這些用途 (例如 zx_task_kill 函式)。
到目前為止,這個方法運作良好,因為我們有權存取需要在 Fuchsia 上執行的軟體原始碼,因此可以為 Fuchsia 系統 ABI 重新編譯軟體,並修改需要配合物件能力系統調整的軟體部分。
隨著我們希望在 Fuchsia 上執行的軟體宇宙不斷擴大,我們遇到了一些希望在 Fuchsia 上執行的軟體,但我們無法重新編譯。舉例來說,Android 應用程式包含已為 Linux 編譯的原生程式碼模組。如要在 Fuchsia 上執行這項軟體,我們必須能夠執行二進位檔,而不需修改這些檔案。
設計
在 Fuchsia 上執行 Linux 二進位檔最直接的方式,就是在虛擬機器中執行這些二進位檔,並將 Linux 核心做為虛擬機器中的客體核心。不過,由於客體程式與系統的其餘部分在不同的作業系統中執行,因此這種方法難以將客體程式與 Fuchsia 系統的其餘部分整合。
Fuchsia 的設計可讓您自備執行階段,也就是說,Fuchsia 系統不會對元件的內部結構設限。如要與 Fuchsia 系統互通,成為一等公民,元件只需要透過適當的 zx::channel 物件傳送及接收格式正確的訊息即可。
starnix 不會在虛擬機器中執行 Linux 二進位檔,而是在 Fuchsia 中原生建立 Linux 執行階段。具體來說,Linux 程式可以包裝在元件資訊清單中,將 starnix 識別為該元件的執行器。Linux 程式的二進位檔會提供給 starnix 執行,而不是直接使用 ELF Runner。
為了執行指定的 Linux 二進位檔,starnix 會手動建立 zx::process,並採用符合 Linux ABI 的初始記憶體配置。舉例來說,starnix 會在初始執行緒的堆疊中填入程式的 argv 和 environ (連同 aux 向量),而不是在 Bootstrap 管道中填入訊息,因為這項資料是在 Fuchsia 系統 ABI 中填入。
系統呼叫
將二進位檔載入用戶端程序後,starnix 會註冊處理來自用戶端程序的所有系統呼叫 (請參閱下方的「系統呼叫機制」)。每當用戶端發出系統呼叫時,Zircon 核心會將控制權轉移至 starnix,後者會根據 Linux 系統呼叫慣例解碼系統呼叫,並執行系統呼叫的工作。
舉例來說,如果用戶端程式發出 brk 系統呼叫,starnix 會使用適當的 zx::vmar 和 zx::vmo 作業,操控用戶端程序的位址空間,藉此變更用戶端程序的程式中斷位址。在某些情況下,我們可能需要詳細說明某個程序 (即 starnix) 來操弄另一個程序 (即用戶端) 的位址空間,但初步實驗顯示,Zircon 已包含遠端位址空間操弄所需的大部分機制。
再舉一個例子,假設用戶端程式發出 write 系統呼叫。如要實作檔案相關功能,starnix 會為每個用戶端程序維護檔案描述元表格。收到 writesyscall 後,starnix 會在用戶端程序的檔案描述元表格中,查閱已識別的檔案描述元。通常,該檔案描述元會由實作 fuchsia.io.File FIDL 通訊協定的 zx::channel 支援。如要執行 write,starnix 會格式化 fuchsia.io.File#Write 訊息,其中包含來自用戶端位址空間的資料 (請參閱「記憶體存取」),並透過管道傳送該訊息,類似於 POSIX Lite 在用戶端程式庫中實作 write 的方式。
全域狀態
如要處理會影響可變動全域狀態的系統呼叫,starnix 會維護用戶端程序之間共用的可變動狀態。舉例來說,starnix 會為執行的每個用戶端程序指派 pid_t,並維護將 pid_t 對應至該程序基礎 zx::process 控制代碼的表格。如要實作 kill 系統呼叫,starnix 會在這個表格中查閱指定的 pid_t,並在相關聯的 zx::process 控制代碼上發出 zx_task_kill 系統呼叫。
這樣一來,每個 starnix 執行個體都會做為相關 Linux 程序的容器。如果希望在兩個 Linux 程序之間提供強大的隔離保證,我們可以在個別的 starnix 執行個體中執行這些程序,而不會產生執行多部虛擬機器的額外負擔 (例如排程複雜度)。
每個 starnix 執行個體也會公開其全域狀態,供其他 Fuchsia 程序使用。舉例來說,starnix 會維護 AF_UNIX 插座的命名空間。這個命名空間可從 starnix 執行的 Linux 二進位檔存取,也可從透過 FIDL 與 starnix 通訊的 Fuchsia 二進位檔存取。
Linux 系統介面也代表全域檔案系統。由於 Fuchsia 沒有全域檔案系統,starnix 會從自己的命名空間為用戶端程序合成「全域」檔案系統。舉例來說,starnix 會將自身命名空間中的 /data/root 掛接為向用戶端程序顯示的全球檔案系統中的 /。starnix 可以在內部實作其他掛接點 (例如 /proc),例如查詢執行中程序的表格。
安全性
starnix 會盡可能以基礎 Fuchsia 系統的安全機制為基礎建構,舉例來說,與檔案系統、網路和圖形等系統服務介接時,starnix 主要會做為轉譯層,將 Linux ABI 的要求重新格式化為 Fuchsia 系統 ABI。系統服務會負責強制執行自身的安全不變量,就像對待其他所有用戶端一樣。不過,
starnix必須實作一些安全機制,才能保護自家服務的存取權。舉例來說,starnix 必須判斷某個用戶端程序是否可 kill 另一個用戶端程序。
為做出這些安全決策,starnix 會追蹤每個用戶端程序的安全環境,包括 uid_t、gid_t、有效 uid_t 和有效 gid_t。需要安全檢查的作業會使用這個安全情境,做出適當的存取控管決策。我們預期這個機制一開始不會經常使用,但隨著用途越來越複雜,存取控制需求也可能越來越複雜。
As she is spoke
當面臨 starnix 在特定情況下的行為選擇時,設計會盡可能偏好與 Linux 行為相近的行為。目標是建立 Linux 介面的實作項目,以便執行現有的未修改 Linux 二進位檔。只要 starnix 與 Linux 語意不同,我們就有可能發現某些 Linux 二進位檔會注意到差異,並出現不當行為。
為方便討論這項設計原則,我們說starnix
實作 Linux
如她所說,也就是說,要包含真實 Linux 系統的所有優點、缺點、巧合和怪異之處。
在某些情況下,實作 Linux 介面時,需要為 Fuchsia 服務新增功能,以提供必要的語意。舉例來說,如要有效實作 inotify,就必須獲得基礎檔案系統實作的支援。我們應以與服務公開的其他功能妥善整合的方式,將這項功能新增至 Fuchsia 服務。
實作
我們計畫將 starnix 實作為 Fuchsia 元件,具體來說,就是實作 runner 通訊協定的正常使用者空間元件。我們計畫在 Rust 中實作 starnix,避免從用戶端程序到 starnix 程序發生權限提升。
管理階層
starnix 的核心部分之一是「執行檔」,可實作 Linux 系統介面中的語意概念。舉例來說,執行檔會包含代表執行緒、程序和檔案說明的物件。
執行檔的結構應能獨立於 starnix 系統的其餘部分進行單元測試。舉例來說,我們將能單元測試複製檔案描述元是否會共用基礎檔案說明,而不需使用 Linux ABI 執行程序。
Linux 系統呼叫定義
為實作 Linux 系統呼叫,starnix 需要每個 Linux 系統呼叫的說明,以及任何相關聯輸入或輸出參數的使用者空間記憶體配置。這些定義位於 Linux uapi 中,這是 C 標頭的獨立集合。如要在 Rust 中使用這些定義,我們會使用 Rust bindgen 生成 Rust 宣告。
Linux uapi 會隨著時間演進。我們最初會以 Linux 5.10 LTS 為目標,但日後可能需要調整支援的 Linux uapi確切版本。uapi
系統呼叫機制
starnix 的初始實作會使用 Zircon 例外狀況,從用戶端程序擷取系統呼叫。具體來說,每當用戶端程序嘗試發出系統呼叫時,Zircon 都會拒絕該系統呼叫,因為 Zircon 要求系統呼叫必須從 Zircon vDSO 內發出,而用戶端程序並不知道 Zircon vDSO 的存在。
Zircon 會產生 ZX_EXCP_POLICY_CODE_BAD_SYSCALL 例外狀況,拒絕這些系統呼叫。starnix 程序會在每個用戶端程序上安裝例外狀況處理常式,藉此擷取這些例外狀況。如要接收系統呼叫的參數,starnix 會使用 zx_thread_read_state 從產生例外狀況的執行緒讀取暫存器。處理系統呼叫後,starnix 會使用 zx_thread_write_state 設定系統呼叫的回傳值,然後在用戶端程序中繼續執行執行緒。
這個機制可以運作,但效能可能不夠高,因此不實用。建立足夠的 starnix 來執行 Linux 基準測試後,我們可能會想以更有效率的機制取代這個系統呼叫機制。舉例來說,starnix 可能會將 zx::port 與處理來自用戶端程序的系統呼叫建立關聯,而 Zircon 會將封包排入 zx::port 的佇列,並註冊用戶端程序的狀態。有了基準,我們就能製作各種方法的原型,並選取當時的最佳設計。
記憶體存取
starnix 的初始實作會使用 zx_process_read_memory 和 zx_process_write_memory,從用戶端程序的位址空間讀取及寫入資料。這個機制雖然可行,但有兩個缺點:
- 基於安全考量,這些系統呼叫會在正式版建構作業中停用。
- 這些系統呼叫的成本遠高於直接讀取及寫入記憶體。
建構足夠的 starnix 來執行 Linux 基準測試後,我們會想以更有效率的機制取代這個機制。舉例來說,starnix 可能會限制用戶端位址空間的大小,並將每個用戶端的位址空間對應至各自的位址空間,且位址空間會以特定於用戶端的偏移量為準。或者,當 starnix 服務處理來自用戶端的系統呼叫時,Zircon 會安排從該執行緒顯示該用戶端的位址空間 (例如,類似於核心執行緒在處理來自這些程序的系統呼叫時,可見使用者空間程序位址空間的方式)。
與系統呼叫機制相同,我們可以製作各種方法的原型,並在有更多執行中的程式碼可評估這些方法時,選取最佳設計。
互通性
我們會採用測試驅動方法開發 starnix。一開始,我們會使用簡單的實作方式,足以執行基本的 Linux 二進位檔。我們已建立實作的原型,可執行 hello_world.c 程式的 -static-pie 建構作業。下一步是清除該原型,並教導 starnix 如何執行動態連結的 hello_world.c 二進位檔。
執行這些基本二進位檔後,我們會從各種程式碼庫調出單元測試二進位檔。這些二進位檔可確保 Linux ABI 的實作方式正確無誤 (即 Linux 的說法)。舉例來說,我們會從 Android 來源樹狀結構執行一些低階測試二進位檔,以及 Linux 測試專案的二進位檔。
效能
效能是這項專案的關鍵要素。一開始,starnix 的效能會相當差,因為我們會使用效率不彰的機制來擷取系統呼叫,並存取用戶端記憶體。不過,一旦我們有足夠的功能,可在 Linux 執行環境中執行基準測試,這些領域的效能應該就能大幅提升。
除了最佳化這些機制,我們也有機會將高頻率作業卸載至用戶端。舉例來說,我們可以在將控制權轉移至 Linux 二進位檔之前,將程式碼載入用戶端程序,直接在用戶端位址空間中實作 gettimeofday。舉例來說,如果 Linux 二進位檔透過 Linux vDSO 叫用 gettimeofday,starnix 可以提供共用程式庫來取代 Linux vDSO,直接透過呼叫 Zircon vDSO 實作 gettimeofday。
安全性考量
這項提案有許多細微的安全性考量。starnix 程序和用戶端程序之間存在信任界線。具體來說,starnix 程序可以保留未完全向用戶端公開的物件功能。舉例來說,starnix 程序會為每個用戶端程序維護檔案描述元表格。一個用戶端程序應可存取儲存在其檔案描述元表格中的控制代碼,但無法存取儲存在另一個程序檔案描述元表格中的控制代碼。同樣地,starnix 會維護共用的可變動狀態,用戶端只能在存取權控管的限制下與其互動。
為提供這個信任範圍,starnix 會在與用戶端程序不同的使用者空間程序中執行。為避免權限提升,我們計畫以 Rust 實作 starnix,並使用 Rust 的型別系統避免型別混淆。我們也打算使用 Rust 的型別系統,清楚區分用戶端資料 (例如用戶端位址空間中的位址,以及從用戶端位址空間讀取的資料),與 starnix 本身維護的可靠資料。
此外,我們也必須考量 Linux 二進位檔本身的來源,因為 starnix 會直接執行這些二進位檔,而不是在虛擬機器或 SFI 容器中執行。我們需要針對涉及 Linux 二進位檔的特定端對端產品使用情境,重新審視這項考量。
starnix 內的存取控制機制需要詳細的安全性評估,最好包括安全性團隊直接參與設計,以及可能參與實作。我們預期初期會採用簡單的存取控管機制。隨著這項機制的要求越來越複雜,我們需要進一步審查安全性。
最後,高效能系統呼叫和用戶端記憶體機制的設計需要仔細的安全審查,特別是如果我們最終使用 starnix 的特殊位址空間設定,或嘗試直接將暫存器狀態從用戶端執行緒轉移至 starnix 執行緒。
隱私權注意事項
這項設計沒有任何立即的隱私權考量。不過,一旦我們有涉及 Linux 二進位檔的特定端對端產品用途,就必須評估該用途對隱私權的影響。
測試
測試是建構 starnix 的核心環節。我們會直接對 starnix 執行單元測試。我們也會嘗試通過要在 Linux 上執行的測試二進位檔,建構 Linux 系統介面的實作項目。接著,我們會在持續整合中執行這些二進位檔,確保 starnix 不會回歸。
我們也會比較在 starnix 中執行 Linux 二進位檔,以及在 Fuchsia 的虛擬機器中執行相同二進位檔的差異。我們預期在 starnix 中,能夠更有效率地執行 Linux 二進位檔,但應先驗證這項假設。
說明文件
我們計畫在此階段透過本 RFC 記錄 starnix。一旦我們讓非微不足道的二進位檔順利執行,就必須記錄如何在 Fuchsia 上執行 Linux 二進位檔。
缺點、替代方案和未知事項
在 Fuchsia 上執行未修改的 Linux 二進位檔,有很大的設計空間可供探索。本節將總結主要設計決策。
Linux 核心
重要設計選項是是否使用 Linux 核心本身來實作 Linux 系統介面。除了建構 starnix,我們還會建構機制,在 Machina 虛擬機器中執行 Linux 核心,藉此執行未修改的 Linux 二進位檔。這種做法的實作負擔很小,因為 Linux 核心的設計就是在虛擬機器內執行,而且 Linux 核心已包含數百個系統呼叫的實作項目,這些系統呼叫構成 Linux 系統介面。
我們可以使用 Linux 核心的方式有很多種,舉例來說,我們可以在虛擬機器中執行 Linux 核心,也可以使用 User-Mode Linux (UML) 或 Linux Kernel Library (LKL)。不過,無論我們如何執行,為了執行 Linux 二進位檔而執行整個 Linux 核心,都會造成龐大的成本。Linux 核心的主要工作是減少高階作業 (例如 write) 到低層級作業 (例如 DMA 資料至基礎硬體)。將 Linux 二進位檔整合至 Fuchsia 系統時,這項核心功能會適得其反。我們希望將 write 作業轉換為 fuchsia.io/File.Write 作業,而非將 write 作業縮減為 DMA,因為這兩者屬於同等語意層級。
同樣地,Linux 核心也附有排程器,可控管所管理程序中的執行緒。這項功能的目的,是將高階作業 (例如執行十幾個並行執行緒) 縮減為低階作業 (例如在這個處理器上執行這個時間片段)。同樣地,這項核心功能會適得其反。如果每個 Linux 二進位檔執行的執行緒,實際上都是由系統中所有其他執行緒的相同排程器排程的 Zircon 執行緒,我們就能為整個系統計算出更完善的排程。
環境
決定直接使用 Fuchsia 系統實作 Linux 系統介面後,我們需要選擇執行該實作的位置。
處理中
我們可以在與 Linux 二進位檔相同的程序中執行實作項目。舉例來說,POSIX Lite 會使用這種方法將 POSIX 作業轉換為 Fuchsia 作業。不過,如果執行未修改的 Linux 二進位檔,這個方法就比較不理想,原因有二:
如果我們在程序內執行實作項目,就必須「隱藏」Linux 二進位檔的實作項目,因為 Linux 二進位檔不會預期系統會在程序中執行 (大量) 程式碼。舉例來說,實作項目使用執行緒本機儲存空間時,必須注意不要與 Linux 二進位檔 C 執行階段管理的執行緒本機儲存空間發生衝突。
Linux 系統介面的許多部分都暗示了可變動的全球狀態。即使是程序內實作,仍需與程序外伺服器協調,才能正確實作介面的這些部分。
基於上述原因,我們選擇從程序外伺服器實作開始。不過,我們可能會將部分作業從伺服器卸載至用戶端,以提升效能。
使用者空間
在此方法中,實作會在與 Linux 程序不同的使用者空間程序中執行。我們已為 starnix 選取這個方法。這種做法的主要挑戰在於,我們需要仔細設計用於系統呼叫和用戶端記憶體存取的機制,才能提供足夠的效能。由於我們需要執行額外的內容切換來進入該程序,因此涉及第二個使用者空間程序時,會有一些無法避免的額外負擔,但其他系統的證據顯示,我們可以達到出色的效能。
Kernel
最後,我們可以在核心中執行實作內容。這是為作業系統提供外國語言介面的傳統做法。不過,為了降低核心的複雜度,我們希望避免採用這種方法。如果核心遵循明確的物件能力規範,就能更輕鬆地推斷核心行為,進而提升安全性。
相較於使用者空間實作,核心內實作的主要優點是效能。舉例來說,核心可以直接接收系統呼叫,且已具備與用戶端位址空間互動的高效能機制。如果我們能透過使用者空間方法獲得優異效能,就沒有什麼理由在核心中執行實作。
非同步信號
Linux 二進位檔會預期核心在非同步訊號處理常式中執行部分程式碼。Fuchsia 目前沒有直接在程序中叫用程式碼的機制,因此沒有明顯的機制可叫用非同步訊號處理常式。如果我們發現需要支援非同步信號處理常式的 Linux 二進位檔,就必須設法支援這項功能。
Futex
Futex 在 Fuchsia 和 Linux 上的運作方式不同。在 Fuchsia 上,futex 會根據虛擬位址建立索引鍵,而 Linux 則提供根據實體位址建立 futex 索引鍵的選項。此外,Linux futex 提供多種選項和作業,但 Fuchsia futex 無法使用。
如要實作 Linux futex 介面,我們必須在 starnix 中實作 futex,或在 Zircon 核心中新增功能,以支援 Linux 二進位檔所需的功能。
既有技術和參考資料
在非 POSIX 系統上執行 Linux (或 POSIX) 二進位檔的技術,有大量先有技術。本節將說明兩個相關系統。
WSL1
本文中的設計與第一個 Windows Subsystem for Linux (WSL1) 類似,後者是在 Windows 上實作 Linux 系統介面,能夠執行未修改的 Linux 二進位檔,包括 Ubuntu、Debian 和 openSUSE 等整個 GNU/Linux 發行版。與 starnix 不同,WSL1 在核心中執行,並為 NT 核心提供 Linux 個性化設定。
很遺憾,NTFS 的效能特性不符合 Linux 軟體的期望,因此 WSL1 受到限制。Microsoft 後來以 WSL2 取代 WSL1,透過在虛擬機器中執行 Linux 核心,提供類似的功能。在 WSL2 中,Linux 軟體是針對 ext4 檔案系統 (而非 NTFS 檔案系統) 執行。
我們應從 WSL1 汲取的重要教訓是,starnix 的效能取決於 starnix 向用戶端程式公開的基礎系統服務效能。舉例來說,如果我們希望 Linux 軟體在 Fuchsia 上順暢運作,就必須提供效能與 ext4 相當的檔案系統實作。
QNX Neutrino
QNX Neutrino 是以微核心為基礎的商用作業系統,可提供高品質的 POSIX 實作項目。本文所述的 starnix 方法與 QNX 中的 proc 伺服器類似,可處理來自用戶端程序的 POSIX 呼叫,並維護 POSIX 介面隱含的可變動全域狀態。與 starnix 類似,proc 是 QNX 上的使用者空間程序。