開啟 'Open' 的生活

為提供 Fuchsia 檔案系統存取權的端對端說明,本文將深入介紹開啟檔案等簡單的操作時,各個圖層使用的細節。請特別注意,這些層都存在於使用者空間中,即使與檔案系統伺服器和驅動程式互動,核心也只會用來在元件之間傳遞訊息。

通話對象:

open(“foobar”);

這項要求會傳送到哪裡?

標準程式庫:定義「open」的位置

「開啟」呼叫是由標準程式庫提供的函式。就 C/C++ 程式而言,這通常會在 unistd.h 中宣告,其在 libfdio 中有支援定義的。Go 程式在 Go 標準程式庫中提供了對應 (但不同) 的實作項目。針對各個語言和執行階段,開發人員可以選擇自行選擇「開放式」定義。

在單體核心上,open 是系統呼叫的輕量填充碼,核心可能會處理路徑剖析、重新導向等。在這個模型中,核心需要根據呼叫端的外部瞭解來調解資源存取權。不過,Zircon 核心會刻意沒有這類系統呼叫。用戶端會透過「管道」存取檔案系統,而程序初始化時會提供一個「命名空間」,這是由「絕對路徑」->「處理常式」對應的資料表。從程序中存取的所有路徑都會透過此命名空間對應引導要求來開啟。

但在本例中,如果要求開啟「foobar」,系統就會使用相對路徑,因此可透過代表目前工作目錄的路徑 (本身以絕對路徑和控制代碼表示) 的路徑傳送來電。

標準程式庫負責取用 (或多個控制代碼),並使其以檔案描述元的形式顯示。因此,「檔案描述元資料表」是用戶端程序中的一種概念 (如果用戶端選擇使用自訂執行階段,就只能以處理常式的形式查看資源,「檔案描述元」包裝是選擇性的)。

這會造成問題,但如果有檔案描述元為檔案、通訊端、管道等,標準程式庫會採取什麼做法,讓這些資源的所有功能都相同?該用戶端如何得知要傳送哪些訊息?

法迪奧

名為 fdio 的程式庫負責為各種資源 (檔案、通訊端、服務、管道等) 提供統一的介面。此層定義了一組函式 (例如讀取、寫入、開啟、關閉、搜尋等),可用於多種通訊協定的檔案描述元。每個支援的通訊協定均負責提供用戶端程式碼,以便解讀互動的具體細節。舉例來說,通訊端會向用戶端提供多個控制代碼,一個用於資料流程,另一個則做為控制層。相反地,檔案通常只會使用單一管道來管理及資料 (除非已執行額外工作要求記憶體對應)。雖然通訊端和檔案都可能會收到對 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」。此外,如果任何標記傳遞至開放式 (例如 O_RDONLY, O_RDWR, O_CREAT 等),這些標記會放在 FIDL 結構的「arg」欄位中。不過,如果作業有所變更 (例如 write),這則訊息的解釋就會改變。

這層確切的位元組協議非常重要,因為這可讓明顯不同的執行階段之間進行通訊:瞭解 FIDL 的程序可在 C、C++、Go、Rust、Dart 程式 (和其他項目) 之間輕鬆公開通訊。

libfidl 包含 C/C++ 實作的 C/C++ 實作用戶端和伺服器端程式碼,負責自動驗證兩者的輸入和輸出內容。

如果是 open 作業,FIDL 通訊協定會預期用戶端將建立管道,並將一端 (做為控制代碼) 傳遞至伺服器。交易完成後,此管道可能會做為與已開啟檔案通訊的機制,就像之前與「CWD」控制代碼一樣。

藉由設計通訊協定,FIDL 用戶端以提供控制代碼 (而非伺服器) 的方式,通訊就更適合管道。FIDL 物件的存取權可以是非同步的;對 FIDL 物件的要求可以在實際開啟物件前傳送。 capabilities這個模型不會使元件上開啟,而禁止在元件上完成啟動程序及開始處理要求。這種模型可讓用戶端立即開始傳送要求,並在元件準備就緒時回應。如要進一步瞭解這個行為如何套用到能力轉送,請參閱 通訊協定開放的生命週期

總而言之,「開啟」呼叫已通過標準程式庫,以「CWD」 fdio 物件採取行動,將要求轉換為 FIDL 訊息,並透過 zx_channel_write 系統呼叫傳送至伺服器。用戶端可選擇使用 zx_object_wait_one 等待伺服器回應,或繼續以非同步方式處理。無論採用哪種方式,都會建立管道,其中一端與用戶端共同運作,另一端則傳送至「伺服器」。

檔案系統:伺服器端

分派

從管道的用戶端傳輸訊息後,訊息就會顯示在頻道的伺服器端,正在等待讀取。伺服器會依照「由管道另一端保留控制代碼的人員」進行辨識,此伺服器可能會和用戶端位於相同 (或不同) 的執行階段,並使用與用戶端相同 (或不同) 的執行階段,並以與用戶端相同的 (或不同語言) 編寫。透過使用商定的線路格式,處理序間依附元件會在管道上的細薄通訊層發生瓶頸。

日後,這個 CWD 控制代碼的伺服器端會需要讀取用戶端傳輸的訊息。這項程序並非自動執行,伺服器必須刻意等待接收控制代碼上的傳入訊息,在本例中,該控制代碼為「目前工作目錄」。開啟伺服器物件 (檔案、目錄、服務等) 時,這些控點會透過伺服器端 Zircon「通訊埠」註冊,等待其基礎控點可讀取 (表示訊息已送達) 或「關閉」 (表示訊息一律不會接收更多訊息)。這個物件稱為調度工具,將傳入的要求調派給適當的控制。它負責將傳入的訊息重新導向至回呼函式,以及某些先前提供的「iostate」代表開放連線。

如果是使用 libfs 的 C++ 檔案系統,這個回呼函式稱為 vfs_handler,會接收幾項重要資訊:

  • 由用戶端提供的 FIDL 訊息 (如果帳號代碼關閉,則由伺服器人為建構,看起來像「關閉」訊息)
  • I/O 狀態代表控點的目前連線 (以先前所述的「iostate」欄位傳遞)。

vfs_handler 可以解讀 I/O 狀態來推斷其他資訊:

  • 檔案 (如果已使用 readdir,則為目錄中的搜尋指標)
  • 用於開啟基礎資源的旗標
  • 代表基礎物件的 Vnode,可於多個用戶端或多個檔案描述元之間共用

此處理常式函式配有這項資訊,可做為大型「切換/案例」資料表,根據用戶端提供的「operation」欄位,將 FIDL 訊息重新導向至適當的函式。在未解決的情況下,系統會將 Open 序數視為作業,因此 (1) 應使用控制代碼,且 (2) 「data」欄位 (「foo」) 會解讀為路徑。

VFS 層

在 Fuchsia 中,「VFS 層」是與檔案系統無關的程式碼程式庫,可能會視情況傳送及解讀伺服器端訊息,並在基礎檔案系統中呼叫作業。值得注意的是,此層完全是選用性質,如果檔案系統伺服器不想連結至這個程式庫,就沒有義務使用。若要成為檔案系統伺服器,程序必須完全理解 FIDL 傳輸格式。因此,單一語言可以有任意數量的「VFS」實作。目前有以下實作:

  • 樹狀結構 C++ VFS:供 Fuchsia 的「main」檔案系統 minfs 和 blobf 使用。它目前是所有 VFS 實作項目最多的功能,但也可能最難使用。
  • 樹狀結構內 Rust VFS:這適用於一些 Rust 檔案系統,包括 fat32 實作。這是較新的做法,目前功能比 C++ 實作少。
  • SDK C++ VFS:SDK 使用者適用的「樹狀結構內」C++ 簡易版本。通常用於服務探索等更簡單的用途。

VFS 層定義了可轉送至基礎檔案系統的作業介面,包括:

  • 讀取/寫入 Vnode
  • 從父項 Vnode 查詢/建立/取消連結 Vnode (依名稱)
  • 依名稱重新命名/連結 Vnode
  • 其他應用程式

如要實作檔案系統 (假設開發人員想要使用共用 VFS 層),您只需要定義實作此介面的 Vnode,然後連結 VFS 層即可。如此一來,您就能輕鬆使用「路徑步行」和「檔案系統掛接」等功能,而且幾乎不需要複製程式碼。為了達成跨檔案系統通用的架構,VFS 層並未明確指出檔案系統使用的基礎儲存空間概念:檔案系統可能需要封鎖裝置、網路或僅記憶體來儲存資料,但 VFS 層只會處理在路徑、位元組陣列和向量上發生的介面。

走路

為了開啟伺服器端資源,伺服器會提供幾個起點 (以已呼叫的控點表示) 和字串路徑。這個路徑會按照「/」字元分割區隔,每個元件都會透過回呼「查詢」基礎檔案系統。如果查詢成功傳回一個 v 節點,而且偵測到另一個「/」區段,則程序會繼續執行,直到 (1) lookup 找不到元件、(2) 路徑處理到達路徑中的最後一個元件;或 (3) lookup 找到掛接點 vnode,這個節點具有附加「遠端」控制代碼的向量節點。目前,我們將忽略掛接點 v 節點,不過詳情請參閱檔案系統掛接一節。

假設 lookup 成功找到「foo」Vnode。檔案系統伺服器會繼續呼叫 VFS 介面「Open」,確認您可以在呼叫「GetHandles」存取要求的資源後,再呼叫「GetHandles」要求,在與 Vnode 互動時需要其他處理。假設用戶端同時要求使用「foo」物件 (這點與預設的 POSIX 開放式呼叫類似),則與「foo」互動所需的任何其他帳號都會封裝至小型 FIDL 說明物件並傳回至用戶端。或者,如果「foo」無法開啟,那麼系統仍會傳回 FIDL 說明物件,但「status」欄位設為錯誤代碼,表示失敗。假設「foo」開放式檔案已經成功伺服器會繼續為「foo」建立「iostate」物件,並向調派程式註冊。如此一來,伺服器就可以處理之後對「foo」的呼叫。「Foo」開啟後,用戶端現在可以傳送其他要求了。

從用戶端的角度來看,在「Open」呼叫開始時,路徑和處理組合透過 CWD 控制代碼傳輸至遠端檔案系統伺服器。由於呼叫是同步的,因此用戶端已繼續等待控點的回應。當伺服器正確找到、開啟並初始化這個檔案的 I/O 狀態後,就會傳回「成功」的 FIDL 說明物件。用戶端會讀取這個物件,識別呼叫是否成功完成。此時,用戶端可以建立一個代表「foo」控制代碼的 fdio 物件,並透過檔案描述元資料表中的項目參照該物件,然後將資料傳回給原始「開啟」函式的提供者。此外,如果用戶端想要傳送任何其他要求 (例如「讀取」或「寫入」) 至「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 |
+-------------------+