RFC-0010:zx_channel_iovec_t 支援 zx_channel_write 和 zx_channel_call | |
---|---|
狀態 | 已接受 |
區域 |
|
說明 | 這份 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
數量。
請注意,只有 bytes
陣列會變更,因此句柄陣列的類型 (zx_handle_t
或 zx_handle_disposition_t
) 並不重要。
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
。這個數字來自 8 個位元組對齊的內嵌 + 離線物件數量,這些物件可放入 65536
位元組訊息中,每個內嵌 + 離線物件都可能使用 zx_channel_iovec_t
項目。
實作
Syscall
- 引入設計部分中定義的
zx_channel_iovec_t
類型。 - 新增「
ZX_CHANNEL_WRITE_USE_IOVEC
」 - 不會變更可見的系統呼叫介面,
zx_channel_iovec_t
陣列會傳遞至現有的bytes
參數。
核心
收到 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
項目。在核心和 FIDL 編碼中,使用 64 個項目的陣列來儲存 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
呼叫中支援多個訊息。
我們考慮了三項提案:
- 平面表示法:使用兩個
uint16_t
欄位,將zx_channel_iovec_t
上的reserved
欄位重新用於其他用途:message_seq
(zx_channel_iovec_t
所屬的訊息) 和handle_count
(buffer
中位元組消耗的句柄數)。序號受限於單調性,且沒有間隔。這項限制可實現效能更高的核心,但日後可視需要降低。這種做法與這項 RFC 一致,且可在現有結構中加入多個訊息支援功能。 - 陣列的陣列表示法:有一個外部訊息陣列,每個訊息都有指向每個訊息內部 iovec 陣列的指標。這類似於 Linux 系統呼叫
sendmmsg
中使用的結構,可能更為使用者所熟悉。雖然並未測量陣列的陣列表示法效能,但有證據顯示,由於間接路徑,可能會產生 5 到 25% 的額外負擔 (請參閱 CL)。 - 標頭前置表示法:緩衝區開頭為標頭,後面接著 iovec 陣列。標頭由 16 個訊息描述符組成,每個描述符只包含一個
uint16_t
message_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 繫結變更為這種方式運作,而且延後新增對 read-path iovec 的支援,似乎也不會造成太多成本,除非有需要。
既有技術與參考資料
Fuchsia 有現有的 zx_stream_readv
和 zx_stream_writev
系統呼叫,可使用向量化 I/O。Linux 也提供類似的 readv
和 writev
系統呼叫,分別讀取及寫入檔案描述項。