下载本文的PDF版本 PDF

套接字何去何从?

高带宽、低延迟和多宿主对套接字 API 提出了挑战。


乔治·V·内维尔-尼尔


套接字 API 是软件中最普及和最持久的接口之一。套接字 API 由加州大学伯克利分校的计算机系统研究组开发,于 1982 年首次作为 4.1c BSD 操作系统的一部分发布。虽然存在更长寿的 API——例如,处理 Unix 文件 I/O 的 API——但一个 API 能够持续使用并基本保持不变 27 年,这令人印象深刻。套接字 API 的唯一重大更新是扩展了辅助例程,以适应 IPv6 使用的更大地址。2

自套接字 API 首次开发以来,互联网和网络世界总体上发生了非常显著的变化,但在许多方面,该 API 对开发人员思考和编写网络应用程序的方式产生了限制作用。本文首先简要考察套接字 API 开发时存在的一些条件,并考虑这些条件如何塑造了网络代码的编写方式。文章的其余部分着眼于开发人员如何尝试绕过 API 中固有的一些限制,并探讨在不断变化的网络世界中套接字的未来。

1982 年和 2009 年的网络之间最大的两个区别是拓扑结构和速度。在很大程度上,人们注意到的是速度的提高,而不是拓扑结构的变化。1982 年,商用长途网络链路的最大带宽为 1.5 Mbps。同时部署的以太网 LAN 的速度为 10 Mbps。家庭用户——当时很少有这样的用户——幸运地通过电话线与任何计算设施建立 300-bps 的连接。局域网上两台机器之间的往返时间以数十毫秒为单位衡量,互联网上系统之间的往返时间以数百毫秒为单位衡量,当然,这取决于位置以及数据包在机器之间路由时所经历的跳数。(要查看早期互联网的图形,请访问 personalpages.manchester.ac.uk/.../m.dodge/.../arpanet4.gif。)

当时的网络拓扑结构相对简单。大多数计算机只有一个连接到局域网;LAN 连接到一个原始路由器,该路由器可能与其他 LAN 有少量连接,并且只有一个连接到互联网。对于一个应用程序到另一个应用程序,连接要么跨越 LAN,要么经过一个或多个路由器,称为 IMP(互联网消息传递)。

套接字的历史

套接字 API 最流行的分布式编程模型是客户端/服务器模型,其中有一个服务器和一组客户端。客户端向服务器发送消息,请求服务器代表他们执行工作,等待服务器完成请求的工作,并在稍后某个时间接收答案。这种计算模型现在非常普及,以至于它通常是许多软件工程师熟悉的唯一模型。然而,在设计时,它被视为将 Unix 文件 I/O 模型扩展到计算机网络上的一种方式。将套接字 API 集中到客户端/服务器模型的另一个因素是,它支持的最流行的协议是 TCP,TCP 具有固有的 1:1 通信模型。

套接字 API 使客户端/服务器模型易于实现,因为程序员只需在其非网络代码中添加少量的额外系统调用,即可利用其他计算资源。虽然其他模型是可能的,但使用套接字 API,客户端/服务器模型已成为主导网络计算的模型。

虽然套接字 API 的入口点比此处显示的更多,但正是这五个入口点是 API 的核心,并将 API 与常规文件 I/O 区分开来

socket() 创建通信端点
bind() 将端点绑定到某些网络层参数集
listen() 设置未完成工作请求数量的限制
accept() 接受来自一个或多个客户端的一个或多个工作请求
connect() 联系服务器以提交工作请求

实际上,socket() 调用可以被删除并替换为 open() 的变体,但这在当时没有完成。socket() 和 open() 调用实际上向程序返回相同的东西:一个进程唯一的文件描述符,用于 API 的所有后续操作。正是 API 的简单性导致了它的普及,但这种普及阻碍了替代或增强型 API 的开发,这些 API 可以帮助程序员开发其他类型的分布式程序。

客户端/服务器计算在开发时具有许多优势。它允许多个用户共享资源,例如大型存储阵列和昂贵的打印设备,同时将这些设备保持在曾经运行大型计算机设施的同一部门的控制之下。使用这种共享模型,可以提高当时昂贵资源的利用率。

套接字 API 不能很好地服务于三个不同的网络领域:低延迟或实时应用程序;高带宽应用程序;以及多宿主系统——即具有多个网络接口的系统。许多人将网络带宽的增加与更高性能混淆,但带宽的增加不一定会降低延迟。套接字 API 面临的挑战是让应用程序更快地访问网络数据。

任何使用套接字 API 的程序发送和接收数据的方式都是通过调用操作系统。所有这些调用都有一个共同点:调用程序必须重复请求交付数据。在客户端/服务器计算的世界中,这些持续的请求是完全合理的,因为没有来自客户端的请求,服务器就无法执行任何操作。除非客户端有要打印的东西,否则打印服务器调用客户端是没有意义的。但是,如果提供的服务是音乐或视频分发呢?在媒体分发服务中,可能有一个或多个数据源和许多听众。只要用户正在收听或观看媒体,最可能的情况是应用程序想要任何已到达的数据。专门请求新数据对于应用程序来说是浪费时间和资源。套接字 API 没有为程序员提供一种方法来说:“只要有我的数据,就调用我直接处理。”

相反,套接字程序是从数据匮乏而不是数据丰富的角度编写的。网络程序非常习惯于等待数据,以至于它们使用单独的系统调用 select(),以便它们可以监听多个数据源,而不会阻塞单个请求。基于套接字的程序的典型处理循环不仅仅是 read()、process()、read(),而是 select()、read()、process()、select()。虽然向循环添加单个系统调用似乎不会增加太多负担,但事实并非如此。每个系统调用都需要编组参数并复制到内核中,以及导致系统阻止调用进程并调度另一个进程。如果调用 select() 时数据对调用者可用,那么跨越用户/内核边界的所有工作都将是浪费的,因为 read() 将立即返回数据。除非连续请求之间的时间非常长,否则恒定的检查/读取/检查是浪费的。

解决此问题需要反转应用程序和操作系统之间的通信模型。已经提出了各种尝试提供允许内核直接调用程序的 API,但没有一个获得广泛接受——原因有几个。开发套接字 API 时的操作系统,除了在非常深奥的情况下,都是单线程的,并且在单处理器计算机上执行。如果内核配备了上调用 API,则会出现调用可以在哪个上下文中执行的问题。由于内核正在执行对应用程序的上调用,而暂停系统上的所有其他工作将是不可接受的,尤其是在有数十到数百个用户的分时系统中。这种软件架构确实获得普及的唯一地方是在嵌入式系统和网络路由器中,这些系统和路由器没有用户,也没有虚拟内存。

虚拟内存问题加剧了实现内核上调用机制的困难。分配给用户进程的内存是虚拟内存,但网络接口等设备使用的内存是物理内存。让内核将来自设备的物理内存映射到用户空间程序会破坏虚拟内存系统提供的基本保护之一。

克服性能问题的尝试

已经提出并在各种操作系统上实现了一些不同的机制,以克服套接字 API 中存在的性能问题。一种这样的机制是零拷贝套接字。任何从事网络协议栈工作的人都知道,复制数据会扼杀网络协议的性能。因此,为了提高对高带宽而不是低延迟更感兴趣的网络应用程序的速度,操作系统被修改为尽可能消除数据副本。

传统上,操作系统为系统接收的每个数据包执行两次复制。第一次复制由网络驱动程序执行,从网络设备的内存复制到内核的内存,第二次复制由内核中的套接字层在用户程序读取数据时执行。每个复制操作都很昂贵,因为它必须为系统接收的每条消息发生。同样,当程序想要发送消息时,必须为发送的每条消息将数据从用户程序复制到内核中;然后,该数据将被复制到设备用于在网络上传输数据的缓冲区中。

大多数操作系统设计人员和开发人员都知道数据复制是系统性能的诅咒,并努力最大限度地减少内核中的此类复制。内核避免数据复制的最简单方法是让设备驱动程序直接将数据复制到内核内存和从内核内存复制数据。在现代网络设备上,这是它们如何构建内存的结果。驱动程序和内核共享两个数据包描述符环——一个用于传输,一个用于接收——其中每个描述符都有一个指向内存的指针。网络设备驱动程序最初使用来自内核的内存填充这些环。当接收到数据时,设备在正确的接收描述符中设置一个标志,并通过中断告诉内核有数据正在等待它。然后,内核从接收描述符环中删除已填充的缓冲区,并用新的缓冲区替换它,供设备填充。然后,数据包以缓冲区的形式通过网络协议栈移动,直到到达套接字层,在该层中,当用户程序调用 read() 时,数据包被复制出内核。程序发送的数据以类似的方式由内核处理,因为内核缓冲区最终被添加到传输描述符环中,然后设置一个标志以告诉设备它可以将缓冲区中的数据放在网络上。

内核中的所有这些工作都使最后一个复制问题悬而未决;已经进行了多次尝试来扩展套接字 API 以消除此复制操作。3,1 问题仍然是如何在用户/内核边界安全地共享内存。内核不能将其内存提供给用户程序,因为在这一点上,它会失去对内存的控制。崩溃的用户程序可能会使内核失去大量可用内存,从而导致系统性能下降。在跨内核/用户边界共享内存缓冲区方面也存在固有的安全问题。目前,对于用户程序如何使用套接字 API 实现更高带宽,还没有一个明确的答案。

对于更关心延迟而不是带宽的程序员来说,做得更少。对于等待网络事件的程序来说,唯一的重大改进是添加了一组程序可以等待的内核事件。内核事件或 kevents() 是 select() 机制的扩展,以涵盖内核可能能够告知程序的任何可能的事件。在 kevents() 出现之前,用户程序可以对任何文件描述符调用 select(),这将让程序知道一组文件描述符中的任何一个何时可读、可写或有错误。当程序被编写为坐在循环中并等待一组文件描述符时——例如,从网络读取和写入磁盘——select() 调用就足够了,但是一旦程序想要检查其他事件,例如定时器和信号,select() 就不再适用。低延迟应用程序的问题是 kevents() 不传递数据;它们只传递数据已准备好的信号,就像 select() 调用一样。下一个合乎逻辑的步骤是拥有一个也传递数据的基于事件的 API。没有充分的理由让应用程序仅仅为了获取内核知道应用程序想要的数据而两次跨越用户/内核边界。

缺乏对多宿主的支持

套接字 API 不仅给应用程序编写者带来了性能问题,而且还缩小了可能发生的通信类型。客户端/服务器范例本质上是一种 1:1 类型的通信。虽然服务器可以处理来自不同客户端组的请求,但每个客户端对于一个请求或一组请求只有一个与单个服务器的连接。在每台计算机只有一个网络接口的世界中,这种范例是完全合理的。客户端和服务器之间的连接由 <源 IP、源端口、目标 IP、目标端口> 四元组标识。由于服务通常具有众所周知的目标端口(例如,HTTP 的 80),因此唯一可以轻松更改的值是源端口,因为 IP 地址是固定的。

在 1982 年的互联网中,每台不是路由器的机器只有一个网络接口,这意味着为了识别服务(例如远程打印机),客户端计算机需要单个目标地址和端口,并且自身只有单个源地址和端口可以使用。计算机可能有多种方式访问服务的想法太复杂且实施成本太高。鉴于这些限制,套接字 API 没有理由向程序员公开编写多宿主程序的能力——即,可以管理哪些接口或连接对其重要的程序。这些功能在实现时是操作系统内路由软件的一部分。程序最终可以访问它们的唯一方法是通过一组晦涩的非标准内核 API,称为路由套接字。

在使用标准套接字 API 的具有多个网络接口的系统上,无法编写可以轻松实现多宿主的应用程序——即,利用两个接口,以便如果一个接口发生故障,或者数据包流经的主路由断开,应用程序不会丢失与服务器的连接。

最近开发的 SCTP(流控制传输协议)4 在协议级别集成了对多宿主的支持,但是不可能通过套接字 API 导出此支持。最初提供了一些临时的系统调用,并且是访问此功能的唯一方法。目前,这是唯一一个既有能力又有用户需求的功能协议,因此该 API 尚未在少数几个操作系统上标准化。此处的表列出了 SCTP 添加的 API。

API 说明
sctp_bindx() 将 SCTP 套接字绑定或解绑到地址列表
sctp_connectx() 使用多个目标地址连接 SCTP 套接字
sctp_generic_recvmsg() 从对等方接收数据
sctp_generic_sendmsg(), sctp_generic_sendmsg_iov() 向对等方发送数据
sctp_getaddrlen() 返回地址族 的地址长度
sctp_getassocid() 返回指定套接字地址的关联 ID
sctp_getpaddrs(), sctp_getladdrs() 向调用者返回地址列表
sctp_peeloff() 将关联从一对多套接字分离到单独的文件描述符
sctp_sendx() 从 SCTP 套接字发送消息
sctp_sendmsgx() 从 SCTP 套接字发送消息

虽然此函数列表包含比严格必要的更多的 API,但重要的是要注意,许多 API 是预先存在的 API 的派生,例如 send(),需要扩展这些 API 以在多宿主世界中工作。API 集需要协调,以使多宿主成为套接字世界中的头等公民。现在的问题是,套接字非常成功和普及,以至于很难更改现有的 API 集,因为害怕混淆其用户或使用它的预先存在的程序。

随着系统内置更多网络接口,编写利用多宿主的应用程序的能力将成为绝对的必需品。人们可以很容易地想象到这种技术在智能手机中的应用,智能手机已经有三个网络接口:通过蜂窝网络的主要连接、WiFi 接口以及通常还有蓝牙接口。即使这些网络接口中的一个正常工作,应用程序也没有理由失去连接。应用程序设计人员面临的问题是,他们希望他们的代码在各种设备(从手机到笔记本电脑再到台式机等)上工作,而几乎无需更改。通过正确定义的 API,我们将消除阻止这种情况发生的人为障碍。仅仅是因为套接字 API 的历史以及它到目前为止“足够好”,所以这个需求尚未得到解决。

高带宽、低延迟和多宿主正在推动套接字 API 替代方案的开发。随着 LAN 现在达到 10 Gbps,对于许多应用程序来说,客户端/服务器风格的通信效率太低而无法使用可用带宽。必须扩展套接字 API 支持的通信范例,以允许跨内核边界的内存共享,以及向应用程序传递数据的更低延迟机制。多宿主必须成为套接字 API 的首要特性,因为具有多个活动接口的设备正在成为网络系统的常态。

参考文献

  1. Balaji, P., Bhagvat, S., Jin, H.-W., Panda, D.K. 2006. 异步零拷贝通信,用于基于套接字直接协议 (sdp) over infiniband journal 的同步套接字。 第 20 届 IEEE 国际并行和分布式处理研讨会论文集:303。
  2. Gilligan, R., Thomson, S., Bound, J., McCann, J., Stevens, W. 2003. IPv6 的基本套接字接口扩展。RFC 3493(2 月); http://www.rfc-editor.org/rfc/rfc3493.txt
  3. Romanow, A., Mogul, J., Talpey, T., Bailey, S. 2005. 基于 IP 协议的远程直接内存访问 (RDMA) 问题陈述。RFC 4297(12 月); http://www.rfc-editor.org/rfc/rfc4297.txt。
  4. Stewart, R., Xie, Q., Morneault, K., Sharp, C., Schwarz-bauer, H., Taylor, T., Rytina, I., Kalla, M., Zhang, L., Paxson, V. 2000. 流控制传输协议。RFC 2960(10 月); http://www.ietf.org/rfc/rfc2960.txt

喜欢还是讨厌?请告诉我们

[email protected]

乔治·V·内维尔-尼尔 ([email protected]) 是 Communications of the 的专栏作家,也是 Queue 编辑委员会的成员。他从事网络和操作系统代码方面的工作,并教授与编程相关的各种主题的课程。

© 2009 1542-7730 /09/0200 $5.00

acmqueue

最初发表于 Queue vol. 7, no. 4
数字图书馆 中评论本文





更多相关文章

大卫·科利尔-布朗 - 你对带宽一窍不通
当您的员工或客户说他们的互联网性能很差时,带宽可能不是问题。一旦他们的带宽达到 50 到 100 Mbps 左右,问题就是延迟,即 ISP 的路由器处理他们的流量需要多长时间。如果您是一家 ISP,并且所有客户都讨厌您,请振作起来。现在,由于一群敬业的人员找到了问题、解决了问题,然后在家庭路由器中验证了他们的解决方案,因此这是一个可以解决的问题。


杰弗里·H·库珀 - 使用 FDO 和不受信任的安装程序模型的设备入职
设备的自动入职是处理正在安装的越来越多的“边缘”和 IoT 设备的一项重要技术。设备的入职不同于大多数设备管理功能,因为设备的信任从工厂和供应链转移到目标应用程序。为了通过自动入职加快流程,必须在设备中形式化供应链中的信任关系,以允许自动化过渡。


布赖恩·伊顿、杰夫·斯图尔特、乔恩·特德斯科、N. 西汉·塔斯 - 通过关键路径跟踪进行分布式延迟分析
低延迟是许多 Google 应用程序(如搜索)的一项重要功能,延迟分析工具在维持大规模低延迟方面发挥着关键作用。对于复杂的分布式系统(包括功能和数据不断发展的服务),将总体延迟保持在最低水平是一项具有挑战性的任务。在大型真实世界的分布式系统中,现有的工具(如 RPC 遥测、CPU 性能分析和分布式跟踪)对于理解整个系统的子组件很有价值,但在实践中不足以执行端到端延迟分析。


大卫·克劳肖 - 一切 VPN 都焕然一新
VPN(虚拟专用网络)已有 24 年的历史。这个概念是为与我们今天所知的互联网截然不同的互联网而创建的。随着互联网的增长和变化,VPN 用户和应用程序也随之发展。VPN 在 2000 年代的互联网中经历了尴尬的青春期,与其他广泛流行的抽象概念交互不良。在过去的十年中,互联网再次发生了变化,这个新的互联网为 VPN 提供了新的用途。一种全新的协议 WireGuard 的开发为构建这些新的 VPN 提供了技术基础。





© 保留所有权利。

© . All rights reserved.