為提供 Fuchsia 上檔案系統存取權的端對端圖像,本文件將深入探討執行開啟檔案這類簡單操作時所使用的各個層級的詳細資料。請務必注意:所有這些層都位於使用者空間中;即使與檔案系統伺服器和驅動程式互動,核心也只會用於將訊息從一個元件傳遞至另一個元件。
呼叫以下項目:
open(“foobar”);
存取要求的位置
標準程式庫:定義「open」
「open」呼叫是 標準程式庫提供的函式。對於 C/C++ 程式,這通常會在 unistd.h
中宣告,後者在 libfdio 中具有備援定義。對於 Go 程式,Go 標準程式庫中也有等同 (但不同的) 實作。開發人員可以選擇針對每個語言和執行階段選擇自己的「開放」定義。
在單體核心上,open
會是系統呼叫周圍的輕量 shim,核心可能會處理路徑剖析、重新導向等作業。在該模型中,核心需要根據呼叫端的相關外部資訊,調解資源存取權。不過,Zircon 核心並沒有這類系統呼叫。相反地,用戶端會透過管道存取檔案系統。當程序初始化時,系統會提供一個命名空間,這是「絕對路徑」->「句柄」對應項目的表格。在程序中存取的所有路徑,都會透過此命名空間對應引導要求開啟。
不過,在本例中,由於涉及開啟「foobar」的要求,因此使用了相對路徑,因此可透過代表目前工作目錄的路徑 (本身以絕對路徑和句柄表示) 傳送傳入的呼叫。
標準程式庫負責取得句柄 (或多個句柄),並將其顯示為檔案描述元。因此,「檔案描述符」表是用於用戶端程序中的概念 (如果用戶端選擇使用自訂執行階段,則可將資源純粹視為句柄,而「檔案描述符」包裝則為選用項目)。
不過,這會引發一個問題:如果有檔案、通訊端、管道等檔案描述符,標準程式庫會如何讓所有這些資源在功能上看起來相同?客戶該如何知道要透過這些控制代碼傳送哪些訊息?
Fdio
fdio 程式庫負責為各種資源 (檔案、通訊端、服務、管道等) 提供整合式介面。這個層定義了一組函式,例如 read、write、open、close、seek 等,這些函式可用於由各種通訊協定支援的檔案描述元。每個支援的通訊協定都負責提供用戶端程式碼,以解讀其互動行為的具體細節。舉例來說,Socket 會為用戶端提供多個句柄,一個用於資料流,另一個用於控制層。相反地,檔案通常只會使用單一管道來控管及傳輸資料 (除非已額外執行作業來要求記憶體對應)。雖然通訊端和檔案都可能會收到對 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 訊息的「data」欄位設為字串「foo」。此外,如果有任何標記會傳遞至 open (例如 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 物件的要求可以在物件實際開啟前傳送。此行為對於與 這個模型不會禁止在元件完成啟動程序並開始處理要求時封鎖開啟,而是讓用戶端立即開始傳送要求,並在元件就緒時做出回應。如要進一步瞭解這項行為如何套用至能力轉送,請參閱通訊協定開放的生命週期。
回顧一下,open 呼叫已透過標準程式庫,對「CWD」fdio 物件執行動作,將要求轉換為 FIDL 訊息,並透過 zx_channel_write
系統呼叫傳送至伺服器。用戶端可選擇使用 zx_object_wait_one
等待伺服器回應,或以非同步方式繼續處理。無論是哪種方式,都會建立管道,其中一個端會與用戶端共用,另一端則會傳輸至「伺服器」。
檔案系統:伺服器端
調度
訊息從管道的用戶端傳送後,就會留在管道的伺服器端,等待讀取。伺服器會由「持有管道另一端句柄的一方」識別。也就是說,伺服器可能與用戶端位於相同 (或不同的) 程序中,使用與用戶端相同 (或不同的) 執行階段,並以與用戶端相同 (或不同的) 語言編寫。透過使用已達成共識的電報格式,可在通道上發生的薄型通訊層中,將跨程序依附元件瓶頸化。
在未來的某個時間點,這個 CWD 控制代碼的伺服器端端需要讀取用戶端傳輸的訊息。這項程序並非自動執行,因此伺服器必須特意等待接收句柄的傳入訊息,在本例中就是「目前工作目錄」句柄。開啟伺服器物件 (檔案、目錄、服務等) 時,其句柄會註冊至伺服器端 Zircon port,等待基礎句柄「可讀取」(表示已收到訊息) 或「關閉」(表示不會再收到任何訊息)。這個物件會將傳入要求分派給適當控制代碼,稱為調度工具。它負責將傳入的訊息重新導向至回呼函式,以及先前提供的代表開放連線的「iostate」。
對於使用 libfs 的 C++ 檔案系統,這個回呼函式會稱為 vfs_handler
,並接收幾個重要資訊:
- 由用戶端提供的 FIDL 訊息 (或由伺服器人為建構,以便在句柄關閉時顯示「關閉」訊息)
- 代表目前與句柄連線的 I/O 狀態 (以先前提及的「iostate」欄位傳遞)。
vfs_handler
可解讀 I/O 狀態,推斷其他資訊:
- 檔案內的搜尋指標 (如果已使用 readdir,就會位於 目錄中)
- 用來開啟基礎資源的旗標
- 代表基礎物件的 Vnode (可由多個用戶端或多個檔案描述元共用)
這個處理常式會根據用戶端提供的「operation」欄位,將 FIDL 訊息重新導向至適當的函式,扮演大型「switch/case」表格的角色。在開放式情況下,系統會將 Open
序數視為作業,因此 (1) 預期的帳號代碼,且 (2)「data」欄位 (「foo」) 為路徑。
VFS 層
在 Fuchsia 中,「VFS 層」是與檔案系統無關的程式碼程式庫,可在適當情況下,調度及解讀伺服器端訊息,並在基礎檔案系統中呼叫作業。值得注意的是,這個層級完全屬於選用層級,如果檔案系統伺服器不想連結至這個程式庫,就沒有使用這個程式庫的義務。如要成為檔案系統伺服器,程序只需瞭解 FIDL 電線格式即可。因此,在某種語言中可能會有多個「VFS」實作。目前有以下實作方式:
- 樹狀結構中的 C++ VFS:Fuchsia 的「主要」檔案系統 minfs 和 blobfs 會使用。目前,它擁有任何 VFS 實作項目中最多的功能,但也可能是最難使用的。
- 樹狀結構中的 Rust VFS:部分 Rust 檔案系統會使用此類別,包括 fat32 實作。這項功能較新,目前的功能比 C++ 實作項目少。
- SDK C++ VFS:這是 SDK 使用者專用的「樹狀結構」C++ 版本的簡化版本。這類服務通常用於服務探索等較簡單的用途。
VFS 層會定義可轉送至基礎檔案系統的作業介面,包括:
- 讀取/寫入 Vnode
- 從父 Vnode 中查詢/建立/取消連結 Vnode (依名稱)
- 依名稱重新命名/連結 Vnode
- 其他應用程式
如要實作檔案系統 (假設開發人員想要使用共用 VFS 層),只需定義實作此介面的 Vnode,並連結至 VFS 層即可。這麼做可讓您以最少的努力,幾乎不重複程式碼的方式,提供「路徑檢查」和「檔案系統掛載」等功能。為了讓 VFS 層不受檔案系統限制,因此不會預先假設檔案系統使用的基礎儲存空間:檔案系統可能需要存取區塊裝置、網路或記憶體,才能儲存資料,但 VFS 層只會處理路徑、資料的位元組陣列和 vnode 的介面。
路徑探索
如要開啟伺服器端資源,伺服器會提供一些起點 (由呼叫的句柄表示) 和字串路徑。這個路徑會以「/」字元分割成多個區段,並透過對底層檔案系統的回呼「查詢」每個元件。如果查詢成功傳回 vnode,且偵測到另一個「/」區段,則程序會繼續執行,直到 (1) lookup
找不到元件、(2) 路徑處理程序到達路徑中的最後一個元件,或 (3) lookup
找到掛載點 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 |
+-------------------+