RFC-0109:快速 UDP 通訊端口 | |
---|---|
狀態 | 已接受 |
區域 |
|
說明 | 使用 zircon 通訊端連線埠實作網路 UDP 通訊端連線埠資料傳輸。 |
Gerrit 變更 | |
作者 | |
審查人員 | |
提交日期 (年-月-日) | 2021-06-17 |
審查日期 (年-月-日) | 2021-06-25 |
摘要
使用 zircon 通訊端連線埠實作網路資料包通訊端連線埠資料傳輸。使用用戶端快取的網路狀態,實作用戶端引數驗證。
提振精神
提高資料包通訊端點的總處理量,並降低 CPU 使用率。
在 https://fxbug.dev/42094952 之前,資料包網路通訊埠是使用 zircon 通訊埠實作;用戶端會使用 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 ------------------> | | <---------------------------------------------------------------+
+--------------+
用戶端的 cache
會「實作」SendMsgPreflight
,大致上是從 fuchsia.net.SocketAddress
到 (vector<zx::eventpair>, maximum_size)
的對應表。
請注意,這項策略所建立的快取最終會一致;無效化可能會交錯客戶端的有效性檢查,以及抵達網路堆疊的酬載。這適用於傳送意義為盡力而為的資料包通訊端。
保證同步行為
背景
在 POSIX 語意下,用戶端會預期系統會針對用戶端對傳入/傳出封包處理作業所做的其他變更,提供同步 send
和 recv
行為。具體來說,用戶端會預期這些變更會套用至所有尚未收到的酬載,且不會套用至已傳送的酬載。
在目前的實作項目中,系統會保證同步行為,原因如下:
- 網路堆疊負責處理所有相關封包。
- 用戶端會透過同步 FIDL 呼叫傳送及接收酬載。
- 用戶端會設定通訊端口選項,要求變更封包處理作業,這些選項也會以同步 FIDL 呼叫的方式實作。
透過 zircon 套接字傳輸,將同步 FIDL 呼叫替換為未確認的 I/O,會破壞這些語意。以下說明產生的問題和解決方法。
傳送路徑
問題
在傳送路徑中,用戶端會設定套接字選項,修改酬載的處理方式。由於 setsockopt
是透過同步 FIDL 實作,因此選項的新值可能會套用至在呼叫之前排入佇列的封包。例如:
- 用戶端將
IP_TOS
設為某個值 A,同步更新網路堆疊中的狀態。 - 用戶端會呼叫
sendto
,在 zircon 網路介面中排入酬載。 - 用戶端將
IP_TOS
設為某個值 B。 - 網路堆疊會將酬載從佇列中移除,並透過
IP_TOS=B
將其傳送至網路。
解決方案
修正用於設定與傳送路徑相關的 Socket 選項的 FIDL 處理常式,以便在網路堆疊的 dequeuing goroutine 將 zircon 套接字耗盡之前,將傳送路徑封鎖。接著,處理程序會修改相關狀態並傳回至用戶端。由於在呼叫 setsockopt
之前傳送的所有酬載都已從 zircon 套接字中移出佇列,並進入網路堆疊,因此不會使用新設定處理。
Recv 路徑
問題
在Recv Path 上,用戶端可以要求控制訊息,藉由設定套接字選項,提供關於酬載及其傳送方式的輔助資料。再次強調,由於 setsockopt
是同步的,因此可能會出現偏差。例如:
- 網路堆疊將酬載排入 Zircon 套接字。
- 用戶端設定
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 傳遞的控制訊息狀態:
+-----------------------------------------+
| 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 通訊端時增加負荷。我們認為這是可接受的取捨,原因有二:
- 我們可能支援的控制訊息可容納約 100 個位元組。假設 MTU 約為 1500 個位元組,這項複製作業的額外負擔率低於 10%。
- 大多數的控制訊息狀態都是「每個封包」,這表示網路堆疊會將其與每個封包一同保留在記憶體中,並在封包排入套接字後釋放該記憶體。因此,系統的整體記憶體用量不應增加。
此外,我們會使用微型基準測試追蹤相關指標,並在新增控制訊息時執行。如果結果顯示不再值得做出權衡,我們可以恢復使用較慢的路徑 (我們還是需要保留這條路徑,以便支援 ICMP 資料包)。
序列化通訊協定
透過 zircon 通訊端執行 I/O 最簡單的方法,就是定義單一 Fidl 資料表來保存 UDP 酬載及其中繼資料,並使用 FIDL at rest 將其序列化。這個方法的缺點是,它會強制傳送端和接收端將酬載和中繼資料序列化為暫存緩衝區,確保至少有一個副本。
由於向量化 Socket 讀取和寫入功能可能很快就會推出,建議您建構可利用向量化 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 at rest 將
SendMsgMeta
進行反序列化。
接收路徑的運作方式完全對稱。
uint32_t tx_meta_buf_size = fidl::MaxSizeInChannel<fuchsia_posix_socket::wire::SendMsgMeta, fidl::MessageDirection::kSending>();
該函式會在傳送方向上計算訊息大小的邊界,前提是沒有不明欄位。網路堆疊可安全地假設 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;
}
...並變更行為,以便在引數允許的情況下 (即呼叫端未要求 ICMP 通訊端) 傳回 DatagramSocket
。
初始實作預期會在每個 SendMsgBoardingPass.validity
中提供兩個元素:
- 代表路由表的已知狀態,由所有通訊端分享,並在路由表有任何變更時失效。
- 代表特定 Socket 的已知狀態;如果 Socket 發生任何可能變更 Socket 行為的變更 (例如呼叫
bind
、connect
、setsockopt(..., SO_BROADCAST, ...)
、setsockopt(..., SO_BINDTODEVICE, ...)
等),就會失效。
成效
SOCK_DGRAM
通訊端點的傳輸量預計會增加一倍;這項預估值是根據 https://fxbug.dev/42094952 後所觀察到的效能回歸。
CPU 使用率預計會減少,但幅度不明。
人體工學
這項變更不會對人體工學造成重大影響,因為下游使用者不會直接使用這裡顯示的介面。
回溯相容性
請一開始保留 fuchsia.posix.socket/Provider.DatagramSocket
不變,並以 DatagramSocket2
的形式實作新功能,以便保留 ABI 相容性。在進行必要的 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
)。
這可能不是專業人士,因為使用者空間方法可能會進行最佳化。我們發現自己一律會使用 time::infinite_past
進行 zx_object_wait_many
,因此只要使用原子作業來維持必要狀態,即可在不進行系統呼叫的情況下,對作業進行最佳化,以便執行作業。這可能需要將句柄表也放在 vDSO 中,但實際上可能並非如此,也可能無法這麼做。
對於具有執行階段的用戶端,另一個替代做法是使用 zx_object_wait_async
而非 wait_many
來維護本機快取,讓快速路徑避免額外的系統呼叫。
我們也避免依附於 靜態 FIDL 和 FIDL 內建的額外資料複本,因為訊息酬載已內建於系統呼叫中,可直接將酬載複製至最終目的地。
缺點
在核心方法中,當路由表變更時,沒有明顯的方式可以執行 O(1) 路徑取消作業。如前所述,我們可以為 zx_socket_remove_route
新增標記,以移除所有路徑 (可能還是會希望如此),但網路堆疊需要在每個 Socket 上發出 zx_socket_remove_route
。
我們可以讓 zx_socket_add_route
為取消事件對應項目,但這會變得相當複雜。
將這些結構體整合至核心的成本也相當高,因為這會在平台中引入另一個通訊協定演進模型;現在,特定 FIDL 類型和系統呼叫之間的耦合會更加緊密,且不會自動保持同步。
未來工作
使用事件組合來指出用戶端快取的有效性,會在 send
和 recv
路徑上產生額外的系統呼叫。您可以改用 VMO 來傳送有效性信號,藉此消除這項系統呼叫。在這種設計中,事件組合會在邏輯上由 VMO 的偏移值取代,並對應至用戶端的位址空間。接著,用戶端可以透過簡單的 VMO 讀取作業來檢查有效性。
未知
另外,還有一個問題是如何處理 SendControlData
。或許需要將其設為 zx_socket_send_to
的額外參數,或是運算的標記。