摘要

  • RFC 817 区分了“协议怎样分层”和“软件怎样执行”。对端可见的接口必须稳定,主机内部的代码边界却可以依据解复用、计时器、调度和状态所有权横切 IP 与 TCP。
  • 模块隔离带来可替换性,也会收取包数、上下文切换和数据复制的租金。Telnet 的 ACK、窗口更新与回显说明,有限且有时限的跨层提示可以合并工作,而不必转移协议权力。
  • 优化的授权来自测量而不是栈图。常见路径、具体流量类型和已定位瓶颈可以支持局部特化;罕见异常、抽象偏好或未经证明的速度想象不能。

一条横线回答不了“谁先被唤醒”

假设 IP 位于内核,而整个 TCP 位于一个独立进程。数据报到达时,只有读出 TCP 端口与连接信息后,系统才能知道最终应把它交给哪个用户进程。于是内核先唤醒 TCP 进程,TCP 完成解复用,再唤醒应用。协议图上干净的 IP/TCP 边界,在运行时变成了两次调度。

RFC 817 提出的办法不是删除 TCP,而是换一条实现切线。在内核中保留足以完成最终解复用的 IP 和 TCP 逻辑,把数据报直接送入目的进程;需要阻塞、使用复杂计时器或依赖该连接环境的其余工作,再在进程上下文中完成。执行边界因此同时穿过 IP 和 TCP,因为昂贵的决定由功能位置决定,而不是由规范的章节标题决定。

网络上发出的比特没有因此改变。应用也没有获得发明 TCP 语义的权力。对外,协议层仍规定同伴可以期待什么;对内,模块只是在选择状态放在哪里、哪个上下文能够等待,以及一次越界究竟要付出多少调度成本。

这种切法甚至容许每条连接拥有自己的 TCP 上下文。文件传输可以偏向吞吐,交互终端可以偏向低延迟。代价也很直接:不同变体会扩大测试矩阵,同一缺陷可能要在多处修复。RFC 817 把它写成尚未普遍接受的实验方向,而不是值得照抄的结论。

内核、进程与前端机只是交换账单

把协议放在普通进程中,开发者可以避开一部分内核修改,也能获得较熟悉的阻塞和计时环境。但每个到达事件都可能等待调度;若服务要模拟终端或本地设备,路径还得绕回内核。组织上的整齐会变成数据路径上的往返。

把协议放进内核,可以减少进程切换,也便于接入设备和终端接口。可内核往往不给复杂计时动作提供安全的进程式环境。在中断级执行意味着代码通常不能等待,可能长时间屏蔽中断;包到得比系统处理得快时,调度器也未必有机会限制网络路径占用整台机器。紧张的内核空间还会让每次增加功能都变成取舍,深度耦合的协议实现也更难随操作系统迁移。

独立通信处理器看起来像第三条出路。它可以运行专门为协议工作的系统,也可能供多台主机复用。但主机仍要通过某种接口与它交换数据、流控、状态和故障信息。这个接口本身就具有协议的性质。把边界移到另一台机器,没有把边界消灭。

所以问题不能只写成“内核还是进程”。要问的是:哪个动作需要靠近设备,哪个动作必须能够阻塞,哪些计时器属于连接,谁做最终解复用,状态由谁拥有,以及每次跨越会增加多少复制、唤醒和失败面。

一个字符暴露了层次隔离的租金

字符模式 Telnet 收到一个按键后,TCP 要确认数据,接收端可能要更新窗口,Telnet 或应用要把字符回显;若字符属于控制序列,还会出现更多动作。每一层都在履行自己的义务,却可能分别发出一个包。一个有用输入由此换来数个正确但可以合并的响应。

浪费不是协议错误,而是相邻组件互不知道对方即将产生数据。固定的逐包开销随之重复:中断、调度、收发路径、当时按流量计费的网络费用,以及对端同样的处理动作。模块隔离保护了责任边界,也隐藏了即将发生的工作。

RFC 817 允许一种很窄的提示:TCP 可以询问上层是否可能很快发送,并在发 ACK 前短暂等待。对交互式 Telnet,这能把确认、窗口信息和回显放进同一个报文段。对单向文件传输,同样的等待可能拖慢发送方的下一批数据。优化是否成立,取决于已知的使用者和流量形态。

RFC 1122 后来把 delayed ACK 写成更具体的要求:延迟不得超过半秒,并且至少每两个完整报文段确认一次。它也重复了终端场景中三个报文段可以合成一个的例子,同时警告过度延迟会干扰 RTT 测量和包时钟。跨层信息可以影响时机,但有期限、有反信号,也不能无限暂停 TCP 的责任。

接口既提供隔离,也遮住意图

稳定而清楚的层接口让一侧能够改变,同时对另一侧保持可预期。多个应用可以共享同一服务,开发者不必理解整个系统,协议家族也能跨越不同机器和团队。这些收益不是图面美观,而是互联网实现得以多样化的前提。

同一个接口也可能把语义压缩得过薄。只暴露字节流的服务可能迫使大块传输走一套并不合适的操作;缺乏共享内存的进程边界会让内核复制数据;每层只知道自己的发送时点,就会产生本可共载的多个包。

RFC 817 因而把层边界称为既有好处又有惩罚的选择。它没有授权实现者随意刺穿接口。它要求把租金摆上账本:包数、复制、调度、查询、延迟、变体数量,以及以后修补或移除优化的成本。只有省下的工作可以证明、泄露的信息足够小、没有提示时仍回到标准正确路径,越界才有正当性。

RFC 1958 后来把“模块化是好事”和“性能与成本必须考虑”放在相邻的架构原则中,并把真实实现的反馈置于格言之上。RFC 3439 又把复杂度与扩展、运营费用和变更风险联系起来。它们共同反对两个绝对判断:纯净并非免费,耦合也不会自动带来效率。

Multics 的六毫秒不是风格宣言

RFC 817 对 Multics 的测量给出了一个有刻度的例子。该机器用 36 位字保存 TCP 报文段的 8 位字节,排列很不方便。早期校验和路径处理 576 字节大约需要六毫秒;经过针对机器的精细重写,时间降到一毫秒以内。

这不是鼓励整个协议栈采用晦涩技巧。文档把这种写法称为肮脏但可接受,原因恰恰是瓶颈已经被识别,特殊代码局限在一个函数,而且结果仍可同参考实现比较。局部丑陋必须被范围、证据和可验证性约束。

其他改进没有同样戏剧性,却更能说明常见路径的纪律。下一个报文段通常按序到达,就先测试这个预期情况。重传队列更常用于删除已获确认的项目,而不是实际重传,就应让删除更便宜。不要仅因为模块接口方便,就把同一份数据再复制一次。

性能损失往往散落在校验和、复制、队列搜索、调度和状态转换中。实现者很少能找到一个怪物,杀掉之后便解决一切。效率必须在边界固化之前,沿整条运行路径逐项测量和构造。

两张地图服务于两种责任

协议栈地图记录同伴之间承诺交换什么。实现地图记录一台主机在哪里消耗时间、移动字节、安排工作和隔离故障。把二者混为一谈,就等于让一张教学图在未经选择的情况下成为调度器。

RFC 817 把选择重新交还给证据:观察每个有用动作产生多少包,追踪每次复制,区分常见与例外流量,记录哪个提示穿过边界、由谁拥有、何时过期,以及提示不存在时会怎样。然后守住共同的外部合同,让另一种实现仍有权画出不同的内部地图。

层是真实的,它承担互操作责任。成本也是真实的,必须在运行路径中偿付。模块只是本地选择,因此每一次横切都应能够说明为什么、如何回退,以及何时应当被删除。

资料来源