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 套接字:
+------------------------+-----------------------+
| 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
行为(与客户端发起的对
传入/传出数据包处理。具体而言,客户希望
更改适用于所有尚未接收的有效负载,
有效负载。
在当前实现中,由于以下原因,可以保证同步行为:
- Netstack 负责所有相关的数据包处理。
- 客户端通过同步 FIDL 调用发送和接收载荷。
- 客户端通过设置套接字选项来请求对数据包处理的更改, 也以同步 FIDL 调用的形式实现。
将同步 FIDL 调用替换为 Zircon 套接字上的未确认 I/O 传输会破坏这些语义。由此产生的问题和解决方案 如下所述。
发送路径
问题
在发送路径上,客户端通过设置
套接字选项。由于 setsockopt
是通过同步 FIDL 实现的,因此
选项的新值可能会应用于调用 之前排入队列的数据包。
。例如:
- 客户端将
IP_TOS
设置为某个值 A,同时同步更新状态 是 Netstack 的技术。 - 客户端调用
sendto
,它会将载荷加入 zircon 套接字队列。 - 客户端将
IP_TOS
设置为某个值 B。 - Netstack 将载荷移出队列,并使用
IP_TOS=B
。
解决方案
修改了用于设置与发送请求相关的套接字选项的 FIDL 处理程序
阻塞路径,直到 Zircon 套接字被 Netstack 的
对 goroutine 进行排队。之后,该处理程序会修改相关状态,
返回给客户端由于所有载荷都是在调用 setsockopt
之前发送的
已经从 zircon 套接字发出的队列进入 Netstack,
它们将按照新设置进行处理。
接收路径
问题
在接收路径上,客户端可以请求提供
通过设置套接字选项提供有关有效负载及其传递的辅助数据。
同样,由于 setsockopt
是同步的,因此存在偏差。例如:
- Netstack 将载荷加入 zircon 套接字队列。
- 客户端设置
IP_RECVTOS
。 - 客户端将载荷移出队列并将其返回给用户,而不使用
IP_TOS
控制消息。
解决方案
在 Netstack 中,将每个载荷与 所需的最小状态一起加入队列, 任何受支持的控制消息。
定义一个 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 插槽。我们认为这是可以接受的 权衡的原因有两个:
- 我们可能支持的控制消息大约可以容纳 100 个字节。 复制开销 <10%(假设 MTU 约为 1500 个字节)。
- 绝大多数控制消息状态都是“每个数据包”,这意味着 Netstack 会将其与每个数据包一起保存在内存中,并释放相应内存 当数据包加入套接字队列时因此,系统的 整体内存消耗不应增加。
此外,我们还将使用一个 Microbenchmark 来跟踪相关指标,并运行 。如果结果有提示,以及何时 因此,我们可以恢复到慢路径, (为了支持 ICMP 数据报,我们仍需保留这一点)。
序列化协议
对 Zircon 套接字执行 I/O 的最简单方法是定义一个 用于保存 UDP 载荷及其元数据并使用 FIDL 对其进行序列化的 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;
};
在返回给客户端的 fuchsia.io/NodeInfo
中,
Describe
,指定用于接收包含
序列化元数据表:
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 |
+--------------+-----------------+----------------------------------+---------+
Netstack:
- 为
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
中的字段是已知的,因为 Netstack 本身会对
消息。它可以假设 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
且其地址不在映射中,
该操作将失败,并且用户空间将需要发送同步消息
发送到 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
,用于移除所有路由(可能需要
但网络堆栈需要发出 zx_socket_remove_route
每个套接字。
我们可以做得非常精美,让zx_socket_add_route
为
但这很有巴洛克风格
将这些结构烘焙到内核的成本也很高, 为平台提供另一个协议演进模型;这方面的需求 特定 FIDL 类型和系统调用之间更紧密的耦合, 不会自动保持同步。
未来工作
使用事件对指示客户端缓存的有效性会导致
在 send
和 recv
路径上都添加了额外的系统调用。此系统调用
可以改用 VMO 发出信号来表明有效。在这样的设计中
事件对在逻辑上被替换为映射到映射到
客户端地址空间随后,客户端可以使用
直接读取到 VMO 中。
未知
此外,还有一个有关如何处理 SendControlData
的问题。也许
需要是 zx_socket_send_to
的附加参数,或者可以是标志
操作