> 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.5.-tcp-suan-fa.md).

# 12.5 TCP 算法

介绍过 TCP、它的状态机和序号空间之后，可以开始考察 4.3BSD 中协议的实现。协议实现的若干方面是全局的：TCP 连接状态、输出状态和状态变化取决于外部事件和定时器。TCP 的处理响应三类事件之一：

1. 用户的请求，如发送数据、从套接字接收缓冲取走数据、打开或关闭连接
2. 连接收到一个分组
3. 一个定时器到期

这些事件分别由例程 `tcp_usrreq()`、`tcp_input()` 和 `tcp_timers()` 处理。每个例程处理当前事件并做连接状态所需的任何更改；然后，对任何可能需要输出的转换，调用 `tcp_output()` 例程做必要的输出。

发送带数据和/或控制信息的分组的判据很复杂，因此 TCP 发送策略是协议实现中最有趣也最重要的部分。例如，视连接的状态和流控参数，下列任何一项都可能使先前不能发送的数据得以发送：

* 用户的 send 调用把新数据放入发送队列
* 从对端 TCP 收到窗口更新
* 重传定时器到期
* 窗口更新定时器到期

此外，即使因这些原因都不允许发送数据，`tcp_output()` 例程也可能决定发送带控制信息的分组：

* 连接状态变化（如打开请求、关闭请求）
* 收到必须确认的数据
* 因数据从接收队列取走而引起的接收窗口变化
* 带紧急数据的发送请求
* 连接中止

描述过所涉状态和定时器之后，我们将更细地考虑这些决定中的多数。先从分布在整个代码中若干部分的定时、连接建立和关闭算法开始，再看新输入的处理和输出处理及算法的概述。

**定时器**

与 UDP 套接字不同，TCP 连接维护着可观的状态信息；因为有状态，有些操作必须异步做。例如数据可能因流控无法在进程呈交时立即发送。可靠交付的要求意味着数据首次传输后必须保留，以备必要时重传。为防分组丢失导致协议挂起，每条连接维护一组定时器，用于从对端 TCP 的丢失或故障中恢复。这些定时器存在连接的协议控制块中。设置之后，`tcp_slowtimo()` 例程（作为 TCP 协议开关的 `pr_slowtimo` 例程被调用）每 500 毫秒递减一次，直到到期触发对 `tcp_timers()` 的调用。

输出处理用两个定时器。一个是重传定时器（retransmit timer，TCPT\_REXMT）。连接上发送数据时，重传定时器即启动（除非已在运行）；全部未决数据确认后停止。定时器到期时，重发最旧的未确认数据（至多一个满尺寸分组），并以更大的值重启定时器。定时器值的增长率（计时器退避，timer backoff）由一张提供超时值近指数增长的多乘数表决定。

维持输出流的另一个定时器是持续定时器（persist timer，TCPT\_PERSIST）。它防范另一种可能使连接淤塞的分组丢失——允许发送更多数据的窗口更新的丢失。数据就绪可发、但发送窗口太小不值得发送（为零或小于合理量）、且无未决数据（重传定时器未设）时，启动持续定时器。定时器到期仍无窗口更新，输出例程就按窗口允许发尽可能大的报文段；大小为零时发送窗口探测报文（window probe，单个八位组的数据）并重启持续定时器。窗口更新若在网络中丢失、或接收方疏忽未发窗口更新，确认将含当前窗口信息；反之，接收方仍不能接受更多数据时，应回一个窗口仍关闭的确认。关闭的窗口可能无限期持续——例如接收方是个网络登录客户，用户停住终端输出吃午饭（或休假）去了。

TCP 的第三个定时器是保活定时器（keepalive timer，TCPT\_KEEP）。它监视这样的空闲连接：对端 TCP 可能因超时或崩溃已不复存在。套接字级选项置位且连接自上次保活超时以来一直空闲时，定时器例程发送保活报文（keepalive packet），图谋从对端 TCP 换来一个确认或一个复位（RST）。收到复位则关闭连接；数次尝试仍无响应则丢弃连接。这一设施让网络服务器免于客户不辞而别后永世凋零。保活报文不是 TCP 协议的显式特性：4.3BSD 为此用的分组把序号设为比 `snd_una` 小一，连接若仍存在，应能从对端 TCP 换来确认。

最后一个 TCP 定时器称为 2MSL 定时器（TCPT\_2MSL，“报文段最长生存期的两倍”）。TCP 在发送对一个 FIN 的确认而完成连接时（从 FIN\_WAIT\_2 或 CLOSING 态——发送侧已关闭）启动它。这些情况下发送方不知道确认是否被收到；FIN 若被重传，希望留有足够状态使确认可以重复。于是进入 TIME\_WAIT 状态时启动 2MSL 定时器，到期即删除控制块。为免这一延迟阻塞关闭连接的进程，任何进程关闭请求都立即成功返回，进程无须等定时器。因此，协议控制块在套接字描述符关闭之后仍可继续存在。此外，用户已关闭后进入 FIN\_WAIT\_2 状态时，4.3BSD 也启动 2MSL 定时器；连接空闲到定时器到期即关闭。用户既已关闭，这样的连接无论如何不能接受新数据。设置这个定时器是因为某些其他 TCP 实现（错误地）不在只收连接上发 FIN；与这类主机的连接除非系统超时，将永远停在 FIN\_WAIT\_2 状态。

除 TCP `tcp_slowtimo()` 例程实现的四个定时器外，TCP 还用协议开关的 `pr_fasttimo` 入口。每 200 毫秒调用的 `tcp_fasttimo()` 例程处理延迟确认请求，见 12.6 节。

**往返时间的估计**

连接必须穿过丢包的慢网络时，决定连接吞吐率的重要一环是重传定时器设置用的值。值太大，连接上的数据流会在丢弃的分组重发前停顿过久：还要等一个往返时间才能收到缺失报文段的确认和允许发送新数据的窗口更新。（运气好时只丢一个报文段，确认会包括已发出的其余报文段。）超时值太小则会无谓重传分组；网络变慢或丢包的原因若是拥塞，无谓重传只会火上浇油。TCP 对这一问题的传统解法是测量各个报文段收到确认所需的时间，为连接路径估计往返时间 `rtt`；往返时间的估计按平滑移动平均 `srtt` 维护 \[Postel, 1981b]：

*srtt* = (ALPHA · *srtt*) + ((1−ALPHA) · *rtt*)

初始重传超时设为当前平滑往返时间的倍数（BETA）。标准 4.3BSD 磁带发行版中的 TCP 版本用这一方案，平滑因子 ALPHA 取 0.9（保留前次平均的 90%），方差因子 BETA 取 2。

4.3BSD Tahoe 发行版包含一个用了更精细算法的新版 TCP：除平滑的往返时间估计外，还维护平滑方差（用平均偏差估计，免得内核做开方运算）。ALPHA 取 0.875，方差的相应平滑因子取 0.75。这些值部分是为了让平滑平均可以用定点值的移位运算计算，而不再像早先系统那样用浮点（许多体系结构上，在中断例程里用浮点运算代价高昂——那会迫使浮点寄存器和状态被保存恢复）。初始重传超时设为当前平滑往返时间加两倍平滑方差。新算法在延迟长而延迟方差小的路径（如卫星链路）上明显更好，因为它动态计算 BETA 因子 \[Jacobson, 1988]。

**连接建立**

建立新的 TCP 连接有两条途径：主动连接由 `connect` 调用发起，被动连接在监听套接字收到连接请求时创建。依次考虑。

主动连接尝试的初始步骤与创建 UDP 套接字的动作相似：进程创建新套接字，导致带 PRU\_ATTACH 请求调用 `tcp_usrreq()`。TCP 像 UDP 一样创建 `inpcb` 协议控制块，然后按 12.1 节的描述创建附加控制块（`tcpcb` 结构）。`tcpcb` 中的部分流控参数此时初始化。进程显式绑定地址或端口号到连接时，动作与 UDP 套接字相同。随后 `connect` 调用发起真正的连接：第一步照 UDP 的做法用 `in_pcbconnect()` 建立关联；为构造每个输出分组创建一个分组头模板；从序号原型选一个初始序号，原型已推进了相当数量；套接字经 `soisconnecting()` 标记，TCP 连接状态设为 TCPS\_SYN\_SENT，保活定时器设为 45 秒以限制连接尝试的时长，然后第一次调用 `tcp_output()`。

输出处理模块 `tcp_output()` 用一个按连接状态索引的分组控制标志数组，确定各状态应发送哪些控制标志。TCPS\_SYN\_SENT 状态下发送 SYN 标志。因为有控制标志要发，分组用刚构造的模板和当前流控参数立即发出。目的地在本地网络（或其子网，见 12.1 节）时，这个分组发送最大报文段大小选项，指定一个小于外出接口最大传输单位的 512 字节的倍数；选项不发时，对方发送的分组不得大于默认值（含头部 576 个八位组）。重传定时器设为默认值（6 秒），因为尚无往返时间信息。

运气好的话，连接目标的响应分组会在重传定时器到期前到达；否则重传分组、以更大的值重启重传定时器。保活定时器到期仍无响应，连接尝试以“连接超时”错误中止。收到响应时，检查它与外发请求是否吻合：它应确认所发 SYN 并含一个 SYN。两者都对的话，接收序号变量初始化，连接状态推进到 TCPS\_ESTABLISHED。响应中如有最大报文段大小选项，连接的最大报文段大小取提供值与外出接口最大传输单位的较小者；选项缺席则记默认大小（512 数据字节）。调用输出例程前在 TCP 控制块置 TF\_ACKNOW 标志，使 SYN 立即得到确认。连接现在可以传数据了。

被动打开创建连接时的事件不同。照旧创建套接字并绑定地址，套接字被 `listen` 调用标记为愿意接受连接。处于 TCPS\_LISTEN 状态的 TCP 套接字有分组到达时，用 `sonewconn()` 创建新套接字——它调用 TCP 的 PRU\_ATTACH 请求为新套接字创建协议控制块。新套接字放上监听套接字领衔的部分连接队列。分组含 SYN 且别处都合格时，绑定新套接字的关联，初始化发送和接收序号，连接状态推进到 TCPS\_SYN\_RECEIVED。照旧设置保活定时器，置 TF\_ACKNOW 后调用输出例程强制确认 SYN，同时外发一个 SYN。这个 SYN 得到妥善确认后，新套接字从部分连接队列移到已完成连接队列。监听套接字的主人若正睡在 `accept` 调用中或做了 `select`，套接字会告知有新连接可用。套接字终于可以发送数据了。到 `accept` 调用完成时，可能已经收到并确认了至多一个窗口的数据。

**连接关闭**

TCP 连接是对称的全双工，任一侧都可独立发起断开。只要连接的一个方向还能承载，连接就保持打开。套接字可以用 `shutdown` 系统调用表示数据已发送完毕，这导致带 PRU\_SHUTDOWN 请求调用 `tcp_usrreq()` 例程：推进连接状态——从 ESTABLISHED 态变为 FIN\_WAIT\_1。随后的输出调用将发送一个 FIN，表示文件结束。接收方套接字推进到 CLOSE\_WAIT，但可以继续发送。进程径直关闭套接字时过程可能不同：FIN 立即发出，但再收到的新数据无法投递。正常情况下更高层协议会结清自己的事务，双方都知道何时关闭。若不这样，TCP 必须拒绝新数据：用户关闭后再收到新数据，就发一个带 RST 的分组。`close` 时套接字发送缓冲中仍有数据的话，TCP 通常会尝试投递。套接字选项 SO\_LINGER 设了零逗留时间则径直冲掉发送缓冲；否则允许用户进程继续，协议等待投递了结。这些情况下套接字标记 SS\_NOFDREF 状态位（无文件描述符引用）。数据传输的完成和最终关闭可以在任意长时间之后。TCP 最终完成连接（或因超时或其他失败而放弃）时调用 `tcp_close()`：此时释放协议控制块和其他动态分配的结构；SS\_NOFDREF 标志置位的话，套接字也随之释放。于是，只要文件描述符或协议控制块还有一方引用套接字，它就继续存在。


---

# 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.5.-tcp-suan-fa.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.
