# 进行中的工作/征求反馈：Socket 缓冲区

* 原文链接：[WPT/CFT: Socket Buffers](https://freebsdfoundation.org/wp-content/uploads/2022/08/WIP-CFT_sockets.pdf)
* 作者：**TOM JONES** & **GLEB SMIRNOFF**

在过去，BSD 网络堆栈有一个通用的套接字缓冲区实现，既用于 TCP、UDP、局部 IPC 套接字（即 UNIX 套接字）等。这些缓冲区当然有一些相似之处——它们用于缓冲数据，但也有一些根本性的区别。

有些是远程的，有些是本地的。有些支持数据流，有些支持数据报。随着 2015 年非阻塞 **sendfile** 的引入，我们提出了流发送缓冲区中“未就绪数据”的概念。然后，随着 2017 年 **KTLS** 的引入，它们变得更加复杂。与此同时，这些缓冲区仍然支持 POSIX 规定的 UNIX 控制消息。所以，我们得到了一段需要同时支持所有可能特性的通用代码——这变得非常复杂。它变得容易受到更改的影响，因为将一个套接字缓冲区修改为偏向某个协议可能会影响到另一个协议的行为。例如，未就绪数据的更改需要进行一次广泛的代码调整，而这些调整与 **sendfile** 完全无关，参见 git 提交 **cfa6009e364** 和 **0f9d0a73a49**。

在最后一个提交中，注意最后一段。**SCTP** 已经在 BSD 部分之外独立实现了自己的套接字缓冲区（这一实现给了我许多关于当前工作的启示）。与此同时，关于在 FreeBSD 中多少复制和粘贴是坏的，多少是好的观念在这些年里也发生了变化。我们有多个设备驱动程序，它们最初是作为其他驱动程序的粘贴而来的，但很明显，差异逐渐累积，最终就显得有意义去编辑一个粘贴，而不是在一个代码中支持两个相似的实例。接近套接字层的一个例子是两个 TCP 堆栈，它们也作为两个独立的源文件进行维护。总结来说，我们不再认为“一份代码适用于所有”是个好主意。最初，套接字代码很难从第一、第二、第三次观察中进行修改。如果你查看当前的 **soreceive\_generic()** 和 **sosend\_generic()**，你就会明白为什么。然而，在做了这些工作之后，我提出了一个计划，使我能够“拾起棍子，留下结构不倒”（[https://en.wikipedia.org/wiki/Pick-up\_sticks）。](https://en.wikipedia.org/wiki/Pick-up_sticks%EF%BC%89%E3%80%82)

1. 我们只有两种 **SOCK\_DGRAM** 套接字：**UNIX** 和 **UDP**。只需重新定义 **pru\_sosend** 和 **pru\_soreceive**，我们就有了 **PF\_UNIX/SOCK\_DGRAM** 的私有代码实现。参见 **34649582462** 和 **e3fbbf965e9**。这使得 **PF\_INET/SOCK\_DGRAM**（即 **UDP**）成为 **uipc\_socket.c** 中通用 **sockbuf** 代码支持的唯一数据报类型。
2. **sockbuf** 可以被拆分为与事件调度交互的公共部分和执行实际缓冲的私有部分。（参见提交 a4fc41423f7 和 a7444f807ec）。这使得 **PF\_UNIX/SOCK\_DGRAM** 完全独立！这意味着 **PF\_INET/SOCK\_DGRAM**（即 UDP）将成为 **struct sockbuf** 中遗留部分需要支持的唯一数据报类型。
3. 现在我们可以开始改进 **PF\_UNIX/SOCK\_DGRAM**，然后再处理其他部分。长期以来，一对多的 **unix/dgram** 套接字存在一个问题：一个写入者可能会使套接字溢出，从而有效地进行 DDoS 攻击。这里是我们历史上的尝试：2e89951b6f20 和 240d5a9b1ce76。我们可以让一对多的套接字为每个对等方维护一个独立的子缓冲区。参见 458f475df8e。此外，也可以通过无锁队列的方式来提高 **unix/dgram** 的速度，但这次我不打算这样做。对于我来说，**unix/dgram** 的每秒数据包性能并不是最关键的。
4. 回到套接字结构——**PF\_INET/SOCK\_DGRAM**（即 UDP）是唯一一个仍使用通用实现的数据报套接字，因此我们也可以将它私有化。很高兴看到 Robert Watson 已经为 UDP 准备了两个函数：**sosend\_dgram()** 和 **soreceive\_dgram()**。但 **soreceive\_dgram()** 还不能完全替代 **soreceive\_generic()**，需要修复使用 **soreceive\_generic()** 处理复杂情况的部分。
5. 现在我们可以开始研究 UDP 性能，并可能让它使用 **buf\_ring(9)** 替代链式 **mbuf** 列表吗？谁愿意承担这个任务？我们肯定关心 UDP 的每秒数据包处理性能，对吧？
6. 由于 **sosend\_generic()** 和 **soreceive\_generic()** 不再需要支持数据报类型，我们终于可以简化它们了！这可能是历史上第一次，这两个庞大的函数将缩小而不是膨胀。
7. 这使得 **UNIX/STREAM** 成为唯一一个由通用代码支持并且具有控制数据的套接字类型。如果它获得私有实现，我们可以从 **sosend\_generic()** 和 **soreceive\_generic()** 中移除对控制数据的支持。此时，它们将进一步简化！
8. 我们距离让 TCP 和 SCTP 独立处理已经非常接近了。需要注意的是，还有一些特殊的套接字，如 **netgraph** 等。目前，不清楚哪种计划更好：要么选择

* **–1**，将 TCP 与通用代码隔离，
* 要么选择 **–2**，将其他所有内容从通用代码中隔离并将通用代码重命名为 TCP。

无论哪种方式，最终目标是将 TCP 和 SCTP 的套接字缓冲区隔离开来，从而解放我们的双手，进行性能优化，而不必担心影响到其他部分。

在 D36002 中，Alexander Chernikov 现在分享了他为 **NETLINK** 套接字类型所做的工作。这个套接字可能会累积整个互联网的路由表视图，涉及需要从内核读取数百兆字节的数据。通用的套接字缓冲区实现需要分配大量 **mbuf** 来存储这些数据。这样的全视图获取可能导致 **mbuf** 短缺，这是路由器上的关键资源。但是，我们为什么一开始就使用 **mbuf** 呢？我们只是需要将数据从内核复制到用户空间。新的 **NETLINK** 将从协议特定的套接字缓冲区中受益，该缓冲区将数据从其特定的数据结构复制到用户空间 I/O，而不需要使用 **mbuf**。

## 人们如何测试这些工作？

**PF\_UNIX/SOCK\_DGRAM** 的新实现已经成为 FreeBSD 主分支的一部分。非常欢迎各种反馈和测试，尤其是那些有大量 **syslog(3)** 流量，且曾受日志套接字溢出问题影响的人。

进一步的计划仍在进行中。我通常在工作处于早期阶段时，将其分享在 <https://github.com/glebius/FreeBSD>，在更成熟时将其发布到 [https://reviews.FreeBSD.org](https://reviews.freebsd.org)。欢迎在那里，或通过电子邮件发表评论。

***

**TOM JONES** 希望基于 FreeBSD 的项目能获得应有的关注。他住在苏格兰东北部，提供 FreeBSD 咨询服务。

**GLEB SMIRNOFF** 在 17 岁时第一次接触 FreeBSD，并对其一见钟情。他曾在大大小小的公司工作，总是寻找一个能让他为开源做出贡献的工作。现在，他在奈飞 OpenConnect 团队工作，正在用前所未有强大的 FreeBSD 服务器流量饱和互联网。
