| RFC-0010:支援 zx_channel_write 和 zx_channel_call 的 zx_channel_iovec_t | |
|---|---|
| 狀態 | 已接受 |
| 區域 |
|
| 說明 | 這項 RFC 為 zx_channel_write 和 zx_channel_call 導入新模式,可從多個記憶體區域複製輸入資料,而非單一連續緩衝區。 |
| 問題 | |
| Gerrit 變更 | |
| 作者 | |
| 審查人員 | |
| 提交日期 (年-月-日) | 2020-09-25 |
| 審查日期 (年-月-日) | 2020-10-06 |
摘要
這項 RFC 為 zx_channel_write、zx_channel_write_etc、zx_channel_call 和 zx_channel_call_etc 導入新模式,可從多個記憶體區域複製輸入資料,而非單一連續緩衝區。這樣一來,訊息資料就能直接從多個使用者空間物件複製,不需經過中間的分配、複製和版面配置步驟,進而提升特定使用者 / 用戶端的效能。方法是更新現有的系統呼叫,在指定選項時採用 zx_channel_iovec_t 記憶體區域描述元陣列。
提振精神
這項提案的主要動機是提升成效。
對於非線性化的網域物件,FIDL 繫結目前需要 (1) 分配緩衝區,以及 (2) 以標準版面配置將物件複製到緩衝區。完成這些步驟後,緩衝區會再次複製到核心。zx_channel_iovec_t 可讓物件直接複製到核心。此外,FIDL 訊息資料不再需要以標準順序排列,只有 zx_channel_iovec_t 陣列必須反映所需順序。
設計
zx_channel_write 目前的簽章如下:
zx_status_t zx_channel_write(zx_handle_t handle,
uint32_t options,
const void* bytes,
uint32_t num_bytes,
const zx_handle_t* handles,
uint32_t num_handles);
輸入資料是由 bytes 指向的連續位元組陣列。
在 zx_channel_write_etc、zx_channel_call 和 zx_channel_call_etc 中,有類似的陣列。這些陣列必須是連續的,因此會產生額外負荷。具體來說,對於含有行外元件的 FIDL 訊息,FIDL 編碼器必須分配緩衝區並將資料重新放置其中,這可能相當耗費資源。
zx_channel_iovec_t 提供替代路徑。zx_channel_write、
zx_channel_write_etc、zx_channel_call 和 zx_channel_call_etc
會改為接收物件的位置和大小清單,
並在核心內進行複製,避免額外重複和分配。
zx_channel_iovec_t 在 C++ 中定義如下:
typedef struct zx_channel_iovec {
void* buffer; // User-space bytes.
uint32_t capacity; // Number of bytes.
uint32_t reserved; // Reserved.
} zx_channel_iovec_t;
每個 zx_channel_iovec_t 都指向要從 buffer 複製到核心訊息緩衝區的下一個 capacity 位元組。reserved 必須指派為零。
只有在 capacity 為 0 時,buffer 欄位才能為 NULL。buffer 指標可能在多個 zx_channel_iovec_t 中重複。
zx_channel_write、zx_channel_write_etc、zx_channel_call 或 zx_channel_call_etc 的簽章維持不變。不過,如果使用者為這些系統呼叫指定 ZX_CHANNEL_WRITE_USE_IOVEC 選項,void* bytes 引數就會解讀為 zx_channel_iovec_t*。同樣地,num_bytes 引數會解讀為陣列中的 zx_channel_iovec_t 數量。
請注意,控制代碼陣列 (zx_handle_t 或 zx_handle_disposition_t) 的型別無關緊要,因為只有 bytes 陣列會變更。
zx_channel_iovec_t 陣列所描述的訊息會連同所有部分一併傳送,否則訊息不會傳送。無論成功或失敗,呼叫端都無法再使用提供給系統呼叫的控制代碼。
錯誤狀況
這些是 zx_channel_write、zx_channel_write_etc、zx_channel_call 和 zx_channel_call_etc 的錯誤情況,由於導入 iovec,因此需要更新。
ZX_ERR_OUT_OF_RANGE num_bytes 或 num_handles 分別大於 ZX_CHANNEL_MAX_MSG_BYTES 或 ZX_CHANNEL_MAX_MSG_HANDLES。如果指定 ZX_CHANNEL_WRITE_USE_IOVEC 選項,當 num_bytes 大於 ZX_CHANNEL_MAX_MSG_IOVEC,或 iovec 容量總和超過 ZX_CHANNEL_MAX_MSG_BYTES 時,就會產生 ZX_ERR_OUT_OF_RANGE。
ZX_ERR_INVALID_ARGS bytes 是無效指標、handles 是無效指標,或 options 包含無效選項位元。如果指定了 ZX_CHANNEL_WRITE_USE_IOVEC 選項,且 buffer 欄位含有無效指標,則為 ZX_ERR_INVALID_ARGS。
對齊
zx_channel_iovec_t 中指定的位元組沒有對齊限制。每個 zx_channel_iovec_t 都會複製,且不含邊框間距。
限制
每則訊息的位元組數 (65536) 和控制代碼數 (64) 現行限制維持不變。請注意,這些限制適用於訊息,而非 zx_channel_iovec_t 項目。
每個系統呼叫的 zx_channel_iovec_t 數量上限為 8192。這個數字來自於可放入 65536 位元組訊息的 8 位元組對齊內嵌 + 離線物件數量,每個內嵌 + 離線物件可能會使用 zx_channel_iovec_t 項目。
實作
Syscall
- 介紹設計部分定義的
zx_channel_iovec_t型別。 - 新增「
ZX_CHANNEL_WRITE_USE_IOVEC」 - 可見的系統呼叫介面沒有任何變更,
zx_channel_iovec_t陣列會傳遞至現有的bytes參數。
Kernel
收到 ZX_CHANNEL_WRITE_USE_IOVEC 選項後,核心會執行下列動作:
- 將
zx_channel_iovec_t物件指向的資料複製到訊息緩衝區。雖然複製作業通常也會按照zx_channel_iovec_t輸入的順序執行,但這並非必要。不過,最終訊息必須按照zx_channel_iovec_t項目順序排列。 - 撰寫傳送給管道訊息。
FIDL
這是系統呼叫變更提案,實作作業會在核心中進行,而 FIDL 繫結變更的具體細節不在範圍內。不過,為了評估這項提案,請務必瞭解 FIDL 編碼的影響。
FIDL 繫結可選擇利用 zx_channel_iovec_t 支援功能,方法是新增將 FIDL 物件編碼為 zx_channel_iovec_t 陣列的支援功能。
這個編碼路徑與現有編碼路徑的主要差異在於,zx_channel_iovec_t 可讓核心就地複製物件。這項做法的主要問題在於指標。FIDL 編碼的訊息必須傳送至核心,且指標須替換為 PRESENT 或 ABSENT 標記值。不過,在許多情況下,系統呼叫後,物件仍需保留原始指標值,才能呼叫解構函式。
也就是說,利用 zx_channel_iovec_t 的繫結有時需要進行額外的記帳作業,確保物件正確清除。
遷移
這項功能預設為停用,因此現有使用者不會立即受到影響。視需要,呼叫網站可以遷移至使用這個選項。
實務上,目的是要遷移可從 zx_channel_iovec_t 受益的 FIDL 繫結,並使用該繫結。預期對 FIDL 使用者造成的影響不大。
效能
原型已實作並完成基準化。
這個原型在核心端實作了 zx_channel_write 選項,
並限制 FIDL 支援 (僅限內嵌物件和向量)。
郵件標頭,以及每個內嵌和非內嵌物件,各有 zx_channel_iovec_t 項目。我們使用 64 個項目的陣列,在核心和 FIDL 編碼中儲存 zx_channel_iovec_t。
這些測量結果來自搭載 Intel Core i5-7300U CPU @ 2.60GHz 的電腦。
位元組向量事件基準 (zx_channel_write、zx_channel_wait_async 和 zx_channel_read) 顯著改善:
- 4096 位元組向量:9398 奈秒 -> 4495 奈秒
- 256 位元組向量:8788 奈秒 -> 3794 奈秒
FIDL 編碼也顯示效能有所提升。
編碼位元組向量範例的時間:
- 4096 位元組向量:345 奈秒 -> 88 奈秒
- 256 位元組向量:251 奈秒 -> 86 奈秒
內嵌物件的編碼作業也有小幅改善:
- 具有 256 個 uint8 欄位的結構:67 奈秒 -> 49 奈秒
安全性考量
由於這是對現有系統呼叫的重大變更,因此實作前需要進行安全性審查。
隱私權注意事項
這項異動不會影響隱私權。
測試
系統會為每個變更的層級新增單元和整合測試。
不打算新增任何裝置或系統層級的端對端測試,但現有的測試涵蓋範圍有助於確保遷移後不會出現非預期的錯誤。
說明文件
系統呼叫說明文件必須更新,以指出支援這項功能。
不需要變更任何架構相關說明文件。
缺點、替代方案和未知事項
這項提案的主要缺點是需要支援核心中的選項,以及使用 ZX_CHANNEL_WRITE_USE_IOVEC 選項的 FIDL 繫結,因此會增加複雜度,需要確保物件在就地複製後能正確清除。
限制
有論點認為 zx_channel_iovec_t 的數量下限應較低,可能更接近 16 而非 8192。這樣一來,zx_channel_iovec_t 陣列就能複製到核心的堆疊。不過,這樣做會導致實作策略無法為每個超出範圍的 FIDL 物件指派一個項目。zx_channel_iovec_t
在實務上,如果 zx_channel_iovec_t 數量龐大,或至少避免將工作轉移至核心,在使用者空間中線性化可能會更有效率。不過,為求簡單,建議您先使用 8192 限制,之後再視需要進一步調整。
上限提高後,實作層面的影響是 zx_channel_iovec_t 陣列無法完全放在核心堆疊上。一般情況下可以使用堆疊緩衝區,但如果項目數量夠多,就必須複製到較大 (且較慢) 的緩衝區。
向量化帳號代碼
可以為控制代碼提供 zx_channel_iovec_t 的對等項目,或將控制代碼與現有 zx_channel_iovec_t 中的位元組一併納入。不過,由於控制代碼陣列通常很小,因此控制代碼的優點較為有限。為求簡單,控點會保留在專屬陣列中。
支援在單一寫入作業中傳送多則訊息
先前版本的 RFC 包含在單一 zx_channel_write 呼叫中支援多個訊息的提案。
我們考慮了三項提案:
- 平面表示法:將
zx_channel_iovec_t上的zx_channel_iovec_t欄位重新用於zx_channel_iovec_t,並使用兩個uint16_t欄位:message_seq(訊息所屬的zx_channel_iovec_t) 和handle_count(buffer中位元組使用的控制代碼數量)。序號必須單調遞增,且不得有間隙。reserved這項限制可提升核心實作的效能,但日後如有需要,可以放寬這項限制。這種做法符合 RFC 規範,且可將多訊息支援功能新增至現有結構。 - 陣列的陣列表示法:訊息的外層陣列,每個訊息都有指標,指向每個訊息的 iovec 內層陣列。這與 Linux 系統呼叫
sendmmsg中使用的結構類似,使用者可能較為熟悉。雖然我們並未測量陣列陣列表示法的效能,但有證據顯示,間接性可能會造成 5% 到 25% 的負擔 (請參閱 CL)。 - 以標頭為前置字元的表示法:緩衝區開頭為標頭,後接 iovec 陣列。標頭包含 16 個訊息描述元,每個描述元只包含
uint16_tmessage_size欄位。這個欄位會決定與訊息相關聯的zx_channel_iovec_t項目數量。這種表示法提供階層式結構,但不需要額外的重新導向和複製作業。
在設計討論中,由於平面呈現方式的效能屬性和簡潔性,因此獲得青睞。雖然多訊息支援的完整提案超出本 RFC 的範圍,但請注意,本 RFC 與平面表示法相容。
專用於 iovec 的系統呼叫
與其在現有系統呼叫中新增選項,不如建立 zx_channel_write_iovec、zx_channel_write_etc_iovec、zx_channel_call_iovec 和 zx_channel_call_etc_iovec 系統呼叫。不過,建議您選擇一個選項,以免系統呼叫次數暴增,增加使用者的認知負荷。
zx_channel_read 中的 zx_channel_iovec_t 支援
這項 RFC 建議支援管道寫入的 zx_channel_iovec_t,但不支援管道讀取。這是因為寫入端使用 iovec 的動機很明確 (避免 FIDL 線性化步驟),但讀取端沒有明確的立即好處。
Rust 繫結可能會受益於讀取端 iovec 支援,方法是將緩衝區分割成多個較小的緩衝區,每個緩衝區都有自己的擁有權。這會簡化類似 LLCPP 的繫結變體,基本上是將緩衝區轉換為輸出物件。不過,目前沒有短期計畫要變更 Rust 繫結,以支援這種做法,而且延後新增對讀取路徑 iovec 的支援,似乎不會造成太多成本。
既有技術和參考資料
Fuchsia 具有現有的 zx_stream_readv 和 zx_stream_writev 系統呼叫,可使用向量化 IO。Linux 也提供類似的 readv 和 writev 系統呼叫,分別用於讀取和寫入檔案描述元。