| RFC-0109:快速 UDP 通訊端 | |
|---|---|
| 狀態 | 已接受 |
| 區域 |
|
| 說明 | 使用 Zircon Socket 導入網路 UDP Socket 資料傳輸。 |
| Gerrit 變更 | |
| 作者 | |
| 審查人員 | |
| 提交日期 (年-月-日) | 2021-06-17 |
| 審查日期 (年-月-日) | 2021-06-25 |
摘要
使用 Zircon Socket 實作網路資料電報 Socket 資料傳輸。使用用戶端快取的網路狀態,實作用戶端引數驗證。
提振精神
提高資料包通訊端輸送量,並降低 CPU 使用率。
在 https://fxbug.dev/42094952 之前,資料包網路插座是使用鋯石插座實作,用戶端會使用 zx_socket_write 傳送資料,並使用 zx_socket_read 接收資料。系統會使用最少的通訊協定來傳輸中繼資料,例如呼叫應用程式提供的目的地地址。
這個方法已遭棄用,因為在某些情況下,它不會向呼叫端發出錯誤信號。假設應用程式想將酬載傳送至網路堆疊沒有路徑的遠端,為了符合以 Linux 為目標的第三方軟體預期,實作項目必須傳回錯誤,指出未代表呼叫端採取任何動作。
目前的實作項目定義於 fuchsia.posix.socket/DatagramSocket.{Recv,Send}Msg。由於支援 FIDL 回應,傳送和接收資料都需要多次切換環境。忽略 FIDL 序列化:
+-----------------------------+-------------------------+
| Client | Netstack |
+-----------------------------+-------------------------+
| `zx_channel_call(...)` | |
| | |
| | `zx_channel_read(...)` |
| | `zx_channel_write(...)` |
| | |
| (`zx_channel_call` returns) | |
+-----------------------------+-------------------------+
反向 (接收資料) 的做法也差不多。與未確認的 I/O 比較:
+-------------------------+------------------------+
| Client | Netstack |
+-------------------------+------------------------+
| `zx_channel_write(...)` | |
+-------------------------+------------------------+
| `zx_channel_write(...)` | `zx_channel_read(...)` |
+-------------------------+------------------------+
雖然可以使用 Zircon 管道和 FIDL 進行未確認的 I/O,但這不會提供背壓,且可能導致記憶體用量暴增。因此,我們建議使用 zircon sockets:
+------------------------+-----------------------+
| Client | Netstack |
+------------------------+-----------------------+
| `zx_socket_write(...)` | |
+------------------------+-----------------------+
| `zx_socket_write(...)` | `zx_socket_read(...)` |
+------------------------+-----------------------+
設計
中繼資料驗證
使用未確認的 I/O 預先假設資料包通訊端中繼資料 (例如目的地地址) 可完全在本機上驗證,而不需要與網路互動,且這項驗證的結果可快取在與通訊端的多次互動中 (因此可攤提驗證成本)。
請注意,這項假設不適用於 IPPROTO_ICMP 插座,因為系統會檢查其酬載是否有效,因此現有的 FIDL 架構通訊協定會保留並用於效能不重要且需要深入驗證的情況。
從現有類型中擷取 FIDL 以供重複使用,並重新命名 DatagramSocket,以利清楚瞭解:
protocol BaseDatagramSocket {
compose BaseSocket;
/// Retrieves creation information from the socket.
GetInfo() -> (Domain domain, DatagramSocketProtocol proto) error fuchsia.posix.Errno;
}
protocol SynchronousDatagramSocket {
compose BaseDatagramSocket;
/// Receives a message from the socket.
RecvMsg(bool want_addr, uint32 data_len, bool want_control, RecvMsgFlags flags) -> (fuchsia.net.SocketAddress? addr, bytes data, RecvControlData control, uint32 truncated) error fuchsia.posix.Errno;
/// Sends a message on the socket.
SendMsg(fuchsia.net.SocketAddress? addr, bytes:MAX data, SendControlData control, SendMsgFlags flags) -> (int64 len) error fuchsia.posix.Errno;
}
使用驗證函式定義新的 FIDL 通訊協定:
/// Matches the definition in //zircon/system/public/zircon/types.h.
const uint32 ZX_WAIT_MANY_MAX_ITEMS = 64;
/// Describes an intent to send data.
type SendMsgArguments = table {
/// The destination address.
///
/// If absent, interpreted as the method receiver's connected address and
/// causes the connected address to be returned in [`SendMsgBoardingPass.to`].
///
/// Required if the method receiver is not connected.
1: to fuchsia.net.SocketAddress;
}
/// Describes a granted approval to send data.
type SendMsgBoardingPass = resource table {
/// The validated destination address.
///
/// Present only in response to an unset
/// [`SendMsgArguments.to`].
1: to fuchsia.net.SocketAddress;
/// Represents the validity of this structure.
///
/// The structure is invalid if any of the elements' peer is closed.
/// Datagrams sent to the associated destination after invalidation will be
/// silently dropped.
2: validity vector<zx.handle:<EVENTPAIR, zx.RIGHTS_BASIC>>:ZX_WAIT_MANY_MAX_ITEMS;
/// The maximum datagram size that can be sent.
///
/// Datagrams exceeding this will be silently dropped.
3: maximum_size uint32;
}
protocol DatagramSocket {
compose BaseDatagramSocket;
/// Validates that data can be sent.
///
/// + request `args` the requested disposition of data to be sent.
/// - response `pass` the constraints sent data must satisfy.
/// * error the error code indicating the reason for validation failure.
SendMsgPreflight(SendMsgArguments args) -> (SendMsgBoardingPass pass) error fuchsia.posix.Errno;
};
定義要在 Zircon 插座上傳送的 FIDL 結構:
/// A datagram received on a network socket, along with its metadata.
type RecvMsgPayload = table {
1: from fuchsia.net.SocketAddress;
2: control RecvControlData;
3: datagram bytes:MAX;
};
/// A datagram to be sent on a network socket, along with its metadata.
type SendMsgPayload = table {
1: args SendMsgArguments;
2: control SendControlData;
3: datagram bytes:MAX;
};
嘗試傳送資料的用戶端會遵循下列 (水平壓縮) 圖表所示的程序:
+--------------------------------+ +---------------------------+ +----------------+
| cache.getSendMsgBoardingPass() | - Present -> | checkPeerClosed(validity) | +- ZX_OK -> | Return success |
+--------------------------------+ +---------------------------+ | +----------------+
| ^ ^ | | |
| | | | | +------------------------------+
| +------+ +----------------------------------+ | | | socket.write(SendMsgPayload) | - != ZX_OK -----+
| | Send | +- Success -> | cache.storeSendMsgBoardingPass() | | | +------------------------------+ |
| +------+ | +----------------------------------+ | | ^ |
| +--------------------+ | | | |
+- Absent -> | SendMsgPreflight() | +- (ZX_OK, ZX_SIGNAL_PEER_CLOSED) -+ +- (ZX_ERR_TIMED_OUT) -+ | |
+--------------------+ | | +- No -+ |
| ^ | +-----------------------------------+ | | |
| | +-> | cache.removeSendMsgBoardingPass() | | +---------------------+ |
| | +-----------------------------------+ +-> | size > maximum_size | |
| | | +---------------------+ |
| | | +--------------+ | |
| +----------+ | | <-------------------------------------- Yes -+ |
| | Return error | |
+- Failure ------------------> | | <---------------------------------------------------------------+
+--------------+
用戶端的「實作」cacheSendMsgPreflight;這大致上是從 fuchsia.net.SocketAddress 到 (vector<zx::eventpair>, maximum_size) 的對應。
請注意,這項策略形成的快取最終會保持一致;無效化可能會交錯進行用戶端的有效性檢查,以及酬載抵達網路堆疊的程序。對於傳送語意為盡量傳送的資料包通訊端,這是可接受的行為。
保證同步行為
背景
在 POSIX 語意下,用戶端會預期系統提供同步 send
和 recv 行為,以處理其他用戶端啟動的傳入/傳出封包變更。具體來說,客戶希望這些變更適用於「所有」尚未收到的酬載,以及「任何」已傳送的酬載。
在目前的實作項目中,由於下列原因,系統保證會同步執行作業:
- 網路堆疊負責處理所有相關封包。
- 用戶端會透過同步 FIDL 呼叫傳送及接收酬載。
- 用戶端會設定通訊端選項,要求變更封包處理作業,這些選項也會以同步 FIDL 呼叫的形式實作。
以透過 Zircon Socket 傳輸的未確認 I/O 取代同步 FIDL 呼叫,會破壞這些語意。以下說明由此產生的問題和解決方法。
傳送路徑
問題
在「傳送路徑」中,用戶端會設定通訊端選項,修改酬載的處理方式。由於 setsockopt 是透過同步 FIDL 實作,因此選項的新值可能會套用至在呼叫前排入佇列的封包。例如:
- 用戶端將
IP_TOS設為某個值 A,並同步更新網路堆疊中的狀態。 - 用戶端呼叫
sendto,將酬載排入 Zircon 插座佇列。 - 用戶端將
IP_TOS設為某個值 B。 - 網路堆疊會將酬載出列,並透過
IP_TOS=B傳送至網路。
解決方案
修改用於設定與傳送路徑相關的插座選項的 FIDL 處理常式,直到 Zircon 插座由網路堆疊的取消佇列 goroutine 排空為止。接著,處理常式會修改相關狀態,並傳回給用戶端。由於在呼叫 setsockopt 之前傳送的所有酬載都已從 Zircon 插座取消佇列到網路堆疊,因此不會以新設定處理任何酬載。
Recv Path
問題
在「Recv Path」中,用戶端可以設定通訊端選項,要求提供酬載及其傳送方式的輔助資料控制訊息。再次強調,由於 setsockopt 是同步,因此有偏差空間。例如:
- 網路堆疊會將酬載排入 Zircon Socket 的佇列。
- 用戶端會設定
IP_RECVTOS。 - 用戶端會將酬載從佇列中移除,並傳回給使用者,但不會傳回
IP_TOS控制訊息。
解決方案
在網路堆疊中,將每個酬載與衍生任何支援的控制訊息所需的最小狀態一起加入佇列。
定義 FIDL 方法,以擷取目前的一組要求控制訊息:
protocol DatagramSocket {
compose BaseDatagramSocket;
/// Returns the set of requested control messages.
///
/// - response `cmsg_set` the set of currently requested control messages.
RecvMsgPostflight() -> (struct {
cmsg_set @generated_name("RequestedCmsgSet") table {
/// Represents the validity of this structure.
///
/// The structure is invalid if the peer is closed.
1: validity zx.handle:<EVENTPAIR, zx.RIGHTS_BASIC>;
/// Identifies whether the `IP_RECVTOS` control message is requested.
2: ip_recvtos bool;
}
});
};
在用戶端中,快取目前要求的控制訊息集,並按照下列程序,使用該訊息集透過通訊端篩選 netstack 傳遞的控制訊息狀態:
+-----------------------------------------+
| socket.read() -> (Payload, RecvMsgMeta) | -----------> ZX_OK
+-----------------------------------------+ |
| |
| +-----------------------------+ |
| | cache.getRequestedCmsgSet() | <---+
| +-----------------------------+
| | |
| | |
| | |
| Absent <--------------------+ +-----------------------------> Present
| | |
| | +-----------------------+ |
| | | Return Payload, cmsgs | +---------------------------+ |
| | +-----------------------+ | checkPeerClosed(validity) |<-----+
| | ^ +---------------------------+
| | | | | ^
| | | | | |
| | (ZX_ERR_TIMED_OUT) <-----------+ | |
| | | |
| | | |
| | +--(ZX_OK, ZX_SIGNAL_PEER_CLOSED) <-----+ |
| | | |
| | | |
| | | +--------------------------------+ |
| | +---> | cache.removeRequestedCmsgSet() | |
| | +--------------------------------+ |
| | | |
| | | |
| | +---------------------+ | |
| +--> | RecvMsgPostflight() | <---+ |
| +---------------------+ |
| | | |
| | | +------------------------------+
| +-------Failure <--+ +--> Success --> | cache.storeRequestedCmsgSet()|
| | +------------------------------+
| |
| |
| | +--------------+
| +--> | |
| | Return error |
+-----> | |
+--------------+
將控制訊息狀態新增至每個酬載時,將記憶體複製到 Zircon 插座和從 Zircon 插座複製記憶體時,會增加負荷。我們認為這項取捨是可以接受的,原因有二:
- 我們可能會支援的控制訊息大小約為 100 位元組。 假設 MTU 為約 1500 個位元組,則副本負擔會低於 10%。
- 絕大多數的控制訊息狀態都是「每個封包」,也就是說,網路堆疊會將其與每個封包一起保留在記憶體中,並在封包排入通訊端佇列後釋放該記憶體。因此,系統的整體記憶體用量不應增加。
此外,我們也會使用微型基準來追蹤相關指標,並在每次新增控制訊息時執行。如果結果顯示不再值得進行取捨,我們可以還原為慢速路徑 (無論如何,我們都需要保留慢速路徑,才能支援 ICMP 資料包)。
序列化通訊協定
透過鋯石插座執行 I/O 最簡單的方法,就是定義單一 FIDL 表格來保存 UDP 酬載及其中繼資料,並使用 FIDL 靜態序列化。這個方法的缺點是會強制傳送者和接收者將酬載和中繼資料序列化到暫時緩衝區,確保至少有一份副本。
由於向量化通訊端讀取和寫入功能可能很快就會推出,因此建議您建構可利用向量化 API 避免複製的通訊協定。
通訊協定
定義 send 和 recv 中繼資料的 FIDL 資料表:
/// Metadata used when receiving a datagram payload.
type RecvMsgMeta = table {
1: fuchsia.net.SocketAddress from;
2: RecvControlData control;
};
/// Metadata used when sending a datagram payload.
type SendMsgMeta = table {
1: SendMsgArguments args;
2: SendControlData control;
};
在 Describe 中傳回給用戶端的 fuchsia.io/NodeInfo 中,指定用於接收含有序列化中繼資料表的位元組的緩衝區大小:
type NodeInfo = strict resource union {
// Other variants omitted.
9: datagram_socket resource struct {
/// See [`fuchsia.posix.socket.DatagramSocket`] for details.
socket zx.handle:<SOCKET, zx.rights.TRANSFER | zx.RIGHTS_IO | zx.rights.WAIT | zx.rights.INSPECT>;
/// Size of the buffer used to receive Tx metadata.
tx_meta_buf_size uint64;
/// Size of the buffer used to receive Rx metadata.
rx_meta_buf_size uint64;
};
};
允許:
tx_meta_bytes = fidl_at_rest::serialize(SendMsgMeta)
tx_meta_size = len(tx_meta_bytes)
傳送酬載時,用戶端會建構下列緩衝區並加入佇列:
( 2 ) (tx_meta_size) (tx_meta_buf_size - tx_meta_size)
+--------------+-----------------+----------------------------------+---------+
| tx_meta_size | tx_meta_bytes | Padding | Payload |
+--------------+-----------------+----------------------------------+---------+
網路堆疊:
- 分配緩衝區,其中包含
2 + tx_meta_buf_size + max_payload_size個位元組的空間,並將訊息出列至該緩衝區。 - 將前兩個位元組解讀為
uint16,以位元組為單位,識別保存序列化中繼資料的緩衝區區域大小。 - 使用 靜態 FIDL 還原序列化
SendMsgMeta。
接收路徑的運作方式完全對稱。
uint32_t tx_meta_buf_size = fidl::MaxSizeInChannel<fuchsia_posix_socket::wire::SendMsgMeta, fidl::MessageDirection::kSending>();
這會計算傳送方向的訊息大小上限,前提是沒有不明欄位。Netstack 可以安全地假設 RecvMsgMeta 中的所有欄位都是已知的,因為網路堆疊本身會序列化該訊息。因為:SendMsgMeta
- 用戶端 ABI 修訂版本會限制用戶端序列化資料表中的所有欄位。
- 如果平台不支援用戶端的 ABI 修訂版本,就會拒絕執行用戶端。
- Netstack 一律會與平台一起建構。現有系統一直默默依賴這項假設。這裡我們明確指出這一點。
實作
將新實作項目新增至 fuchsia.io/NodeInfo:
resource union NodeInfo {
/// The connection composes [`fuchsia.posix.socket/DatagramSocket`].
N: DatagramSocket datagram_socket;
};
/// A [`NodeInfo`] variant.
// TODO(https://fxbug.dev/42154392): replace with an anonymous struct inline.
resource struct DatagramSocket {
zx.handle:<SOCKET, zx.RIGHTS_BASIC | zx.RIGHTS_IO> socket;
};
將 fuchsia.posix.socket/Provider.DatagramSocket 的傳回類型變更為變數:
/// Contains a datagram socket implementation.
resource union DatagramSocketImpl {
1: DatagramSocket datagram_socket;
2: SynchronousDatagramSocket synchronous_datagram_socket;
}
...並變更行為,以便在引數允許時傳回 DatagramSocket (即呼叫端未要求 ICMP Socket)。
初步實作時,預計每個 SendMsgBoardingPass.validity 都會提供兩個元素:
- 代表路由表的已知狀態;由所有通訊端共用,且在路由表有任何變更時失效。
- 代表特定通訊端的已知狀態;通訊端發生任何可能改變通訊端行為的變更時,就會失效,例如呼叫
bind、connect、setsockopt(..., SO_BROADCAST, ...)、setsockopt(..., SO_BINDTODEVICE, ...)等。
效能
SOCK_DGRAM 插座的輸送量預計會增加約一倍;這項預估值是根據 https://fxbug.dev/42094952 之後的效能回歸情形而得。
CPU 使用率預計會大幅降低,但降幅不明。
人體工學
這項變更不會對人體工學造成重大影響,因為下游使用者不會直接使用這裡顯示的介面。
回溯相容性
一開始請先保留 ABI 相容性,讓 fuchsia.posix.socket/Provider.DatagramSocket 保持不變,並以 DatagramSocket2 形式實作新功能。完成必要的 ABI 轉換後,請將 DatagramSocket2 重新命名為 DatagramSocket,並移除先前的實作項目。在另一個 ABI 轉換後,移除 DatagramSocket2。
安全性考量
這項提案不會影響安全性。
隱私權注意事項
這項提案不會影響隱私權。
測試
現有的單元測試涵蓋受影響的功能。
說明文件
除了這裡顯示的 FIDL 文件註解外,不需要任何文件。
缺點、替代方案和未知事項
本提案會在使用者空間中建構機制,以解決動機問題。另一種可能性是在核心中建構這項機制。
以下是將其轉換為核心的草圖:
- 對於每個
zx::socket端點,核心都會維護從SocketAdddres到max_size的對應。 - 我們會新增一些
zx_socket_add_route/zx_socket_remove_route系統呼叫,在對等端點上修改該地圖。 - 我們會新增一些
zx_socket_send_to/zx_socket_receive_from系統呼叫,這些呼叫會耗用/提供位址。
如果使用者空間呼叫 zx_socket_send_to 時使用的位址不在對應中,作業就會失敗,且使用者空間必須傳送同步訊息至 Netstack,要求將該路徑新增至 zx::socket。如果該要求失敗,位址作業也會失敗並顯示錯誤。
優點
在核心方法中,傳送 UDP 封包 (在快速案例中) 是單一系統呼叫 (zx_socket_send_to),而不是兩個系統呼叫 (zx_object_wait_many、zx_socket_write)。
由於使用者空間方法可能經過最佳化,這可能不是專業版。我們發現一律會 zx_object_wait_many,因此可最佳化作業,在以原子作業維護必要狀態的情況下,讓作業不必進行系統呼叫即可完成工作。time::infinite_past這可能也需要將控制代碼表放在 vDSO 中,但這可能不是事實,也可能無法實現。
對於有執行階段的用戶,替代方案是使用 zx_object_wait_async 而不是 wait_many 來維護本機快取,讓快速路徑避開額外的系統呼叫。
此外,由於訊息有效負載會併入系統呼叫,因此可直接將有效負載複製到最終目的地,我們也避免依附於靜態 FIDL 和 FIDL 固有的額外資料副本。
缺點
在核心方法中,路由表變更時,沒有明顯的方法可以執行 O(1) 路由取消作業。如前所述,我們可以在 zx_socket_remove_route 中新增標記來移除所有路徑 (這可能也是您想要的),但 netstack 必須在每個通訊端上發出 zx_socket_remove_route。
我們可以非常花俏,讓 zx_socket_add_route 取得取消的 eventpair,但這有點巴洛克。
將這些結構體烘焙到核心中,也會在向平台導入另一個通訊協定演進模型時,造成高昂的成本;現在特定 FIDL 型別和系統呼叫之間的耦合會更加緊密,這不會自動保持同步。
後續作業
使用 eventpair 來指出用戶端快取是否有效,會在 send 和 recv 路徑上產生額外的系統呼叫。改用 VMO 來表示有效性,即可消除這項系統呼叫。在這種設計中,事件配對會以 VMO 的偏移量取代,並對應至用戶端的位址空間。隨後,用戶端就能透過簡單讀取 VMO 的方式檢查有效性。
不明
此外,還有關於如何處理 SendControlData 的問題。這可能需要做為 zx_socket_send_to 的額外參數,或是運算中的旗標。