為提供 Fuchsia 檔案系統存取的端對端圖片,本文將深入探討執行簡單動作 (例如開啟檔案) 時使用的每個層。請注意,所有這些層都存在於使用者空間;即使與檔案系統伺服器和驅動程式互動,核心也只會用於將訊息從一個元件傳遞至另一個元件。
系統會撥打電話給:
open(“foobar”);
這項要求會傳送至何處?
標準程式庫:定義「open」的位置
「open」呼叫是由標準程式庫提供的函式。如果是 C/C++ 程式,這通常會在 unistd.h 中宣告,而 libfdio 中有備份定義。如果是 Go 程式,Go 標準程式庫中也有對應 (但不同) 的實作方式。開發人員可以選擇自行定義每種語言和執行階段的「開放」狀態。
在單一核心上,open 會是系統呼叫周圍的輕量型墊片,核心可能會處理路徑剖析、重新導向等。在該模型中,核心需要根據呼叫端的外部知識,調解資源存取權。不過,Zircon 核心刻意沒有這類系統呼叫。用戶端會改用「管道」存取檔案系統。初始化程序時,系統會提供「命名空間」,這是「絕對路徑」->「控制代碼」對應的表格。從程序內存取的所有路徑,都會透過這個命名空間對應來導向要求,進而開啟。
不過,在這個範例中,由於涉及開啟「foobar」的要求,因此使用了相對路徑,傳入的呼叫可以透過代表目前工作目錄的路徑傳送 (該路徑本身以絕對路徑和控制代碼表示)。
標準程式庫負責取得控制代碼 (或多個控制代碼),並讓這些控制代碼看起來像檔案描述元。因此,「檔案描述元表格」是存在於用戶端程序中的概念 (如果用戶端選擇使用自訂執行階段,可以純粹將資源視為控制代碼,而「檔案描述元」包裝是選用項目)。
不過,這也引發一個問題:如果提供檔案、通訊端、管道等的檔案描述元,標準程式庫會採取哪些做法,讓所有這些資源在功能上看起來都相同?該用戶端如何知道要透過這些控制代碼傳送哪些訊息?
Fdio
名為 fdio 的程式庫負責為各種資源 (檔案、插座、服務、管道等) 提供統一介面。這個層級定義了一組函式,例如 read、write、open、close、seek 等,可用於各種通訊協定支援的檔案描述元。每個支援的通訊協定都負責提供用戶端程式碼,以解讀互動的具體細節。舉例來說,通訊端會為用戶端提供多個控制代碼,一個用於資料流程,另一個則做為控制層。相較之下,檔案通常只會使用單一管道進行控制和資料傳輸 (除非已額外要求記憶體對應)。雖然插座和檔案可能會收到 open 或 write 的呼叫,但兩者需要以不同方式解讀這些指令。
本文將著重於檔案系統用戶端使用的主要通訊協定:FIDL。
FIDL
呼叫 open("foo") 的程式會呼叫標準程式庫,找到與目前工作目錄對應的「fdio」物件,並需要向遠端伺服器傳送「請開啟 foo」的要求。該如何達成這項要求?該計畫提供下列工具:
- 代表 CWD 連線的一或多個控制代碼
- zx_channel_write: 可傳送位元組和控制碼 (透過管道) 的系統呼叫
- zx_channel_read: 可接收位元組和控制碼 (透過管道) 的系統呼叫
- zx_object_wait_one: 可等待控制代碼可讀取 / 寫入的系統呼叫
使用這些基本項目,用戶端可以在 CWD 控制代碼上將訊息寫入檔案系統伺服器,伺服器可以讀取訊息,然後在寫回用戶端時,以「成功」或「失敗訊息」回應。伺服器正在處理資料,判斷實際要開啟的內容時,用戶端可能會選擇等待,也可能不會,然後嘗試讀取狀態訊息。
用戶端和伺服器必須就這些 N 位元組和 N 控制代碼的解讀方式達成共識,才能傳送或接收訊息。如果雙方意見不一致,訊息可能會遭到捨棄 (或更糟的是,扭曲成非預期的行為)。此外,如果這個通訊協定允許用戶端任意控制伺服器,這個通訊層就很容易遭到濫用。
FIDL IO 通訊協定說明在兩個實體之間傳輸時,這些位元組和控制代碼實際應代表的線路格式。通訊協定會說明「預期控制代碼數量」、「列舉作業」和「資料」等項目。在我們的案例中,open("foo") 會建立 Open 訊息,並將 FIDL 訊息的「資料」欄位設為字串「foo」。此外,如果任何標記傳遞至開啟 (例如 O_RDONLY, O_RDWR, O_CREAT 等),這些標記會放在 FIDL 結構的「arg」欄位中。不過,如果作業已變更 (例如變更為 write),這則訊息的解讀方式就會改變。
這個層級的位元組一致性至關重要,因為這可讓截然不同的執行階段進行通訊:瞭解 FIDL 的程序可在 C、C++、Go、Rust、Dart 程式 (和其他程式) 之間輕鬆進行通訊,且過程完全透明。
libfidl 包含 FIDL C/C++ 實作的用戶端和伺服器端程式碼,負責自動驗證兩端的輸入和輸出內容。
如果是 open 作業,FIDL 通訊協定會預期用戶端建立管道,並將一端 (做為控制代碼) 傳遞至伺服器。交易完成後,這個管道可用於與開啟的檔案通訊,就像先前與「CWD」控制代碼通訊一樣。
由於 FIDL 用戶端提供控制通訊協定,而非伺服器,因此通訊更適合用於管道化。存取 FIDL 物件可以是非同步作業;要求可以傳輸至 FIDL 物件,然後再實際開啟物件。這項行為對於與功能互動至關重要,因為系統預設會在要求能力時,透過開放式呼叫延遲啟動元件。這個模型不會在元件完成啟動程序並開始處理要求時封鎖開啟作業,而是允許用戶端立即開始傳送要求,元件準備就緒後就會回應要求。如要進一步瞭解這項行為如何套用至能力轉送,請參閱「通訊協定開啟的生命週期」。
回顧一下,「開啟」呼叫已通過標準程式庫,並對「CWD」fdio 物件採取行動,將要求轉換為 FIDL 訊息,然後使用 zx_channel_write 系統呼叫傳送至伺服器。用戶端可以選擇使用 zx_object_wait_one 等候伺服器回應,或繼續非同步處理。無論是哪種方式,系統都會建立管道,一端位於用戶端,另一端則會傳輸至「伺服器」。
檔案系統:伺服器端
Dispatching
訊息從管道的用戶端傳輸後,就會存放在管道的伺服器端,等待讀取。伺服器是由「持有管道另一端控制代碼的人」所識別,可能與用戶端位於相同 (或不同) 的程序、使用與用戶端相同 (或不同) 的執行階段,以及以與用戶端相同 (或不同) 的語言編寫。使用雙方同意的線路格式時,跨程序依附元件會受限於透過管道發生的精簡通訊層。
在未來的某個時間點,CWD 控制代碼的伺服器端需要讀取用戶端傳輸的訊息。這項程序不會自動執行,伺服器必須刻意等待接收控制代碼上的傳入訊息,在本例中,接收控制代碼是「目前的工作目錄」控制代碼。開啟伺服器物件 (檔案、目錄、服務等) 時,系統會向伺服器端 Zircon 埠註冊這些物件的控制代碼,等待這些物件的基礎控制代碼可讀取 (表示已收到訊息) 或關閉 (表示不會再收到訊息)。這個物件會將傳入的要求分派給適當的控制代碼,因此稱為分派器。這個函式負責將傳入的訊息重新導向至回呼函式,以及一些先前提供的「iostate」,代表開啟的連線。
對於使用 libfs 的 C++ 檔案系統,系統會呼叫這個回呼函式 vfs_handler,並接收幾項重要資訊:
- 用戶端提供的 FIDL 訊息 (或伺服器人工建構的訊息,看起來像是「關閉」訊息,如果控制代碼已關閉)
- 代表目前與控制代碼連線的 I/O 狀態 (以「iostate」欄位傳遞,如先前所述)。
vfs_handler 可以解讀 I/O 狀態,推斷額外資訊:
- 檔案內的搜尋指標 (或目錄內的搜尋指標,如果已使用 readdir)
- 用於開啟基礎資源的旗標
- Vnode 代表基礎物件 (可能在多個用戶端或多個檔案描述元之間共用)
這個處理常式函式會根據用戶端提供的「作業」欄位,將 FIDL 訊息重新導向至適當的函式,因此可視為大型「switch/case」資料表。在開啟的案例中,Open 序數會視為作業,因此 (1) 系統會預期有控點,且 (2)「data」欄位 (「foo」) 會解讀為路徑。
VFS 層
在 Fuchsia 中,「VFS 層」是與檔案系統無關的程式碼程式庫,可視情況調度及解讀伺服器端訊息,並在基礎檔案系統中呼叫作業。值得注意的是,這個層完全是選用項目,如果檔案系統伺服器不想連結這個程式庫,就沒有義務使用。如要成為檔案系統伺服器,程序只需瞭解 FIDL 線路格式。因此,某種語言中可能會有任意數量的「VFS」實作項目。目前有下列實作方式:
- 樹狀結構內 C++ VFS:由 Fuchsia 的「主要」檔案系統 minfs 和 blobfs 使用。目前這是功能最多的 VFS 實作項目,但也是最難使用的項目。
- 樹狀結構內 Rust VFS:部分 Rust 檔案系統會使用這個 VFS,包括 fat32 實作項目。這個版本較新,目前的功能比 C++ 實作版本少。
- SDK C++ VFS:為 SDK 使用者提供的「樹狀結構內」C++ 版本,經過某種程度的簡化。這最常用於服務探索等較簡單的用途。
VFS 層會定義可路由至基礎檔案系統的作業介面,包括:
- 讀取/寫入 Vnode
- 從父項 Vnode 查閱/建立/取消連結 Vnode (依名稱)
- 依名稱重新命名/連結 Vnode
- 其他應用程式
如要實作檔案系統 (假設開發人員想使用共用的 VFS 層),只要定義實作這個介面的 Vnode,並連結至 VFS 層即可。這樣一來,您就能以最少的力氣,使用「路徑走訪」和「檔案系統掛接」等功能,而且幾乎不會有重複的程式碼。為避免受檔案系統限制,VFS 層不會預先瞭解檔案系統使用的基礎儲存空間:檔案系統可能需要存取區塊裝置、網路或單純的記憶體來儲存資料,但 VFS 層只會處理作用於路徑、資料位元組陣列和 vnode 的介面。
Path Walking
如要開啟伺服器端資源,伺服器會取得一些起點 (以呼叫的控制代碼表示) 和字串路徑。這個路徑會依「/」字元分割成多個區段,並透過回呼至基礎檔案系統「查閱」每個元件。如果查閱作業順利傳回 vnode,且系統偵測到另一個「/」區段,程序就會繼續執行,直到 (1) lookup 無法找到元件、(2) 路徑處理作業抵達路徑中的最後一個元件,或 (3) lookup 找到掛接點 vnode 為止。掛接點 vnode 是附加「遠端」控制代碼的 vnode。目前我們會忽略掛接點 Vnode,但會在檔案系統掛接一節中討論。
假設 lookup 成功找到「foo」Vnode。檔案系統伺服器會繼續呼叫 VFS 介面「Open」,驗證是否能使用提供的標記存取所要求的資源,然後呼叫「GetHandles」,詢問基礎檔案系統是否需要其他控制代碼才能與 Vnode 互動。假設用戶端同步要求「foo」物件 (預設 POSIX 開啟呼叫會隱含這項要求),與「foo」互動所需的任何其他控制代碼都會封裝成小型 FIDL 說明物件,並傳回給用戶端。或者,如果「foo」無法開啟,系統仍會傳回 FIDL 說明物件,但「status」欄位會設為錯誤代碼,表示失敗。假設「foo」開啟成功。伺服器會繼續為「foo」建立「iostate」物件,並向調度器註冊。這樣一來,伺服器就能處理日後對「foo」的呼叫。「Foo」已開啟,用戶端現在可以傳送其他要求。
從用戶端的角度來看,在「Open」呼叫開始時,路徑和控制代碼組合會透過 CWD 控制代碼傳輸至遠端檔案系統伺服器。由於呼叫是同步的,因此用戶端會繼續等待控制代碼上的回應。伺服器正確找到、開啟並初始化這個檔案的 I/O 狀態後,就會傳回「成功」的 FIDL 說明物件。用戶端會讀取這個物件,判斷呼叫是否成功完成。此時,用戶端可以建立代表「foo」控制代碼的 fdio 物件,並在檔案描述元表格中以項目參照該物件,然後將 fd 傳回給呼叫原始「open」函式的任何對象。此外,如果用戶端想對「foo」傳送任何其他要求 (例如「讀取」或「寫入」),則可使用開啟檔案的連線,直接與檔案系統伺服器通訊,日後的要求不必再透過「CWD」路徑傳送。
公開賽的生命週期:圖表
+----------------+
| Client Program |
+----------------+
| fd: x | fd: y |
| Fdio (FIDL)| Fdio (FIDL)|
+-------------------------+
| '/' Handle | CWD Handle |
+-------------------------+
^ ^
| |
Zircon Channels, speaking FIDL State BEFORE open(‘foo’)
| |
v v
+-------------------------+
| '/' Handle | CWD Handle |
+-------------------------+
| I/O State | I/O State |
+-------------------------+
| Vnode A | Vnode B |
+-------------------------+
| Filesystem Server |
+-------------------+
+----------------+
| Client Program |
+-------------------------+
| fd: x | fd: y |
| Fdio (FIDL)| Fdio (FIDL)|
+-------------------------+
| '/' Handle | CWD Handle | **foo Handle x2**
+-------------------------+
^ ^
| |
Zircon Channels, speaking FIDL Client Creates Channel
| |
v v
+-------------------------+
| '/' Handle | CWD Handle |
+-------------------------+
| I/O State | I/O State |
+-------------------------+
| Vnode A | Vnode B |
+-------------------------+
| Filesystem Server |
+-------------------+
+----------------+
| Client Program |
+-------------------------+
| fd: x | fd: y |
| Fdio (FIDL)| Fdio (FIDL)|
+-------------------------+--------------+
| '/' Handle | CWD Handle | ‘foo’ Handle |
+-------------------------+--------------+
^ ^
| |
Zircon Channels, speaking FIDL Client Sends FIDL message to Server
| | Message includes a ‘foo’ handle
v v (and waits for response)
+-------------------------+
| '/' Handle | CWD Handle |
+-------------------------+
| I/O State | I/O State |
+-------------------------+
| Vnode A | Vnode B |
+-------------------------+
| Filesystem Server |
+-------------------+
+----------------+
| Client Program |
+-------------------------+
| fd: x | fd: y |
| Fdio (FIDL)| Fdio (FIDL)|
+-------------------------+--------------+
| '/' Handle | CWD Handle | ‘foo’ Handle |
+-------------------------+--------------+
^ ^
| |
Zircon Channels, speaking FIDL Server dispatches message to I/O State,
| | Interprets as ‘open’
v v Finds or Creates ‘foo’
+-------------------------+
| '/' Handle | CWD Handle |
+-------------------------+
| I/O State | I/O State |
+-------------------------+-------------+
| Vnode A | Vnode B | Vnode C |
+------------------------------+--------+
| Filesystem Server |
+-------------------+
+----------------+
| Client Program |
+-------------------------+
| fd: x | fd: y |
| Fdio (FIDL)| Fdio (FIDL)|
+-------------------------+--------------+
| '/' Handle | CWD Handle | ‘foo’ Handle |
+-------------------------+--------------+
^ ^ ^
| | |
Zircon Channels, FIDL | Server allocates I/O state for Vnode
| | | Responds to client-provided handle
v v v
+-------------------------+--------------+
| '/' Handle | CWD Handle | ‘foo’ Handle |
+-------------------------+--------------+
| I/O State | I/O State | I/O State |
+-------------------------+--------------+
| Vnode A | Vnode B | Vnode C |
+------------------------------+---------+
| Filesystem Server |
+-------------------+
+----------------+
| Client Program |
+-----------------------------+----------+
| fd: x | fd: y | fd: z |
| Fdio (FIDL)| Fdio (FIDL)| Fdio (FIDL) |
+-------------------------+--------------+
| '/' Handle | CWD Handle | ‘foo’ Handle |
+-------------------------+--------------+
^ ^ ^
| | |
Zircon Channels, speaking FIDL | Client recognizes that ‘foo’ was opened
| | | Allocated Fdio + fd, ‘open’ succeeds.
v v v
+-------------------------+--------------+
| '/' Handle | CWD Handle | ‘foo’ Handle |
+-------------------------+--------------+
| I/O State | I/O State | I/O State |
+-------------------------+--------------+
| Vnode A | Vnode B | Vnode C |
+------------------------------+---------+
| Filesystem Server |
+-------------------+