RFC-0196:FIDL 大型郵件

RFC-0196:FIDL 大型郵件
狀態已接受
區域
  • FIDL
說明

支援透過 FIDL 通訊協定傳送大型訊息。

問題
  • 100478 號
變更
  • 695625
作者
審查人員
提交日期 (年/月)2022-06-28
審查日期 (年/月)2022-10-27

摘要

目前,FIDL 語言會限制在以 Zircon 為基礎的傳輸方式 (例如通道與通訊端) 至 64KiB 上的訊息大小。本文件建議針對處理任意的大型訊息,建議採用高階設計,即使訊息溢出訊息位元組上限也超出基礎傳輸限制。為此,您需要將一項與完善現有模式 (使用 fuchsia.mem.Data) 類似的解決方案,升級為一流的 FIDL 語言支援機制。

提振精神

透過線傳送的所有 FIDL 訊息現在都會限制為 ZX_CHANNEL_MAX_MSG_BYTES 的大小,目前等同於 64KiB,且衍生自透過 Zircon 管道傳送的訊息大小上限。超過此限制的訊息無法編碼。

目前的常見解決方案是,透過 zx.handle:VMO (或 fuchsia.mem.Data) 將任意大型資料以 blob 形式 (或 fuchsia.mem.Data) 傳送,而基礎 VMO 本身包含傳送的資料 blob。這些 blob 通常包含使用者想要代表的結構化資料,並以 FIDL 進行編碼/解碼,但無法且強制執行手動投放。fuchsia.git 中的這些包裝函式類型目前有很多用途。

除了無法支援大型郵件以外,還會導致一些問題。其中最主要的原因是,通訊協定很少需要傳送大型訊息,但在技術上具備這類錯誤,造成的大量錯誤。例如,網址非常龐大,或是掃描 Wi-Fi 時產生的大量網路清單。需要採用 :MAX 大小 vectorstring 的每個 API 都會容易受到這個問題影響,例如 table 版面配置很少填入所有欄位。一般來說,任何需要以訊息格式不接受 64KiB 以下的訊息格式,都可能會受到這個故障模式的影響。

透過 VMO 傳送不具類型的資料 blob 並不符合人體工學,因為系統會遺失所有類型資訊,您必須在接收端手動重新建構。系統不會利用 FIDL 描述資料形狀並抽離編碼->傳送->解碼管道舉例來說,ProviderInfo API 的子項類型 InspectConfigInspectSource 目前分別由 fuchsia.mem.Bufferzx.handle:VMO 表示,但代表 FIDL 可描述及處理的結構化資料。

使用 zx.handle:VMOfuchsia.mem.Data 時,系統會強制只有資料專用的 FIDL 類型納入 resource 修飾符。這會對繫結 API 產生下游效果,導致 Rust 等語言的產生的類型無法在合理情況下衍生 Clone 特徵。

因為支援空間不足的大型訊息支援造成的錯誤和人體工學問題十分廣泛。在本 RFC 草擬過程中進行的問卷調查顯示,我們發現過去至少 30 個案例,指出更可靠的大型訊息支援能為 FIDL 使用者提供協助。

相關人員

講師:hjfreyer@google.com

審查人員:abarth@google.com、chcl@google.com、cpu@google.com、mseaborn@google.com、nickcano@google.com、surajmalhotra@google.com

諮詢:bprosnitz@google.com、geb@google.com、hjfreyer@google.com、jmatt@google.com、tombergan@google.com、yifeit@google.com

社交化:五個團隊 (元件解析器、DNS 解析器、驅動程式庫開發、WLAN SME 和 WLAN 政策) 都利用這個設計審查原型:

  • 元件解析器:geb@google.com
  • DNS 解析器:dhobsd@google.com
  • 駕駛開發:dgilhooley@google.com
  • WLAN 政策:nmccren@google.com
  • WLAN 主題專家:chcl@google.com

此外,我們也訪問了超過 30 名現有的 fuchsia.mem.Datafuchsia.mem.Buffer 類型使用者,希望針對設計意見回饋及用途合適度進行採訪。

設計

本文件中的重要字詞「必須」、「不得」、「必要」、「應」、「不應」、「應該」、「不應該」、「建議」、「可能」和「選用」均採用 IETF RFC 2119 說明。

訊息溢位是一種傳輸層級的問題。構成大型訊息的要素,以及處理符合該定義的訊息的最佳方式,在 Zircon 管道、驅動程式庫程式架構和超網路等之間存在極大差異。這表示呼叫特定方法要求或回應「large」並不是抽象的陳述式,因此「必須」一律為該方法本身的通訊協定定義「large」定義。

以下內容會指定訊息宣告方式:「我根據伴隨的傳輸預計,因此需要特別處理」。針對實際溢位的任何指定訊息執行個體,在 *.fidl 檔案的介面定義時間內,這項宣告「必須」易於辨識。

具體而言:在這項設計之前,傳送者可以傳送原本完全有效的訊息,而這些訊息超過基礎傳輸的最大訊息位元組上限,因此造成意外且難以偵錯的 PEER_CLOSED 執行階段失敗。完成這些變更後,fidlc 編譯器會靜態檢查酬載類型是否可能超過傳輸的訊息位元組上限,如果是的話,就會產生特殊的「溢位」處理程式碼來因應這種情況。這個模式會啟用大型訊息的次要執行階段訊息傳送機制,而不受限的側邊管道 (若為向量頻道,即 VMO) 會用來儲存訊息內容。這個新的訊息傳送路徑完全新增至所產生繫結程式碼的「引數」,以便維持 API 和 ABI 相容性。FIDL 方法實作器現在可以確定不會因為達到任意位元組大小限製而觸發 PEER_CLOSED

傳輸格式變更

新的位元稱為 byte_overflow 標記,會新增至 FIDL 交易訊息標頭動態旗標部分。這個標記在翻轉時,表示目前保留的訊息只包含訊息的控制層,而訊息的其餘部分會儲存在可能的獨立緩衝區中。

這個獨立緩衝區的位置及緩衝區的存取方式取決於傳輸。如果 byte_overflow 旗標處於有效狀態,傳輸中的控制層訊息「必須」包含 16 位元組交易訊息標頭,後面接著額外的 16 位元組附錄訊息,說明大型訊息的大小。這表示這則訊息「必須」剛好 32 個位元組:預設的 FIDL 訊息標頭,後面接著稱為的 message_info 結構,其中包含三項資料:uint32 用於標記,uint32 為可能在排除溢位緩衝區時指定訊息附加的控制數量,而 uint64 則指出 VMO 本身中的資料大小:

type MessageInfo = struct {
  // Flags pertaining to large message transport and decoding, to be used for
  // the future evolution and migration of this feature.
  // As of this RFC, this field must be zeroed out.
  flags uint32;
  // A reserved field, to be potentially used for storing the handle count in
  // the future.
  // As of this RFC, this field must be zeroed out.
  reserved uint32;
  // The size of the encoded FIDL message in the VMO.
  // Must be a multiple of FIDL alignment.
  msg_byte_count uint64;
};

由於需要產生一個額外控制代碼以指向溢位緩衝區,因此大型 FIDL 訊息可能只能附加 63 個控制代碼,而非一般的 64。這種行為不合理,且會讓使用者感到意外,而且只會透過執行階段錯誤回報。這個不幸的角案例,是因為致力於開發核心改善功能,以期在未來修正銳利邊緣。

byte_overflow 旗標「必須」在動態旗標位元陣列中佔用最大位元 #6 (即最重大的位元)。位元 #5 會保留給日後可能使用的 handle_overflow 位元,但這個位元目前並未使用。這個位元不得用於其他用途。

執行階段要求

有許多條件在解碼期間違反,必須導致 FIDL 傳輸錯誤,並立即關閉通訊管道。如果設定了 byte_overflow 旗標,控制層訊息的大小「必須」為 32 個位元組 (如上所述);訊息內文「必須」透過其他媒介傳送。

在 Zircon 管道傳輸的情況下,位元組溢位緩衝區的媒介必須為 VMO。這表示控制層訊息中隨附的控點數量「必須」至少一個。由該最後一個控點指出的核心物件必須為 VMO,且接收器從該 VMO 讀取的位元組數必須等於 message_info 結構的 msg_byte_count 欄位值。如果已知訊息設有限制,此值「必須」小於或等於相關酬載的靜態結構大小上限。

訊息傳送者「必須」透過 zx_vmo_create 系統呼叫發出新的 VMO,接著在後面加上 zx_vmo_write,以填入訊息內文。他們「必須」確保代表溢位 VMO 的控制代碼沒有正確的 ZX_RIGHT_WRITE

在接收端,訊息的收件者「必須」使用 zx_vmo_read 讀取其保存的資料。因此,透過 zircon 管道傳送的一般 FIDL 訊息只需要兩個系統呼叫 (傳送者為 zx_channel_write_etc,接收者的 zx_channel_read_etc),而位元組溢位訊息需要更多 (傳送者的 zx_channel_write_etczx_vmo_createzx_vmo_write,以及接收者的 zx_channel_read_etczx_vmo_readzx_handle_close)。這會造成極大的懲罰,但日後的最佳化作業 (例如改善 zx_channel_write_etc API) 可能就要獲得部分費用。訊息接收器不得嘗試寫入收到的溢位 VMO。

程式碼產生變更

FIDL 繫結實作作業必須針對位元組數上限可能大於通訊協定傳輸限制的任何酬載訊息產生溢位處理常式。為此,FIDL 訊息可以大致分為三個類別:

  • 受限: 一律知道累計位元組數上限的訊息。 這個類別包含大部分的 FIDL 訊息。如果是這類訊息,繫結產生器「必須」使用訊息計算出的位元組數上限,判斷是否要在編碼時設定 byte_overflow 標記,以及是否要在解碼時檢查標記。具體來說,如果累計位元組數上限超過通訊協定傳輸限制所允許的上限 (在 zircon 通道中則為 64KiB),則可在編碼時設定 byte_overflow 標記,並檢查產生的程式碼「必須」,否則這些都「不得」
  • 半界限: 累計位元組數上限的訊息只會在編碼時得知。這個類別包含任何會受限制的訊息,但遞移包含 flexible uniontable 定義。如果是這類訊息,繫結產生器「必須」使用訊息計算出的位元組數上限,判斷是否要在編碼時設定 byte_overflow 標記,但「必須」一律在解碼時檢查這個標記。
  • 無界限: 累積位元組數上限的訊息一律無法解析。這個類別包括任何會轉換納入遞迴定義或不受限 vector 的訊息。對於這類訊息,產生的繫結程式碼「必須」一律具備在編碼上設定 byte_overflow 旗標的功能,且「必須」一律在解碼時進行檢查。
@transport("Channel")
protocol Foo {
  // This request has a well-known maximum size at both encode and decode time
  // that is not larger than 64KiB limit for its containing transport. The
  // generated code MUST NOT have the ability to set the `byte_overflow` on
  // encode, and MUST NOT check it on decode.
  BoundedStandard() -> (struct {
    v vector<string:256>:16; // Max msg size = 16+(256*16) = 4112 bytes
  });
  BoundedStandardWithError() -> (struct {
    v vector<string:256>:16; // Max msg size = 16+16+(256*16) = 4128 bytes
  }) error uint32;

  // This request has a well-known maximum size at both encode and decode time
  // that is greater than the 64KiB limit for its containing transport. The
  // generated code MUST have the ability to set the `byte_overflow` on encode,
  // and MUST check it on decode.
  BoundedLarge() -> (struct {
    v vector<string:256>:256; // Max msg size = 16+(256*256) = 65552 bytes
  });
  BoundedLargeWithError() -> (struct {
    v vector<string:256>:256; // Max msg size = 16+16+(256*256) = 65568 bytes
  }) error uint32;

  // This response's maximum size is only statically knowable at encode time -
  // during decode, it may contain arbitrarily large unknown data. Because it
  // is not larger than 64KiB at encode time, the generated code MUST NOT have
  // the ability to set the `byte_overflow` on encode, but MUST check for it on
  // decode.
  SemiBoundedStandard(struct {}) -> (table {
    v vector<string:256>:16; // Max encode size = 32+(256*16) = 4128 bytes
  });
  SemiBoundedStandardWithError() -> (table {
    v vector<string:256>:16; // Max encode size = 16+32+(256*16) = 4144 bytes
  }) error uint32;

  // This response's maximum size is only statically knowable at encode time -
  // during decode, it may contain arbitrarily large unknown data. Because it
  // is larger than 64KiB at encode time, the generated code MUST have the
  // ability to set the `byte_overflow` on encode, and MUST check for it on
  // decode.
  SemiBoundedLarge(struct {}) -> (table {
    v vector<string:256>:256; // Max encode size = 32+(256*256) = 65568 bytes
  });
  SemiBoundedLargeWithError(struct {}) -> (table {
    v vector<string:256>:256; // Max encode size = 16+32+(256*256) = 65584 bytes
  }) error uint32;

  // This event's maximum size is unbounded. Therefore, the generated code MUST
  // have the ability to set the `byte_overflow` on encode, and MUST check for
  // it on decode.
  -> Unbounded (struct {
    v vector<string:256>;
  });
};

ABI 和 API 相容性

這個設計在全面推出後,就能完全與 ABI 和 API 相容。它一律具有 ABI 安全性,因為任何變更會將先前繫結的酬載轉換為不受限或半邊界的酬載 (例如將 struct 變更為 table,或是變更 vector 大小邊界),因此已經是 ABI 破壞性變更。

對於不受限或半繫結的酬載,無論大小為何,系統一律會在訊息解碼期間檢查 byte_overflow 旗標。這表示任何可在連線結尾進行編碼的訊息,在另一端進行編碼,即使演變新增了從解碼器所呈現的酬載類型檢視畫面,導致訊息出現異常大的情況也一樣。

在發布期間的過渡期中,其中一方可能對於連線具有瞭解大型訊息的 FIDL 繫結,而另一方可能知道大型訊息,則大型訊息無法解碼。這與目前的情況類似,在這些情況下,這類訊息在編碼期間會失敗,但失敗的時間會稍微遠離來源。

由於大多數會傳送大型訊息的 API 已採用通訊協定層級的因應措施 (例如區塊區塊),因此在中繼發布期間發生解碼失敗的風險應視為低。主要風險向量是,如果通訊協定開始透過現有方法傳送現在允許使用的大型訊息。這類通訊協定改為導入支援大型訊息的新型方法。

設計原則

本設計遵循幾項重要原則。

用多少付多少

FIDL 的主要設計原則是,用多少付多少。本文件所述的大型訊息功能,旨在維持這個理想。

採用受限酬載的方法轉譯後,此 RFC 存在的效能會下降。方法如果採用半繫結或無界限的酬載,但不會傳送超過通訊協定傳輸的依據數量限制的訊息,只需在接收端支付單次位元旗標檢查的費用。只有實際使用大型訊息溢位緩衝區的訊息,才會有效能影響。

如果使用者不需要大量訊息支援 (也就是大多數可能會在 FIDL 中表示的方法/通訊協定),就不用支付任何費用,因為執行階段效能成本以及編寫 FIDL API 時產生的心理開銷。

無遷移

現在起,任何可能使用這類酬載的酬載都會啟用大型訊息,因此不必遷移現有的 FIDL API 或其用戶端/伺服器實作項目。之前會導致 PEER_CLOSED 執行階段錯誤的情況現在已經「正常運作」。

載客量身訂作

此設計可彈性調整,因應現有和推測的不同傳輸需求。舉例來說,只要 byte_overflow 位元已翻轉,且傳輸作業知道如何排序包含封包的溢位,就可以透過網路使用多封包訊息等慣例。

實作

這項功能將在實驗性 fidlc 標記後方推出。接著系統會修改每個繫結後端,以針對特別指定實驗性標記的輸入處理這個 RFC 指定的大型訊息。一旦功能處於穩定狀態,系統就會移除旗標,以便一般使用。

這項屬性不應需要額外的 fidlc 支援,因為只會將執行溢位檢查所需的資訊傳遞至選取後端,將學習如何支援大型訊息。

在此 RFC 之前,繫結全面會將編碼/解碼緩衝區放入堆疊。日後,建議您,針對沒有翻轉 byte_overflow 標記的訊息,繫結「應」繼續發生這種行為。如果是包含這類訊息的訊息,請改為「應」在堆積上分配繫結。

效能

您可以在略為自訂的情境中使用核心微基準測試來估算建議提供方式的效能影響;加總和比較兩種情況:傳送單一尺寸 B 的管道訊息與傳送 16 位元組頻道訊息的總和、傳送大小為 B - 16 的 VMO、KiB6、KiB6、KiB6、KiB6、KiB6、32KiB、

清單 1:以 16 位元組通道訊息傳送 B 位元組的資料,以及 B 至 16 大小的 VMO 訊息 (而非 B 大小的管道訊息) 傳送 B 位元組的資料時,預估的運送時間效能「稅金」。

訊息大小 / 策略 僅限頻道 管道 + VMO VMO 使用稅
16KiB 2.5 微秒 5.9 微秒 136%
32KiB 4.5 微秒 7.7 微秒 71%
64KiB 7.9 微秒 13 微秒 65%
128 公里 16.5 微秒 23.3 微秒 41%
256KiB 35.8 微秒 54.4 微秒 52%
512KiB 71.3 微秒 107.4 微秒 51%
1024KiB 157.0 微秒 223.4 微秒 42%
2048KiB 536.2 微秒 631.8 微秒 18%
4096KiB 1328.2 微秒 1461.8 微秒 10%

清單 2:以 16 位元組通道訊息的形式提供 B 位元組的資料,以及為 B 至 16 的 VMO 訊息 (而非大小 B 的管道訊息) 提供 B 個位元組時,預估送達時間效能的「稅金」圖表。

僅限傳統管道與管道 + VMO 的比較圖表

清單 3:比較從 16 位元組管道訊息與 B 至 16 大小 (而非作為 B 大小的管道訊息) 提供 B 位元組的資料到的傳送時間效能的線性比例比較。

不同酬載大小的 VMO 用量懲處圖表

這些資料會產生一些有趣的觀察。可以看到資料大小與資料傳送時間的關聯近乎線性。這兩種方法在效能上會有明顯差距,但隨著訊息大小增加,兩者間的效能差距似乎更小。

結合這些結果後,我們就能使用本設計中指定的方法模擬傳送 FIDL 大型訊息的預期效能。我們預期使用相同大小的純舊管道訊息 (我們允許) 時,指定大小的「VMO 稅金」大約能增加 20-60% 的端對端傳送時間。有趣的是,當傳送的訊息大小增加時,百分之百的差距會稍微降低,表示 VMO 稅金在酬載大小方面稍微增加,並以子線性的方式成長。

清單 4:顯示根據本文所述的設計模型推送時間效能的資料表。

訊息大小 / 策略 僅限頻道 訊息 + VMO
16KiB 2.5 微秒 --
32KiB 4.5 微秒 --
64KiB 7.9 微秒 13 微秒
128 公里 -- 23.3 微秒
256KiB -- 54.4 微秒
512KiB -- 107.4 微秒
11024KiB -- 223.4 微秒
2048KiB -- 631.8 微秒
4096KiB -- 1461.8 微秒

清單 5:線性比例圖顯示本文所描述設計的模型推送時間效能。請注意,64KiB 的中斷性會從一般訊息切換為大型訊息。

模擬效能線性圖

人體工學

此變更構成了人體工學的重大改善,基本上,現在 zx.handle:VMOfuchsia.mem.Bufferfuchsia.mem.Data 的所有用途都可以改用一流的 FIDL 概念來描述。下游繫結程式碼的優點也包括一般須透過一般的 FIDL 路徑處理原本必須透過線路傳送的資料。基本上,針對大型訊息產生的 FIDL API 現在與非大型訊息對應的項目相同。

回溯相容性

這些變更將完全回溯相容。現有 API 的語意有所改變 (從每訊息 64KiB 的邊界限製到無界限),但由於這是先前的限制的鬆散,因此現有的 API 不會受到影響。

安全性考量

這些變更對安全性的影響不大。我們已使用 fuchsia.mem.Data 結構提升進入「最高類別」狀態的模式,此模式不會對安全性造成任何負面影響。不過,仍要確保實作作業在所有情況下都能安全無虞。

這項設計也可提高與 FIDL 通訊協定相關的阻斷服務風險。在此之前,發送 VMO 配置大量記憶體的攻擊向量,只有明確傳送 fuchsia.mem.Data/fuchsia.mem.Buffer/zx.handle:VMO 包含類型的通訊協定才能使用接收器。現在,凡是包含至少一個方法且具有未繫結或半繫結酬載的通訊協定,都會暴露在這類風險中。目前由於 zircon 中有許多阻斷服務向量,因此判定為可容許。更全面的解決方案可以解決這個問題,一旦超出這套設計的限制範圍,

這項設計不會強制在接收端檢查 ZX_INFO_VMO,進而引進額外的阻斷服務向量。如此一來,受呼叫功能的 VMO 就不會因為承諾提供的網頁而停止運作。意外發生的風險將評定為低,因為只有少數程式使用呼叫器支援的 VMO 機制。與上述原因類似,這種阻斷服務向量受到容忍,直到日後設計時可以導入更全面的解決方案為止。

隱私權注意事項

一項重要的隱私權考量是訊息傳送者「必須」確保每個以 VMO 為基礎的訊息都使用新建立的 VMO。它們「不得」重複使用訊息之間的 VMO,否則會有資料外洩風險。您必須設定繫結,才能強制執行這些限制。

測試

我們將擴充 fidlc 的單元測試以及下游和繫結輸出的標準 FIDL 測試策略,以配合大型訊息使用情境。

說明文件

您必須更新 FIDL 傳輸格式規格,以說明本文件導入的傳輸格式變更。

缺點、替代方案和未知

缺點

這種設計有許多缺點。儘管這些標準看似較低,尤其是在完全不付任何執行,或未實作考慮的替代方案時,它們仍值得指出。

效能驟降

如同 效能探索中的描述,這個 RFC 中描述的策略會在 ZX_CHANNEL_MAX_MSG_BYTES 裁切點,在使用者開始支付較大的訊息時支付「稅金」。具體來說,如果訊息大小超過 64KiB,所得的處理時間會比 64KiB 高出約 60% (13μs 不是 7.9μs)。儘管這樣的懸崖大致上沒問題,但它相對較小,而且未來可透過日後的核心變更進行融化。

阻斷服務

現在,只要通訊協定至少採用一種方法,且該方法採用未受限或半繫結酬載,就可能發生記憶體遭拒的問題:惡意攻擊者可以在 message_info 結構的 msg_byte_count 欄位中,傳送含有極大值的溢位訊息,並附加了相當大的 VMO。接著,系統會強制要求接收器為這個酬載分配足夠的記憶體處理量,在惡意酬載夠大時無法避免停止運作。

如上方列舉的安全性考量所述,這是非常真實的風險,在日後提供更完善的解決方案前,這項設計會著重於解決問題。

處理溢位極端情況

這項設計無法完全避免因訊息超出預期規模而在執行階段發生 PEER_CLOSED 意外的情況:如果一般訊息有超過 64 個控點,或是含有超過 63 個代碼的大型訊息,仍會觸發錯誤狀態。目前可以視為可接受此做法,因為作者知道實務上並未使用這類酬載。此時可視需要處理這個極端案例。在 message_info 結構中加入 reserved 欄位,可確保日後在設計時能夠靈活地處理溢位支援。

與情境相關的訊息屬性

byte_overflow 和旗標是第一個代表不同傳輸方式不同的標頭標記 (靜態標記除外,則可能的不必要標記除外,視我們是否將「靜態 FIDL」視為「傳輸」而定)。這會造成一些歧義:只是查看採用有線編碼的 FIDL 交易訊息,卻不知道傳送哪種傳輸訊息可能再足以處理訊息。現在需要「預先處理」步驟,視標頭標記和傳送訊息的傳輸而定,我們會執行特殊程序來組合完整的訊息內容。例如,在 zircon 管道傳輸中溢位的非處理常式訊息現在會在其控點陣列中取得控制,而 fdf 訊息則不會溢位。

遭拒的替代方案

設計這個 RFC 時,考慮到許多替代解決方案。下面列出最有趣的提案。

提高 Zircon 訊息大小限制

大型訊息最需要的是,zx.channel 傳輸是目前訊息大小上限為 64KiB。這裡顯而易見的解決方案就是 直接提高上限

這並非理想的情況,原因如下。其一,這項功能只是將問題排除在途中。情況相當複雜,因為 ABI 破壞核心對遷移作業來說並不容易,因為必須謹慎管理,確保在提高限制之後編譯的二進位檔不會意外傳送超過編譯量才能處理的資料量。

許多 FIDL 實作也會在限制範圍內做出有用的假設。部分繫結 (例如 Rust 的繫結) 會採用「猜測和檢查」分配策略來接收收到的訊息。該程式庫會分配小型緩衝區,並嘗試 zx_channel_read_etc。如果這個 Sys 呼叫因 ZX_ERR_BUFFER_TOO_SMALL 而失敗,會傳回實際訊息大小。如此一來,繫結即可分配適當大型的緩衝區並重試。

其他繫結 (例如 C++ 的繫結) 只會擲回風力,一律為傳入的訊息分配 64KiB,避免可能因配置較大而發生多次系統呼叫。後者策略不會擴充為任意大型訊息。

最後,使用 VMO 是經過充分測試的解決方案:過去數年來,透過 fuchsia.mem.Datafuchsia.mem.Buffer 類型傳輸大型訊息都是首選。不過,提高核心限制是較不合格的解決方案,具有更多潛在的未知數。

zx_vmar_map 取代 zx_vmo_read

對現有設計進行最佳化調整後,FIDL 編碼器就會使用 zx_vmar_map 直接從 VMO 緩衝區讀取資料。這種方法有兩個問題。

主要問題在於這個方法並不安全,而是必須修改核心基本功能才能解決問題。這裡的問題是,對應記憶體會造成訊息傳送者在接收者在讀取期間修改訊息內容,因而產生 TOCTTOU 風險。讀取器可以嘗試直接讀取對應且可能可變動的 VMO 資料,而不必先複製,但即使系統執行防禦性副本,也難以安全進行讀取。如果透過 zx_vmo_create_child 呼叫強制實行 VMO 不變性,則可能會降低系統呼叫的不變性和最嚴重的複製作業負擔,藉此降低這類安全性風險。

其他關於記憶體對應效能的問題,以及有線 C++ 繫結的小工具 (例如決定記憶體可釋放的時機),都可能會使這個選項不適合使用。

封包化

這裡的概念是擷取非常大型的訊息,並將訊息分割為許多訊息,每個區塊的大小不超過 64KiB,然後在另一端組合。交易訊息標頭可能會有某種接續標記,以指出是否預期「更多資料」會持續追蹤。這不僅具有內建資料流控制的特性,而且對於從在標準程式庫中使用串流基本功能的語言來說,程式設計人員也十分熟悉。

這種方法的缺點是,如果訊息不容易分割,這個方法就沒有太大幫助。這也非常複雜:傳送多個執行緒時,部分訊息區塊的監聽陣列會完成傳輸,需要在另一端重新組合傳輸作業。

最擔心的,這項策略會導致阻斷服務風險,日後無法單憑新的核心基本功能或日後新增的受限通訊協定修正此風險:惡意或錯誤的用戶端可能會傳送非常長的訊息封包串流,但是卻無法傳送「關閉」封包。接收方在等待最後一個封包時,會強制將記憶體中的所有剩餘的封包保留,讓用戶端能夠「預訂」伺服器上可能無限無限的永久記憶體配置。課程可能有一些方法,例如逾時和政策限制,但這很快就會轉為透過 FIDL 重新實作 TCP。

顯式溢位

本文件提議的設計簡化了向使用者傳送大型訊息的詳細步驟。使用者只需定義酬載,繫結也會在背景執行其他工作。

替代設計可讓使用者透過宣告的方式,指定 VMO 的使用時機,例如按酬載或按酬載成員指定層級。基本上,這只需要修改 FIDL 語言,為 fuchsia.mem.Data 提供更精確的拼字。

現有的設計會取出改良後的效能和 API 相容性,但這對於較不確定性和精細的控制是值得的交換。

僅允許值類型

此設計的早期版本建議只為值類型啟用 overflowing。這裡的理由很簡單:現有用途全都不是資源類型,而且單一 FIDL 訊息不太可能一次傳送超過 64 個處理代碼,因此會被視為低優先順序。

fuchsia.component.resolution 程式庫這個解決方案的原型過程中,我們發現瞭解決方法。某些方法已經使用資料表來裝載酬載,且不會取代整個方法,而是希望將資料表逐步淘汰使用 fuchsia.mem.Data。具體做法:

// Instead of adding a new method to support large messages, the preferred
// solution is to extend the existing table and keep the current method.
protocol Resolver {
  Resolve(struct {
    component_url string:MAX_COMPONENT_URL_LENGTH;
  }) -> (resource struct {
    component Component;
  }) error ResolverError;
};

type Component = resource table {
  // Existing fields - note the two uses of `fuchsia.mem.Data`.
  1: url string:MAX_COMPONENT_URL_LENGTH;
  2: decl fuchsia.mem.Data;
  3: package Package;
  4: config_values fuchsia.mem.Data;
  5: resolution_context Context;

  // Proposed additions for large message support.
  6: decl_new fuchsia.component.decl.Component;
  7: config_values_new fuchsia.component.config.ValuesData;
};

這些方法會產生一個有趣的問題:即使酬載較大,且其處理控點在實務上也完全獨佔,但對 fidlc 編譯器而言,這並不陌生。從層面來看,這些只是資源類型雖然您可能會想用某些難度來教導編譯器的這個特定案例,但在開發更合適的核心基本功能之前,我們認為允許 63 處理大型訊息是比較簡單的做法。

overflowing 修飾符

這項設計的早期疊代作業可讓使用者在定義通訊協定方法的 FIDL 中設定 overflowing 值區,如下所示:

// Enumerates buckets for maximum zx.channel message sizes, in bytes.
type msg_size = flexible enum : uint64 {
  @default
  KB64 = 65536;    // 2^16
  KB256 = 262144;  // 2^18
  MB1 = 1048576;   // 2^20
  MB4 = 4194304;   // 2^22
  MB16 = 16777216; // 2^24
};

@transport("Channel")
protocol Foo {
  // Requests up to 1MiB are allowed, responses must still be less than or equal
  // to 64KiB.
  Bar overflowing (BarRequest):zx.msg_size.MB1
      -> (BarResponse) error uint32;
};

使用者最終認為這個做法太複雜又微妙,因為就算允許多個選項,也沒有明確指引合適的選擇。無論如何,大部分使用者都只希望回答簡單的是/否問題 (「我需要大型訊息支援?」),而不是擔心特定限制的每分鐘效能影響。

另一個替代方案還考慮使用單一的 overflowing 關鍵字 (不含值區),明確告知使用者接受效能可能較低的 API。最終的效能差距最終決定不夠大,在任何情況下都能縮小到足以對語言本身呈現的這類呼叫。

未來的預期工作

隨附工作在啟用大型訊息功能時並非絕對必要的,但對於達成此目標而言,有許多重要的補充和最佳化方式是不可或缺的。

核心異動

核心變更可能會有一些變化,雖然在實作這項功能的重要路徑上,並不會對減少系統呼叫造成的衝擊和效能最佳化。向使用者顯示的大型訊息 API 是實作這些額外最佳化功能時,「不應」變更。fuchsia.mem.Data 的大多數現有使用者都不具特別的延遲時間 (否則他們不會使用 fuchsia.mem.Data!),因此修改核心的主要功用是改善在 FIDL 中啟用大型訊息後彈出的新興用途的效能。

一流的訊息串

每當出現大型訊息用途時,就不得再問一個問題:「在 FIDL 中實作第一類串流無法解決這項問題嗎?」要回答任何特定情況時,有兩個有用的屬性能夠將大量資料進行分類:「區塊性」和「適用性」

區塊性是指相關資料是否可拆分為實用的子部分,更重要的是,如果資料接收者只能對子集執行工作。基本上,這就是指 T 類型資料類型與 vector<T>array<T> 類型資料之間的差異,因為清單的某個部分仍能做為行動的依據。分頁清單是可分段處理的清單,而傳送要排序的項目清單則不是。同理,樹狀圖並非可區塊,不是因為樹狀結構的 (任意部分) 部分能處理的種類太多。

附加性問題:資料在傳送後是否能修改。 附加 API 的經典範例是 Unix 管道:雖然從讀取端讀取資料,但甚至可能 (甚至預期) 從寫入結尾加入更多。可在傳送後加入的資料。傳送時不可變更的資料,即使在清單形式中也是如此。

清單 6:偏好的大型資料處理策略矩陣,適用於所有可能的區塊性與可附加性組合。

大型資料處理策略矩陣

這兩個差異很有用,因為將這兩個差異結合成矩陣,有助於您瞭解哪些大型訊息或串流更適合。

對靜態 blob (例如資料轉儲、B-tree 或大型 JSON 字串) 而言,使用者不需要串流:只傳送至單一訊息,且對 FIDL 而言是 (或至少) 過大,表示意外的複雜性通常相當令人擔心。在這種情況下,他們希望有一種方法指示系統「無論要接收所有訊息,但要傳輸所有資訊的線路要有的不合理大小」。資料具體化到裝置位置的記憶體後,在流程之間移動資料就不太合理。

針對可區塊化的動態資料結構,例如網路封包串流,串流是顯而易見的選項 (就在名稱中!)。使用者已經建立自訂疊代器來處理這個案例,編寫可處理傳送端設定串流的程式庫,並在接收端中以乾淨的方式公開,這似乎是適合採用一流處理的理想選擇。此外,這是非常自然的模式,在大多數語言中都具有強大的支援機制,並且有熟悉的程式設計師熟悉 FIDL 在 (C++、Dart、Rust) 中擁有繫結。

如果是可分批傳送,大部分為靜態的訊息 (例如列出連線至裝置週邊裝置的快照),呢?要將這些訊息區塊化為串流,其實非常容易,但並不顯然很有用:某些 API 會公開這類資訊,讓作者認為分頁是含有這些資訊的分頁,而非核心功能。在此情況下,串流或大型訊息是比較適合的選項,因此內容似乎非常背景資訊。

重點在於:大型訊息只是可能透過線取得大量資料的工具箱中的一種工具。FIDL 極有可能在日後採用一流的串流實作方式,這項實作能夠補強,而不是取代大型訊息提供的功能。

繫結通訊協定和彈性信封大小限制

這項設計在阻斷服務的風險極大,其中某些通訊協定可能會想避免,尤其是以其他不同用戶端共用的通訊協定。為此,我們可以假設在通訊協定中加入 bounded 修飾符,藉此強制執行編譯時間,讓所有方法都只使用受限的類型:

// Please note that this syntax is very speculative!
bounded protocol SafeFromMemoryDoS {
  // The payload is bounded, so this method compiles.
  MySafeMethod(resource struct {
    a bool;
    b uint64;
    c array<float32, 4>;
    d string:32;
    e vector<zx.handle, 64>;
  });
};

這項設計的其中一項結果是,FIDL 通訊協定作者必須選擇「bifurcating」選項:加入 bounded 後,即因為訊息大小不受限,導致通訊協定無法阻斷服務,但會阻止該方法的酬載使用 tableflexible union 類型傳遞。這可不幸的是,因為改進性和 ABI 相容性都是 FIDL 語言的核心目標。我們強制使用者採用 ABI 穩定版,藉此大幅限制日後使用者改進酬載的功能。

其中一種可能的缺點是,為 flexible 加入的版面配置導入明確的大小限制。這可提供 ABI 相容性,因為彈性定義會隨時間改變,但仍會對類型的最大大小強制執行嚴格的 ABI 中斷限制:

// Please note that this syntax is very speculative!
@available(added=1)
type SizeLimitedTable = resource table {
  1: orig vector<zx.handle>:100;
  // Version 2 still compiles, as it contains <=4096 bytes AND <=1024 handles.
  @available(added=2)
  2: still_ok string:3000;
  // Version 3 fails to compile, as its maximum size is greater than 4096 bytes.
  @available(added=2)
  3: causes_compile_error string:1000;
}:<4096, 1024>; // Table MUST contain <=4096 bytes AND <=1024 handles.

這種大小限制提供「柔軟」的彈性:酬載依然可以隨著時間變更,但首次定義酬載時,就難以 (ABI 中斷) 限制的成長範圍。

先前的圖片和參考資料

本提案的前例是 fuchsia.mem.Data,之前在 fuchsia.git 程式碼集中則廣泛使用及支援 fuchsia.mem.Bufferzx.handle:VMO。這種決策基本上是此模式經過完整測試的「一流」進化。

先前「遭捨棄」的 RFC 描述有點類似,同樣使用 VMO 做為大型訊息的基礎傳輸機制。

附錄 A:fuchsia.git 的 FIDL 酬載邊界

下表顯示截至 2022 年 8 月初為止,在 fuchsia.git 程式碼集中,溢位 (大於 64KiB) 與標準酬載之間的界限分佈情形。資料便是建構 fuchsia.git 的「一切」版本之後收集而來。接著,系統會透過一系列的 jq 查詢分析結果的 JSON IR。

清單 7:表格顯示 fuchsia.git 存放區中每個酬載受限程度和大小的測量頻率。

限制性 / 郵件種類 標準 溢位 總計
有限制 3851 (76%) 45 (%) 3896 (77%)
半邊界 530 (10%) 70 號 (1%) 600 (11%)
無界限 0 (0%) 602 (12%) 602 (12%)
總計 4381 (86%) 717 (14%) 5098 (100%)

  1. 您可能會想比較潛在封包解決方案的效能,例如將 64KiB 訊息的傳送時間乘以 64 來比較 4MiB 訊息的效能,但這並不正確。對執行效能比較的機器的處理器快取影響,可確保傳輸 <=1MiB 作業執行速度比依序傳輸超過 1 MiB 時更快;如清單 2 所示,圖表 2 的「notch」中可見證。這種基準化方法只有在大小相同的訊息之間直接啟用有效的比較項目。