RFC-0109:快速 UDP 通訊端

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 語意下,用戶端會預期系統會針對用戶端對傳入/傳出封包處理作業所做的其他變更,提供同步 sendrecv 行為。具體來說,用戶端會預期這些變更會套用至所有尚未收到的酬載,且不會套用至已傳送的酬載。

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

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

透過 zircon 套接字傳輸,將同步 FIDL 呼叫替換為未確認的 I/O,會破壞這些語意。以下說明產生的問題和解決方法。

傳送路徑

問題

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

  1. 用戶端將 IP_TOS 設為某個值 A,同步更新網路堆疊中的狀態。
  2. 用戶端會呼叫 sendto,在 zircon 網路介面中排入酬載。
  3. 用戶端將 IP_TOS 設為某個值 B。
  4. 網路堆疊會將酬載從佇列中移除,並透過 IP_TOS=B 將其傳送至網路。
解決方案

修正用於設定與傳送路徑相關的 Socket 選項的 FIDL 處理常式,以便在網路堆疊的 dequeuing goroutine 將 zircon 套接字耗盡之前,將傳送路徑封鎖。接著,處理程序會修改相關狀態並傳回至用戶端。由於在呼叫 setsockopt 之前傳送的所有酬載都已從 zircon 套接字中移出佇列,並進入網路堆疊,因此不會使用新設定處理。

Recv 路徑

問題

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;
    }
  });
};

在用戶端中,快取目前要求的控制訊息集合,並根據下列程序使用該集合篩選由 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 通訊端時增加負荷。我們認為這是可接受的取捨,原因有二:

  1. 我們可能支援的控制訊息可容納約 100 個位元組。假設 MTU 約為 1500 個位元組,這項複製作業的額外負擔率低於 10%。
  2. 大多數的控制訊息狀態都是「每個封包」,這表示網路堆疊會將其與每個封包一同保留在記憶體中,並在封包排入套接字後釋放該記憶體。因此,系統的整體記憶體用量不應增加。

此外,我們會使用微型基準測試追蹤相關指標,並在新增控制訊息時執行。如果結果顯示不再值得做出權衡,我們可以恢復使用較慢的路徑 (我們還是需要保留這條路徑,以便支援 ICMP 資料包)。

序列化通訊協定

透過 zircon 通訊端執行 I/O 最簡單的方法,就是定義單一 Fidl 資料表來保存 UDP 酬載及其中繼資料,並使用 FIDL at rest 將其序列化。這個方法的缺點是,它會強制傳送端和接收端將酬載和中繼資料序列化為暫存緩衝區,確保至少有一個副本。

由於向量化 Socket 讀取和寫入功能可能很快就會推出,建議您建構可利用向量化 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,用於識別緩衝區中保存序列化中繼資料的區域大小 (以位元組為單位)。
  • 使用 FIDL at restSendMsgMeta 進行反序列化。

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

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

該函式會在傳送方向上計算訊息大小的邊界,前提是沒有不明欄位。網路堆疊可安全地假設 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;
}

...並變更行為,以便在引數允許的情況下 (即呼叫端未要求 ICMP 通訊端) 傳回 DatagramSocket

初始實作預期會在每個 SendMsgBoardingPass.validity 中提供兩個元素:

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

缺點、替代方案和未知事項

本提案會在使用者空間中建構機制,以解決動機問題。另一種做法是在核心中建構這項機制。

用來將其轉換為核心的草圖:

  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 的地址不在地圖中,該作業會失敗,而使用者空間需要向 netstack 傳送同步訊息,要求將該路由新增至 zx::socket。如果要求失敗,則地址操作會失敗並顯示錯誤。

優點

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

未來工作

使用事件組合來指出用戶端快取的有效性,會在 sendrecv 路徑上產生額外的系統呼叫。您可以改用 VMO 來傳送有效性信號,藉此消除這項系統呼叫。在這種設計中,事件組合會在邏輯上由 VMO 的偏移值取代,並對應至用戶端的位址空間。接著,用戶端可以透過簡單的 VMO 讀取作業來檢查有效性。

未知

另外,還有一個問題是如何處理 SendControlData。或許需要將其設為 zx_socket_send_to 的額外參數,或是運算的標記。