> For the complete documentation index, see [llms.txt](https://book.bsdcn.org/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://book.bsdcn.org/4.3-bsd-unix-cao-zuo-xi-tong-she-ji-yu-shi-xian/di-si-bu-fen-jin-cheng-jian-tong-xin/12.7.-tcp-shu-chu-chu-li.md).

# 12.7 TCP 输出处理

终于可以研究 TCP 实现中最有趣的部分——发送策略了。如前所见，TCP 分组除数据外还含确认和窗口字段，三者任一变化都可以发出一个分组。天真的 TCP 发送策略可能发出远多于必要的分组。想想用户往使用远程回显的远程终端连接敲一个字符会发生什么：服务器侧 TCP 收到一个单字符分组，它可以立即发对这个字符的确认；几毫秒后登录服务器读走字符、把它从接收缓冲移除，TCP 可以立即发窗口更新，说发送窗口多了一个八位组；再过一毫秒左右，登录服务器向客户发回显字符，输入一个字符引出第三个分组。显然这三个响应（确认、窗口更新、数据返回）可以合并进单个分组。但服务器若不回显输入数据，确认又不能扣得太久，否则客户侧 TCP 就要开始重传。发送策略中既把网络流量减到最少、又把吞吐提到最高的算法，是 TCP 实现中最精妙的部分。4.3BSD 的发送策略包含若干标准算法和网络研究社区建议的一些办法。我们逐一考察。

如前所见，触发连接上数据发送的事件有多种；此外还必须发分组传达确认和窗口更新（想想单方向的连接！）。

## 发送数据

调用 TCP 输出模块 `tcp_output()` 最明显的原因是用户向套接字写了新数据。写操作经带 PRU\_SEND 请求调用 `tcp_usrreq()` 完成（回想 `sosend()` 必要时等套接字发送缓冲腾出足够空间，然后把用户数据复制进 mbuf 链，带 PRU\_SEND 请求交给协议）。`tcp_usrreq()` 的动作只是用 `sbappend()` 把新输出数据放进套接字的发送缓冲并调用 `tcp_output()`。流控允许的话，`tcp_output()` 立即发送数据。

实际发送操作与 UDP 数据报套接字的差别不大：头部更复杂、要初始化更多字段；所发数据只是用户数据的副本 ——为备重传，副本必须留在套接字发送缓冲。数据八位组数超过单个最大报文段大小时，一次调用将构造并发送多个分组。

`tcp_output()` 例程分配一个 mbuf 装输出分组头，把头部模板的内容复制进去。待发数据用 `m_copy()` 从发送缓冲的相应部分取得，作为单独的 mbuf 链附上。分组的序号取自 `snd_nxt`，确认取自 `rcv_nxt`。标志从含各连接状态应发标志的数组取得。宣告的窗口按套接字接收缓冲剩余空间计算；但剩余量小（不足缓冲的四分之一且不足一个报文段）时窗口设为零。窗口绝不允许终止在比以前分组更小的序号上。发过紧急数据时，紧急指针和标志相应设置。还有个标志要设：分组的 PUSH 标志表示数据应递给用户，类似记录结束标记或缓冲冲刷请求。这一标志一般被认为过时，但发送缓冲中全部数据发出时总会置位；4.3BSD 在输入时无视它。头部填好后对分组做校验和，初始化 IP 头的其余部分，用 `ip_output()` 发送分组。重传定时器未在运行则启动，连接的 `snd_nxt` 和 `snd_max` 随之更新。

**糊涂窗口综合症的避免**

糊涂窗口综合症（silly-window syndrome）指基于窗口的流控方案中的一个潜在问题：接收方不等地出大小合理的窗口，而是连连发送小额分配 \[Clark, 1982]。例如，网络登录客户程序总接收缓冲 4096 八位组，用户在大打印输出期间停住终端输出，缓冲随新的满尺寸报文段到达而近乎填满。剩余缓冲降到 10 字节时，接收方再自告奋勇多收 10 个八位组并无用处；用户随后放行少量字符又停住，接收 TCP 发窗口更新允许再收 14 个八位组同样没用。合理做法是等到能发送一个大小合理的分组——接收缓冲里的数据已够下一几页输出之用。流控连接的接收方和发送方都希望避免糊涂窗口综合症，哪一端都能阻止糊涂的小窗口被使用。接收方的避免前面已描述：发送分组时，接收窗口小于一个分组且不足接收缓冲四分之一就宣告为零。发送方的避免：至少一整块分组的数据就绪、却因发送窗口大小而发不出一整块时，输出操作推迟。不发送，而是靠启动持续定时器把输出状态置为持续状态。定时器到期仍无窗口更新，就把允许的数据发出去，指望确认带来更大的窗口；没有的话连接留在持续状态，周期性发窗口探测直到窗口打开。

发送方避免的一个初期实现在与使用微小缓冲 TCP 实现的主机的连接上产生了大延迟和低吞吐。不幸的是，那些实现 **总是** 宣告小于 512 八位组的接收窗口，被本实现认为是糊涂的。由于这个问题，4.3BSD TCP 在协议控制块变量 `max_sndwnd` 中记录对端提供过的最大接收窗口。可以发送至少 `max_sndwnd` 的一半时，就发新的报文段。这一技术改善了与原始主机通信时的性能。

**小分组的避免**

网络流量的分组大小呈双峰分布。批量数据传输倾向用最大可能的分组换取最大吞吐；网络登录服务倾向用小分组，常常只含单个数据字符。以太网这样的快速局域网上，单字符分组一般不是问题——网络带宽通常没有饱和。ARPANET 等较慢的远程网络或经慢速链路互连的网络上，则希望把输入攒一段时间再放进单个网络分组发送。人们设计过各种方案：把输入攒固定时间（通常约 50 到 100 毫秒）再单分组发送。这些方案在快速网络上明显拖慢字符回显，在慢速网络上又常常省不了几个分组。相比之下，Nagle 提出了一个简单优雅的减少小分组流量的方案 \[Nagle, 1984]：允许第一个输出的八位组立即单独成组发送；但在这个分组被确认之前，不得发送新的小分组。足够多的新数据到来填满一个最大分组时，再发一个分组。未决数据一被确认，等待第一个分组期间排队的输入即可发送。一条连接任一时刻至多一个未决小分组。净效果是：小量数据在一个往返时间内排队。往返时间短于字符到达间隔时（局域网上的远程终端会话即如此），传输从不延迟，响应时间保持低；慢网络介入时，第一个字符之后的输入被排队，下一个分组装着前一个往返时间收到的输入。这一算法既简单又自调谐，颇为可取。

后来人们发现这一算法对某些发送无法成批的小请求流的网络客户并不好用。一个这样的客户是基于网络的 X Window System \[Scheifler & Gettys, 1986]：为给橡皮筋拉框之类用户界面提供实时反馈，它要求小消息立即投递。于是 TCP 增加了 TCP\_NODELAY 选项，按连接停用这一算法。该选项可用 `setsockopt` 调用设置，经 `tcp_ctloutput()` 例程到达 TCP。

**窗口更新**

TCP 分组的发送理由不止数据传输。单方向连接上，接收 TCP 仍须发分组确认收到的数据、推进发送方的发送窗口。这些分组何时发送是网络吞吐率的决定因素。例如，批量数据连接上接收方若只是每次收到数据就置 TF\_DELACK 标志，确认将每 200 毫秒发一次；10 Mbit/s 以太网上用 4096 八位组窗口，最大吞吐只有 160 Kbit/s——物理网络带宽的 1.6%。显然，发送方填满所给发送窗口后必须停下，等接收方确认旧数据（使它们能从发送缓冲移除、由新数据顶替）并提供窗口更新（允许发送新数据）。

TCP 基于窗口的流控受套接字接收缓冲空间约束，因此 TCP 的协议开关表项设置 PR\_RCVD 标志，使用户做接收调用、从接收缓冲取走数据时协议会被调用（经 `tcp_usrreq()` 的 PRU\_RCVD 请求）。PRU\_RCVD 入口只是调用 `tcp_output()`。（4.2BSD 中 `tcp_input()` 每收一个分组也调用 `tcp_output()`；4.3BSD 在多数场合删除了该调用。）`tcp_output()` 判定当前情况下发送窗口更新能给发送方提供值得的新的发送窗口时，就发送确认加窗口更新。接收方若真要等到窗口全满，发送方收到窗口更新时早已闲置多时。因此 4.3BSD 的窗口更新策略基于接收窗口的一个比例：新的窗口更新将至少暴露总接收缓冲的 35% 时发送。以太网上用默认 4096 字节缓冲和 1024 八位组报文段时，这一判据恰好每隔一个报文段确认一次。窗口更新策略使确认流量减半、发送方输入处理减半。这一策略假设每条连接的发送方有与接收方接收缓冲大小相仿的发送缓冲。不幸的是这一假设不总成立：发送方缓冲小于接收方接收缓冲的 35% 时，除了 `tcp_fasttimo()` 强制发出延迟确认外，窗口更新永不被触发。因此 4.3BSD Tahoe 版 TCP 把策略改为：发送方窗口至少增加两个报文段大小时发送窗口更新。

**重传状态**

发送方等确认期间重传定时器到期时，调用 `tcp_output()` 重传。重传定时器先设为退避序列中往返时间的下一个倍数；变量 `snd_nxt` 从当前序号退回 `snd_una`；然后发送传输队列中最旧数据的单个分组。与某些系统不同，4.3BSD 不保存连接上已发分组的副本，只保留数据。于是，虽然只重传一个分组，该分组可能比最旧的未决分组装更多数据。小量数据的慢连接上，一个丢失的单八位组分组重传时可能带上首次发送以来排队的全部数据。

网络中只丢了一个分组时，重传的分组将换来对已发全部数据的确认。丢了多个分组时，下一个确认将包括重传的分组，可能还有部分中间数据，还可能带新的窗口更新。于是，重传超时后收到确认时，未被确认的旧数据将重发，可能还有新数据。

**源熄灭处理与拥塞控制**

连接所用路由上的网关收到的分组多于它能沿该路径发出的量时，可能被迫丢包。此时网关可以向被丢分组的主机发 ICMP 源熄灭（Source Quench）错误报文，示意发送方放慢。这一报文虽表明该做些改变，却不说要改多少、改多久。Nagle 提出并由 4.3BSD 实现了一个处理源熄灭的算法 \[Nagle, 1984]。收到源熄灭说明网络中未决分组过多，收到源熄灭的主机必须减少一次发送的分组数。算法用第二个窗口——像发送窗口但单独维护——称为拥塞窗口（`snd_cwnd`）。起初拥塞窗口设为很大的量。某目的地收到源熄灭时，拥塞窗口设为未决数据的一个比例但不小于最大报文段大小（4.3BSD 中取 0.8）。发送策略随之修改：正常发送窗口和拥塞发送窗口都允许才发新数据。网络拥塞可能随时间减轻，每收到新确认，拥塞窗口就放开一小部分（如 10%）。放开得过大，自会有新的源熄灭到来。于是未决数据量应缓慢地围绕网络可容忍的限度震荡。

这一方案在 4.3BSD 中的实现有两个缺陷。第一，拥塞窗口须以字节保持才有意义，发送策略本应把这个限度截断为报文段大小的倍数；原实现会在拥塞窗口末尾发一个较小的报文段，对网络拥塞毫无缓解，却使接收方缓冲碎片化，还导致一段时间内出现古怪的分组大小。这一问题在 4.3BSD Tahoe 版中修正（见下两节）。第二，许多网关并不为每个丢弃的分组发源熄灭报文。

**慢启动**

许多 TCP 连接在源和目的地之间穿过多个网络。其中一些网络比另一些慢时，通往最慢网络的入口网关常被塞进超出其处理能力的流量。它可以缓冲一定数量的输入分组以免流量突变时丢包，但缓冲终会填满而不得不开始丢包。TCP 连接首次经快速网络向通往慢网络的网关发数据时，可能撞上网关缓冲已近满。4.3BSD 原发送策略下，批量数据传输建立连接后一上来就发整个窗口的分组；这些分组能以网络的全部速度涌到瓶颈网关，网关却只能慢得多的速率发出。结果初始分组串极可能溢出网关缓冲，部分分组丢失。这样的连接若为提高性能用了扩大的窗口——例如穿过往返时间很长的卫星网络——问题会更严重。但连接只要能达到稳态，分组沿路径摊匀时网络常常装得下一整窗口数据：稳态下，只有先前的分组被确认才有新分组注入网络，网络中的分组数恒定；即使分组成团到达外出网关，穿过最慢网络时也至少被各自的传输时间拉开。接收方每收一个分组就发确认的话，确认将以大致正确的间距返回发送方；发送方由此获得按网络正确速率发送的自计时手段，不再发出瓶颈装不下的分组串。

一个名叫慢启动（slow start）的算法把 TCP 连接带到这一稳态 \[Jacobson, 1988]。它叫 **慢启动** 是因为穿过慢网络时必须慢慢启动数据传输。方案简单：连接以只有一个未决分组的限度起步；每收到一个确认，限度增加一个分组；确认若还带窗口更新，可以响应发两个分组。如此继续直到窗口全开。慢启动阶段每个分组单独被确认的话，限度每轮交换翻倍，窗口指数打开。延迟确认可能使确认合并——多个分组在 200 毫秒内到达接收方时——让窗口开得稍慢。但发送方在打开阶段从不出发两三个以上分组的串，窗口打开后一次也只发一两个分组。

慢启动算法的实现用连接块中原供 4.3BSD 源熄灭处理的同一拥塞窗口字段 `snd_cwnd`。连接一停顿，该字段就设为一个分组的大小。对不共享网络上的目的地，任何连接都把它初始化为一个分组；因超时而停止传输时也设为一个分组。否则，丢弃的分组一旦被确认，随之而来的窗口更新可能允许发送一整窗口数据，再次冲垮沿途网关。重传超时后做慢启动，也免去了输出例程中“首次超时时限制只发一个分组”的测试。此外，超时可能表明网络因拥塞变慢，暂时缩小窗口有助于网络恢复。连接停顿后必须重建确认的时钟，慢启动也做到了这一点。

重传超时后使用慢启动，使连接无论是否收到表示丢包的源熄灭，都能对丢弃的分组做出正确反应。这一版 TCP 收到源熄灭的动作只是提前预期丢包导致的超时：把拥塞窗口设为一个分组。这一动作阻止新分组被发送，直到下次超时重发被丢的分组，届时慢启动重新开始。

**用慢启动避免拥塞**

TCP 加入慢启动算法后，连接可以按网络容忍的速率发送分组，达到“只有另一个分组离开网络才发分组”的稳态。单个连接可以合理使用更大的窗口，而不会在启动时淹没慢网络的入口网关。对某些路径，慢启动加大窗口能带来远超以往的佳绩：穿过卫星链路的路径固有延迟长，带宽也许不低，但吞吐被限制为每个往返时间一窗口数据。即便如此，源与卫星上行链路之间若有缓冲有限的网关（多数 ARPANET 网关如此），没有慢启动仍会被淹没。TCP 连接最好能自调谐，因为路径的特性在端点很少可知。长延迟链路在路径中时给得出好性能的窗口大小，在往返时间大头是排队延迟时会冲垮网络。连接慢启动打开窗口时向网络注入分组，直到网络链路被占满；再扩大窗口只会增大网关中的队列。把窗口开过全部传输链路都占满的这一点不增加吞吐，只增加连接经历的延迟。连接对某条路径扩张到过大的窗口、或网络的额外负载总计超出容量时，网关队列会涨到必须丢包。此时连接把拥塞窗口关到一个分组并发起慢启动；但若窗口对该路径就是太大，窗口一开过头这一过程就会重演。

连接可以从这个问题中学习，用与慢启动同时安装的另一个算法相应调整行为。该算法为每条连接保留一个新状态变量 `t_ssthresh`（慢启动阈值，slow-start threshold），即该路径可用窗口的估计。出现重传超时所表明的丢包时，窗口估计设为未决数据八位组数的一半——当前窗口显然太大，窗口使用的收缩必须大到拥塞得以减退而不是稳定。同时慢启动窗口（`snd_cwnd`）设为一个报文段重新开始。连接如前启动，指数打开窗口直到 `t_ssthresh` 限度；为试探网络的改善，继续慢慢扩大窗口——只要扩大成功，连接就能继续利用减轻的网络负载。这一阶段的扩大是线性的：每发送一整窗口数据，当前窗口加一个满尺寸报文段。缓慢增长让连接发现何时可以安全恢复使用更大窗口，同时减少丢包后等待恢复传输造成的吞吐损失。注意：连接在这一阶段只要不丢包，窗口增长是线性的；而拥塞迹象一出现，窗口缩减是指数的（每次超时减半）。用了这一动态窗口调整算法，就可以对所有目的地使用更大的默认窗口，又不会冲垮承载不起它们的网络。


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://book.bsdcn.org/4.3-bsd-unix-cao-zuo-xi-tong-she-ji-yu-shi-xian/di-si-bu-fen-jin-cheng-jian-tong-xin/12.7.-tcp-shu-chu-chu-li.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
