Starnix 核心

Starnix 核心負責在 Fuchsia 上實作 Linux 使用者空間 API (UAPI)。Starnix 核心會攔截來自 Linux 程序的系統呼叫,並實作讓 Linux 程式正確執行的必要語意。本文說明 Starnix 核心的內部結構。

愜意

Starnix 的目標是執行未修改的 Linux 二進位檔。為以現有形式執行這些二進位檔,Starnix 的目標是與 Linux 核心達到完全相容。我們在這個精確度層級進行互通性測試時,通常會採取廣泛測試的做法。如要瞭解 Linux UAPI 的語意,我們會編寫測試來探查介面的邊緣和極端情況。這些測試在 Linux 核心上通過後,我們會在持續整合基礎架構中執行測試,如果測試在 Starnix 上未通過,就會標示為失敗。隨著 Starnix 改善,這些測試最終會「意外通過」,也就是說我們可以將其標示為通過。

單元測試與使用者空間測試

在絕大多數情況下,我們偏好編寫使用者空間程式來測試 Starnix,並將其編譯為 Linux 二進位檔。使用這種方法,我們可以對 Starnix 和 Linux 核心執行相同的測試,確保兩者行為一致。

在某些情況下,我們會使用單元測試測試 Starnix,這些測試會在 Starnix 核心中執行,並依附於未透過 UAPI 公開的 Starnix 實作詳細資料。這種做法的缺點是,我們無法確保這些測試預期的行為與 Linux 核心的行為相符。此外,隨著實作方式演進,這些測試也更可能需要維護。不過,這種做法很有用,因為在某些情況下,存取核心內部元件可大幅簡化測試。

在某些情況下,我們會使用整合測試執行使用者空間程式,但測試斷言位於 Fuchsia 端。這類測試並不常見,但有助於驗證使用者空間不易存取的恆定值。

track_stub!

Linux UAPI 的語意非常廣泛。即使是個別的系統呼叫或虛擬檔案,也可能具備更多功能,我們隨時準備好實作。為追蹤我們尚未實作的語意,我們會使用 track_stub! 巨集。這個巨集會記錄缺少功能的程式碼路徑、將這些程式碼路徑與錯誤追蹤器中的錯誤繫結,並檢測 Starnix 核心二進位檔,讓我們觀察 Linux 程式何時會執行這些程式碼路徑。

從概念上來說,原始的 Starnix 核心實作方式是針對系統呼叫號碼的 match 陳述式,透過呼叫 track_stub! 巨集「實作」每個系統呼叫。我們嘗試執行越來越多複雜的 Linux 程式時,不得不將這個巨集的例項替換為實際的系統呼叫實作項目。不過,許多系統呼叫都有選項或不同模式。我們在這些系統呼叫中推送了 track_stub! 巨集,以「實作」缺少的選項或模式。

我們內部設有資訊主頁,其中包含 track_stub! 巨集執行頻率和情境的統計資料。

隨著我們在 Starnix 中實作更多功能,應繼續使用 track_stub! 巨集追蹤進度。

結構

Starnix 核心是以多個 Rust Crate 實作而成。Starnix 核心本身就是 Crate,只包含 main.rs,這是主要進入點,但沒有其他內容。相反地,核心機制的 Kernel 位於依附元件圖表中間的 starnix_core Crate 中。

流程模型

Starnix 核心會以工作中的 Fuchsia 程序集合形式執行。每個 Linux 程序 (嚴格來說是每個 Linux 位址空間,因為 Linux UAPI 中沒有「程序」這種東西) 都會對應一個 Fuchsia 程序,另外還有一個額外的 Fuchsia 程序。額外的 Fuchsia 程序是初始程序,Starnix 核心二進位檔會載入這個程序,並開始執行。這個程序具有 Starnix 共用位址空間,但沒有受限位址空間

初始程序的主要執行緒會執行一般的 Fuchsia 非同步執行器,並回應 FIDL 要求。舉例來說,這個執行緒會處理要求,讓核心執行 Starnix 容器。這個程序也包含稱為「kthread」的背景執行緒,可為核心執行背景工作。這些執行緒必須在初始程序中執行,才能在導致建立這些執行緒的任何使用者空間程序終止後繼續執行。

starnix_syscall_loop

建立後,使用者空間執行緒會進入主要的 syscall 迴圈,該迴圈是由 starnix_syscall_loop Crate 實作。在這個迴圈中,執行緒會進入使用者模式 (即受限模式),並處於特定機器狀態。最後,Linux 程式會結束使用者模式,執行緒的控制權會返回 Starnix 核心。離開使用者模式最常見的原因是程式發出系統呼叫,但執行緒也可能因其他原因離開使用者模式,例如發生例外狀況或遭踢回核心模式。

每當 Linux 程式發出系統呼叫時,starnix_syscall_loop Crate 中的 dispatch_syscall 函式會解碼系統呼叫,並呼叫適當的系統呼叫實作函式。將 dispatch_syscall 函式放在與系統呼叫實作項目不同的 Crate 中,可讓我們在多個 Crate 中分片實作系統呼叫。目前,絕大多數的系統呼叫實作項目都在 starnix_core Crate 中,但我們計畫將這些項目移出該 Crate,以逐步降低 starnix_core Crate 的複雜度。

模組

Starnix 核心的許多功能都是以模組的形式實作。初始化時,Starnix 核心會初始化每個模組。大多數模組都受到功能旗標保護,也就是說,只有在啟用對應的功能旗標時,模組才會初始化。初始化期間,模組通常會向 starnix_core 註冊。舉例來說,實作裝置的模組會向 DeviceRegistry 註冊自己,做為適當的主要和次要裝置號碼的處理常式。同樣地,實作檔案系統的模組會向 FsRegistry 註冊。

starnix_core 不會直接呼叫模組。而是為提供的抽象概念實作適當的特徵。舉例來說,提供裝置的模組會實作 DeviceOps 特徵,提供檔案系統的模組則會實作 FileSystemOps 特徵。這些特徵通常會傳回實作其他特徵的物件,例如 FileOpsFsNodeOps

需要儲存核心全域狀態的模組應使用 kernel.expando 物件,而非在 Kernel 結構體上定義自己的欄位。核心擴充功能會以 Rust 型別做為鍵,讓每個模組定義自己的儲存空間,而不必擔心與其他模組發生衝突。此外,這個機制可避免為未使用的模組提交資源。

starnix_core

starnix_core Crate 包含 Starnix 核心的核心機制。這個 Crate 負責處理工作、記憶體管理、裝置註冊和虛擬檔案系統 (VFS)。這些子系統彼此密切相關,且有許多循環依附元件。starnix_core 的許多設計都記錄在原始碼的 rustdoc 註解中。

程式庫

如果 starnix_core、模組或 Starnix 的其他部分需要程式碼,但該程式碼不依附於 starnix_core,我們偏好在 //src/starnix/lib 目錄中將該程式碼實作為獨立 Crate。使用個別 Crate 可限制程式碼的依附元件圖表,因此更容易瞭解程式碼。此外,使用個別 Crate 可縮短漸進式建構時間,因為修改 starnix_core 時不需要重建這個程式碼。

UAPI

starnix_uapi Crate 位於 Starnix 核心的依附元件圖底部。這個 Crate 會為 Linux UAPI 中定義的概念,定義符合人體工學的 Rust 型別。舉例來說,Linux UAPI 可能會定義具有各種位元語意的 u32。為了讓使用這類型的操作更符合人體工學,starnix_uapi Crate 可能會使用 bitflags! 巨集,為這些位元定義 Rust 型別。

UserAddress 型別代表使用者空間位址,定義於 starnix_uapi Crate 中。處理使用者空間記憶體的指標時,Starnix 核心會使用這種類型,而非 Rust 指標,避免意外取消參照這類指標。這種做法可避開兩項危險。首先,使用者空間可能會提供核心位址,而非使用者空間位址,這可能會誘騙核心操控自己的記憶體。其次,除非核心使用 usercopy 機制安全地執行存取作業,否則存取使用者空間位址時,核心可能會發生恐慌。

starnix_uapi Crate 依附於 linux_uapi Crate,後者是使用 bindgen 從 Linux 程式使用的 Linux UAPI 的 C 定義自動產生。