RFC-0109:快速 UDP 通訊端

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 語意下,用戶端會預期系統會針對傳入/傳出封包處理的其他用戶端啟動的變更,提供同步的 sendrecv 行為。具體而言,客戶會預期這些變更會套用到「所有」尚未收到的酬載,以及「所有」已傳送的酬載。

在目前的實作項目中,保證同步行為的原因如下:

  1. 網路堆疊負責所有相關封包處理。
  2. 用戶端透過同步 FIDL 呼叫傳送及接收酬載。
  3. 用戶端會設定通訊端選項以要求變更封包處理,這些選項也實作為同步 FIDL 呼叫。

透過 zircon 通訊端以未確認的 I/O 取代同步 FIDL 呼叫會破壞這些語意。以下說明產生的問題和解決方法。

傳送路徑

問題

在「Send Path」(傳送路徑) 上,用戶端會設定通訊端選項,修改酬載的處理方式。由於 setsockopt 是透過同步 FIDL 實作,因此選項的新值可能會套用至呼叫前排入佇列的封包。例如:

  1. 用戶端將 IP_TOS 設為某個值 A,並在網路堆疊中同步更新狀態。
  2. 用戶端呼叫 sendto,這會在 zircon 通訊端將酬載排入佇列。
  3. 用戶端將 IP_TOS 設為某個值 B。
  4. 網路堆疊會將酬載取消佇列,並使用 IP_TOS=B 將其傳送至網路。
解決方案

修訂用於設定與傳送路徑有關的通訊端選項的 FIDL 處理常式,直到網路堆疊的排除佇列來排除 zircon 通訊端為止。之後,處理常式會修改相關狀態並傳回用戶端。由於呼叫 setsockopt 之前傳送的所有酬載皆已從 zircon 通訊端解除佇列至網路堆疊,因此系統不會根據新設定處理所有酬載。

參考檔案路徑

問題

在「Recv Path」上,用戶端可透過設定通訊端選項來要求控制訊息,以便提供酬載及其傳送的相關輔助資料。再次提醒您,由於 setsockopt 是同步性質,因此可能會有偏差的空間。例如:

  1. 網路堆疊將酬載排入 zircon 通訊端。
  2. 用戶端設定了 IP_RECVTOS
  3. 用戶端會將酬載解除佇列,並在沒有 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 通訊端時增加負擔。我們認為這是可接受的優缺點,原因如下:

  1. 我們可能需要支援的控制訊息,大小約可達 100 個位元組。這個值是小於 10% 的複製負荷 (假設 MTU 為 1500 個位元組)。
  2. 絕大多數的控制訊息狀態都是「每個封包」,這表示網路堆疊會將該訊息與每個封包一起保存在記憶體中,並在封包排入通訊端後釋放記憶體。因此,系統的整體記憶體用量不應增加。

此外,我們也將使用 Microbenchmark 追蹤相關指標,並在新增控制訊息時執行這個指標。如果結果顯示權衡已無法再進行,我們就會改採慢速路徑 (我們還是必須保留該路徑,以便支援 ICMP 資料元)。

序列化通訊協定

透過 zircon 通訊端執行 I/O 的最簡單方法,就是定義單一 FIDL 資料表來保存 UDP 酬載及其中繼資料,並使用 FIDL 靜態資料進行序列化。此方法的缺點是,會強制傳送者和接收者將酬載和中繼資料序列化為臨時緩衝區,並保證至少兩者的副本。

由於向量通訊通訊端讀取和寫入功能很快就會推出,因此建議您建構通訊協定,藉由利用向量化 API 避免複製作業。

通訊協定

sendrecv 中繼資料定義 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,識別保存序列化中繼資料的緩衝區區域 (以位元組為單位)。
  • 使用靜態 FIDLSendMsgMeta 還原序列化。

接收路徑完全以對稱的方式運作。

uint32_t tx_meta_buf_size = fidl::MaxSizeInChannel<fuchsia_posix_socket::wire::SendMsgMeta, fidl::MessageDirection::kSending>();

,假設沒有不明欄位,則會計算訊息在傳送方向中的邊界。Netstack 可以安全地假設 RecvMsgMeta 中的所有欄位都是已知的,因為網路堆疊本身會序列化該訊息。這可以假設 SendMsgMeta 中的所有欄位都是已知的,原因如下:

  1. 用戶端的 ABI 修訂版本會擷取用戶端序列化資料表中的所有欄位組合。
  2. 如果平台不支援用戶端的 ABI 修訂版本,就會拒絕執行用戶端。
  3. 系統一律會利用這個平台建構 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 中提供兩個元素:

  1. 代表路由表的已知狀態;由所有通訊端共用,並在路由資料表發生任何變更時失效。
  2. 代表特定通訊端的已知狀態;對可能變更通訊端行為的任何通訊端變更無效,例如呼叫 bindconnectsetsockopt(..., 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 文件註解以外,您不需要提供任何說明文件。

缺點、替代項目和未知

本提案透過在使用者空間中建構機器,藉此解決您的遊戲動機。另一種做法是在核心中建構這個機器。

翻譯成核心的草圖:

  1. 針對每個 zx::socket 端點,核心會維護從 SocketAdddresmax_size 的對應關係。
  2. 我們要新增一些 zx_socket_add_route / zx_socket_remove_route 系統呼叫,用於修改對等點端點上的對應。
  3. 我們要新增一些會耗用 / 提供地址的 zx_socket_send_to/zx_socket_receive_from 系統呼叫。

如果名稱為 zx_socket_send_to 且位址不存在於地圖中的使用者空間,這項作業將失敗,且使用者空間必須向網路堆疊傳送同步訊息,要求將該路徑新增至 zx::socket。如果要求失敗,位址作業就會失敗並顯示錯誤。

優點

在核心方法中,傳送 UDP 封包 (在快速情況下) 會是單一系統呼叫 (zx_socket_send_to),而非兩個系統呼叫 (zx_object_wait_manyzx_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 類型和系統呼叫之間的耦合關係,系統不會自動保持同步。

未來工作

使用事件配對表示用戶端快取的有效性時,將在 sendrecv 路徑上進行額外的系統呼叫。您可以改用 VMO 來通知有效性,這樣就能消除這項系統呼叫。在這種設計中,事件組合會由對應至用戶端位址空間的 VMO 偏移取代。之後,用戶端只要簡單讀取 VMO,就能檢查有效性。

不明

還有如何處理 SendControlData 的問題。也許需要是 zx_socket_send_to 的額外參數,或是作業的標記。