RFC-0109:快速 UDP 通訊端 | |
---|---|
狀態 | 已接受 |
領域 |
|
說明 | 使用 zircon 通訊端實作網路 UDP 通訊端資料傳輸。 |
更小鳥 | |
作者 | |
審查人員 | |
提交日期 (年月分) | 2021-06-17 |
審查日期 (年-月-日) | 2021-06-25 |
摘要
使用 zircon 通訊端實作網路 Datagram 通訊端資料傳輸。使用用戶端快取網路狀態實作用戶端引數驗證。
提振精神
提高 Datagram 通訊端處理量並降低 CPU 使用率。
在 https://fxbug.dev/42094952 Datagram 網路通訊端之前,則是使用 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(...)` |
+-------------------------+------------------------+
雖然未經確認的 I/O 可能適用於 zircon 管道和 FIDL,但並不提供背壓,並可能導致記憶體增加。因此,我們建議使用 zircon 通訊端:
+------------------------+-----------------------+
| Client | Netstack |
+------------------------+-----------------------+
| `zx_socket_write(...)` | |
+------------------------+-----------------------+
| `zx_socket_write(...)` | `zx_socket_read(...)` |
+------------------------+-----------------------+
設計
中繼資料驗證
使用未經確認的 I/O 會讓系統在沒有與網路互動的情況下,在本機機器上完整驗證 Datagram 通訊端中繼資料 (例如目的地位址) 的驗證結果,而且系統可以在與通訊端的多項互動中快取這項驗證結果 (這樣驗證費用即可撤銷)。
請注意,這項假設並未針對 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)
。
請注意,這項策略產生的快取具有最終一致性;如果撤銷用戶端的效力檢查及其傳入網路堆疊的酬載,就可能無效。對於會盡量提供語意的 Datagram 通訊端來說,這是可接受的做法。
保證同步行為
背景
在 POSIX 語意下,用戶端會預期系統會針對傳入/傳出封包處理的其他用戶端啟動的變更,提供同步的 send
和 recv
行為。具體而言,客戶會預期這些變更會套用到「所有」尚未收到的酬載,以及「所有」已傳送的酬載。
在目前的實作項目中,保證同步行為的原因如下:
- 網路堆疊負責所有相關封包處理。
- 用戶端透過同步 FIDL 呼叫傳送及接收酬載。
- 用戶端會設定通訊端選項以要求變更封包處理,這些選項也實作為同步 FIDL 呼叫。
透過 zircon 通訊端以未確認的 I/O 取代同步 FIDL 呼叫會破壞這些語意。以下說明產生的問題和解決方法。
傳送路徑
問題
在「Send Path」(傳送路徑) 上,用戶端會設定通訊端選項,修改酬載的處理方式。由於 setsockopt
是透過同步 FIDL 實作,因此選項的新值可能會套用至呼叫前排入佇列的封包。例如:
- 用戶端將
IP_TOS
設為某個值 A,並在網路堆疊中同步更新狀態。 - 用戶端呼叫
sendto
,這會在 zircon 通訊端將酬載排入佇列。 - 用戶端將
IP_TOS
設為某個值 B。 - 網路堆疊會將酬載取消佇列,並使用
IP_TOS=B
將其傳送至網路。
解決方案
修訂用於設定與傳送路徑有關的通訊端選項的 FIDL 處理常式,直到網路堆疊的排除佇列來排除 zircon 通訊端為止。之後,處理常式會修改相關狀態並傳回用戶端。由於呼叫 setsockopt
之前傳送的所有酬載皆已從 zircon 通訊端解除佇列至網路堆疊,因此系統不會根據新設定處理所有酬載。
參考檔案路徑
問題
在「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;
}
});
};
在用戶端中,根據下列程序,快取目前要求的控制訊息組合,並使用該設定來篩選網路堆疊透過通訊端傳遞的控制訊息狀態:
+-----------------------------------------+
| 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 個位元組。這個值是小於 10% 的複製負荷 (假設 MTU 為 1500 個位元組)。
- 絕大多數的控制訊息狀態都是「每個封包」,這表示網路堆疊會將該訊息與每個封包一起保存在記憶體中,並在封包排入通訊端後釋放記憶體。因此,系統的整體記憶體用量不應增加。
此外,我們也將使用 Microbenchmark 追蹤相關指標,並在新增控制訊息時執行這個指標。如果結果顯示權衡已無法再進行,我們就會改採慢速路徑 (我們還是必須保留該路徑,以便支援 ICMP 資料元)。
序列化通訊協定
透過 zircon 通訊端執行 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 通訊端)。
初始實作應在每個 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
且位址不存在於地圖中的使用者空間,這項作業將失敗,且使用者空間必須向網路堆疊傳送同步訊息,要求將該路徑新增至 zx::socket
。如果要求失敗,位址作業就會失敗並顯示錯誤。
優點
在核心方法中,傳送 UDP 封包 (在快速情況下) 會是單一系統呼叫 (zx_socket_send_to
),而非兩個系統呼叫 (zx_object_wait_many
、zx_socket_write
)。
由於使用者空間方法可能經過最佳化,因此這可能不是專業做法。透過 time::infinite_past
發現我們一律透過 zx_object_wait_many
執行 zx_object_wait_many
,即可在不使用系統呼叫的情況下,將作業最佳化,前提是必要狀態是以不可分割的作業保留。這可能需要把處理表也放在 vDSO 中,但可能不是這樣,而且可能不適用。
使用執行階段的用戶端替代方法是使用 zx_object_wait_async
(而非 wait_many
) 來維護本機快取,以便透過快速路徑避免額外的系統呼叫。
我們也避免對靜態 FIDL 和 FIDL 中的額外資料複製固有的依賴性,因為訊息酬載已嵌入系統呼叫,而能直接將酬載直接複製到最終目的地。
缺點
在核心方法中,在路由表變更時,沒有明顯的方法取消 O(1) 路徑取消。如上所述,我們可以在 zx_socket_remove_route
中新增標記,藉此移除所有路徑 (或許沒問題),但網路堆疊必須在每個通訊端發出 zx_socket_remove_route
。
我們可以表現得十分出色,讓 zx_socket_add_route
可以用事件組合來取消,但實際上看起來相當巴洛克風格。
將這些結構放入核心中,也必須對平台引進另一個通訊協定演變模型,因此所費不貲。現在,我們現在有非常緊密的 FIDL 類型和系統呼叫之間的耦合關係,系統不會自動保持同步。
未來工作
使用事件配對表示用戶端快取的有效性時,將在 send
和 recv
路徑上進行額外的系統呼叫。您可以改用 VMO 來通知有效性,這樣就能消除這項系統呼叫。在這種設計中,事件組合會由對應至用戶端位址空間的 VMO 偏移取代。之後,用戶端只要簡單讀取 VMO,就能檢查有效性。
不明
還有如何處理 SendControlData
的問題。也許需要是 zx_socket_send_to
的額外參數,或是作業的標記。