開啟 'Open' 的生活

為提供 Fuchsia 上檔案系統存取功能的端對端圖像,本文件將深入探討執行開啟檔案這類簡單操作時所使用的各個層級的詳細資訊。請務必注意:所有這些層都位於使用者空間中;即使與檔案系統伺服器和驅動程式互動,核心也只會用於將訊息從一個元件傳遞至另一個元件。

呼叫以下項目:

open(“foobar”);

這項要求會送到哪裡?

標準程式庫:定義「open」

「open」呼叫是 標準程式庫提供的函式。對於 C/C++ 程式,這通常會在 unistd.h 中宣告,後者在 libfdio 中具有備援定義。對於 Go 程式,Go 標準程式庫中也有等同 (但不同的) 實作方式。針對每種語言和執行階段,開發人員可以選擇自己的「開放」定義。

在單體核心上,open 會是系統呼叫周圍的輕量 shim,核心可能會處理路徑剖析、重新導向等作業。在該模型中,核心需要根據呼叫端的相關外部資訊,調解資源存取權。不過,Zircon 核心會刻意不提供這類系統呼叫。相反地,用戶端會透過管道存取檔案系統。當程序初始化時,系統會提供一個命名空間,這是「絕對路徑」->「句柄」對應項目的表格。透過這個命名空間對應,可將從程序中存取的所有路徑都開啟。

不過,在本例中,由於涉及開啟「foobar」的要求,因此使用了相對路徑,因此可透過代表目前工作目錄的路徑 (本身以絕對路徑和句柄表示) 傳送傳入的呼叫。

標準程式庫負責取得句柄 (或多個句柄),並將其顯示為檔案描述元。因此,「檔案描述符」表是用於用戶端程序中的概念 (如果用戶端選擇使用自訂執行階段,則可將資源純粹視為句柄,而「檔案描述符」包裝則為選用項目)。

不過,這會引發一個問題:如果有檔案、通訊端、管道等檔案描述符,標準程式庫會如何讓所有這些資源在功能上看起來相同?該用戶端如何得知要透過這些句柄傳送哪些訊息?

Fdio

名為 fdio 的程式庫負責為各種資源 (檔案、通訊端、服務、管道等) 提供統一的介面。這個層定義了一組函式,例如 read、write、open、close、seek 等,這些函式可用於由各種通訊協定支援的檔案描述元。每個支援的通訊協定都負責提供用戶端程式碼,以解讀其互動行為的具體細節。舉例來說,Socket 會為用戶端提供多個句柄,一個用於資料流,另一個用於控制層。相反地,檔案通常只使用單一管道進行控制和資料處理 (除非已額外執行作業來要求記憶體對應)。雖然 Socket 和檔案都可能會收到對 openwrite 的呼叫,但它們需要以不同的方式解讀這些指令。

為了配合本文件的目的,我們將著重於檔案系統用戶端使用的主要通訊協定: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 物件的要求可以在物件實際開啟前傳送。capabilities這個模型可讓用戶端立即開始傳送要求,而非在元件完成啟動程序並開始提供要求時才解除封鎖,然後在元件就緒時回應。如要進一步瞭解這項行為如何套用至能力轉送,請參閱「通訊協定開啟時間」。

回顧一下,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 |
+-------------------+