Appearance
TCP 拥塞控制:慢开始、拥塞避免、快重传、快恢复
概念
拥塞控制(congestion control)是发送方为了"不让网络里的路由器与链路被打爆"而主动限制自己发送速率的机制——它靠维护一个自己的状态变量——拥塞窗口 cwnd(congestion window)——并把"实际能发多少"定为
★ 与流量控制的分工(两句话记牢):流量控制是"接收方喊停"(怕自己的缓存被撑爆,点对点,依据 rwnd);拥塞控制是"发送方自省"(怕网络被压垮,全局,依据 cwnd)。 两者同时生效,谁小谁说话。
★ "慢开始"这个名字是最大的误导:它指的不是"增长慢",而是"从很小的地方开始"——cwnd 从 1 个 MSS 起步;它的增长方式恰恰是四个算法里最快的(每 RTT 翻倍,指数增长)。 考试里"慢开始慢在哪"的标准答法是"起点低(cwnd 初值为 1 个 MSS),而不是增速慢"。
与上一章的衔接:上一章(net/43-flow.md)说"接收方通过窗口字段画一条不能越过的线"——本章是"发送方自己再画一条更保守的线",两条线取小才是真正能发的量。 上一章末尾"1 Gbps 需要 5 MB 窗口"的账,也正是拥塞控制要解决的"拥塞崩溃"问题的背景。
原理
一、三个变量与两条增长律
★ 三个变量(必背):
| 变量 | 单位 | 含义 | 谁在调 |
|---|---|---|---|
| cwnd | MSS(报文段) | 拥塞窗口:发送方估计"网络还能承受多少" | 发送方自己 |
| ssthresh | MSS | 慢开始门限:cwnd 到这个值就从"翻倍"改成"加 1" | 发生拥塞时被改写 |
| rwnd | 字节 | 接收窗口:接收方通告的缓存空闲量(见 net/43-flow.md) | 接收方 |
⚠️ 单位陷阱:cwnd 与 ssthresh 的单位是"报文段个数(MSS)",rwnd 的单位是"字节"。 两者比较时要么都换成字节(乘以 MSS),要么题目自己说清"cwnd = 12"指的是 12 个 MSS。 本章统一用"个 MSS"作 cwnd 的单位。
★ 两条增长律:
| 阶段 | 触发条件 | 增长方式 | 形状 |
|---|---|---|---|
| 慢开始(slow start) | cwnd < ssthresh | 每经过 1 个 RTT,cwnd 翻倍(收到几个 ACK 就加几个 MSS,效果是翻倍) | 指数 |
| 拥塞避免(congestion avoidance) | cwnd ≥ ssthresh | 每经过 1 个 RTT,cwnd 加 1 | 线性 |
★ 切换点是"相等"那一刻:当 cwnd 增长到等于 ssthresh 时,立即从慢开始转入拥塞避免——所以第 5 轮 cwnd = 16 = ssthresh 那一轮,是慢开始的最后一轮。
★ 由"指数 vs 线性"直接推出的一条结论:cwnd 从 1 涨到 16 只要 4 个 RTT(
二、两种拥塞信号,两种反应
★ 发送方只有两个可观测的信号——它看不到路由器内部,只能从"确认"上推断:
| 信号 | 现象 | 推断 | 反应 |
|---|---|---|---|
| 超时(RTO 到期仍未确认) | 一个报文都没有被确认 | 网络堵得很厉害(可能连续丢包) | |
| 3 个重复 ACK | 后面的报文到了,只是中间缺一段 | 轻度拥塞(零星丢包) | 快重传 + 快恢复: |
★ "乘性减、加性增"(AIMD,Additive Increase Multiplicative Decrease):
text
★ 加性增: 拥塞避免阶段每 RTT 只加 1 -> 慢慢试探上限
★ 乘性减: 一旦拥塞就把 ssthresh 砍一半 -> 立刻退让
-> 结果: cwnd 走出一条"锯齿"曲线 (缓慢上升 -> 陡降 -> 再缓慢上升)
cwnd
│ /\ /\ /\
│ / \ / \ / \
│ / \ / \ / \ <- 每次"减半" = 乘性减
│ / \ / \ / \
│ / \ / \ / \
└───┴──────────┴──────────┴──────────┴───► 时间
锯齿的上升段 = 拥塞避免, 下降段 = 一次拥塞事件★ 四个算法的完整分工(必背表):
| 算法 | 什么时候用 | 干什么 |
|---|---|---|
| 慢开始 | 连接开始 / 超时之后 | cwnd 从 1 起步,每 RTT 翻倍,直到 ssthresh |
| 拥塞避免 | cwnd ≥ ssthresh | 每 RTT 加 1 |
| 快重传 | 收到 3 个重复 ACK | 不等超时,立刻重传那个缺失的报文段 |
| 快恢复 | 快重传之后 |
⚠️ 快重传与快恢复是"一对":快重传解决"赶紧补上丢的那段";快恢复解决"补完之后 cwnd 定多大"。 前三个算法(慢开始、拥塞避免、快重传)是 RFC 5681 的标准组成;快恢复是 Reno 版实现,与 Tahoe 版的区别就在这里(见第四节)。
三、完整演化:锚点 27 轮
★ 锚点(本章定死):ssthresh 初值 = 16(个 MSS)、cwnd 从 1 起、cwnd 涨到 24 时发生超时、之后涨到 16 时收到 3 个重复 ACK。
text
阶段 轮次 cwnd 轨迹 事件
─────────────────────────────────────────────────────────────────
慢开始 1 ~ 5 1 2 4 8 16 到 16 = ssthresh, 转下一阶段
拥塞避免 6 ~ 13 17 18 ... 24 ★ cwnd = 24 时超时
★ 超时 14 1 ssthresh := 24/2 = 12
慢开始 15 ~ 18 2 4 8 12 到 12 = ssthresh
拥塞避免 19 ~ 22 13 14 15 16 ★ cwnd = 16 时收到 3 个重复 ACK
★ 快重传+快恢复 23 8 ssthresh := 16/2 = 8, cwnd := 8
拥塞避免 24 ~ 27 9 10 11 12 线性增长
─────────────────────────────────────────────────────────────────
★ 全程 27 轮, 累计发送 330 个 MSS 段
★ ssthresh 三值: 16 -> 12 -> 8★ 三个"最容易算错"的地方:
| 易错点 | 正确做法 |
|---|---|
| 超时那一轮的 cwnd 记成 24 | 超时是"事件",处理完 cwnd 就变成 1——画曲线时要画到 24 再"断崖"到 1 |
| 3 个重复 ACK 之后 cwnd 回到 1 | 错。快恢复让 cwnd = ssthresh = 8,直接从 8 开始线性增长 |
| 把 ssthresh 写成新 cwnd 的一半 | 注意顺序: |
示例
例 1:C 实现——27 轮 cwnd 演化轨迹
参数:ssthresh 初值 16;慢开始起点 cwnd = 1;超时发生在 cwnd = 24;3 个重复 ACK 发生在 cwnd = 16。
#include <stdio.h>
int main(void) {
int cwnd = 1, ssthresh = 16, round = 0, total = 0;
printf("(1) cwnd 演化 (单位 = MSS; ssthresh 初值 = 16)\n");
printf(" %-6s %-16s %-6s %-9s %s\n", "round", "event", "cwnd", "ssthresh", "sent");
/* 阶段 1: 慢开始, cwnd 每轮翻倍, 直到等于 ssthresh */
while (cwnd <= ssthresh) {
round++; total += cwnd;
printf(" %-6d %-16s %-6d %-9d %d\n", round, "slow-start", cwnd, ssthresh, cwnd);
if (cwnd == ssthresh) break;
cwnd *= 2;
}
/* 阶段 2: 拥塞避免, 每轮加 1, 直到 cwnd = 24 (此刻超时) */
while (cwnd < 24) {
cwnd++; round++; total += cwnd;
printf(" %-6d %-16s %-6d %-9d %d\n", round, "avoidance", cwnd, ssthresh, cwnd);
}
/* 阶段 3: 超时 -> ssthresh 减半, cwnd 回 1 */
ssthresh = cwnd / 2; cwnd = 1; round++; total += cwnd;
printf(" %-6d %-16s %-6d %-9d %d\n", round, "timeout", cwnd, ssthresh, cwnd);
/* 阶段 4: 再次慢开始, 到 ssthresh = 12 为止 (翻倍但不超过 ssthresh) */
while (cwnd < ssthresh) {
cwnd = (cwnd * 2 > ssthresh) ? ssthresh : cwnd * 2;
round++; total += cwnd;
printf(" %-6d %-16s %-6d %-9d %d\n", round, "slow-start", cwnd, ssthresh, cwnd);
}
/* 阶段 5: 拥塞避免到 cwnd = 16 (此刻收到 3 个重复 ACK) */
while (cwnd < 16) {
cwnd++; round++; total += cwnd;
printf(" %-6d %-16s %-6d %-9d %d\n", round, "avoidance", cwnd, ssthresh, cwnd);
}
/* 阶段 6: 3 个重复 ACK -> 快重传 + 快恢复 */
ssthresh = cwnd / 2; cwnd = ssthresh; round++; total += cwnd;
printf(" %-6d %-16s %-6d %-9d %d\n", round, "fast-recovery", cwnd, ssthresh, cwnd);
/* 阶段 7: 拥塞避免继续线性增长 */
while (cwnd < 12) {
cwnd++; round++; total += cwnd;
printf(" %-6d %-16s %-6d %-9d %d\n", round, "avoidance", cwnd, ssthresh, cwnd);
}
printf("\n");
printf("(2) 小计\n");
printf(" 共 %d 轮; 累计发送 %d 个 MSS 段\n", round, total);
printf(" ssthresh: 16 -> 12 (超时) -> 8 (3 个重复 ACK)\n");
printf(" ★ 超时: cwnd = 1 (回到慢开始)\n");
printf(" ★ 3 个重复 ACK: cwnd = ssthresh = 8 (快恢复, 不回 1)\n");
printf("\n");
printf("(3) 两条增长律\n");
printf(" 慢开始: 每个 RTT 翻倍 (1 2 4 8 16)\n");
printf(" 拥塞避免: 每个 RTT 加 1 (17 18 ... 24)\n");
printf(" ★ 慢开始并不慢: 起点低, 但增长是指数的\n");
printf("\n");
printf("(4) Tahoe 与 Reno\n");
printf(" Tahoe: 超时或 3 个重复 ACK -> cwnd = 1\n");
printf(" Reno : 超时 -> cwnd = 1; 3 个重复 ACK -> cwnd = ssthresh (快恢复)\n");
return 0;
}
c 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
预期输出:
text
(1) cwnd 演化 (单位 = MSS; ssthresh 初值 = 16)
round event cwnd ssthresh sent
1 slow-start 1 16 1
2 slow-start 2 16 2
3 slow-start 4 16 4
4 slow-start 8 16 8
5 slow-start 16 16 16
6 avoidance 17 16 17
7 avoidance 18 16 18
8 avoidance 19 16 19
9 avoidance 20 16 20
10 avoidance 21 16 21
11 avoidance 22 16 22
12 avoidance 23 16 23
13 avoidance 24 16 24
14 timeout 1 12 1
15 slow-start 2 12 2
16 slow-start 4 12 4
17 slow-start 8 12 8
18 slow-start 12 12 12
19 avoidance 13 12 13
20 avoidance 14 12 14
21 avoidance 15 12 15
22 avoidance 16 12 16
23 fast-recovery 8 8 8
24 avoidance 9 8 9
25 avoidance 10 8 10
26 avoidance 11 8 11
27 avoidance 12 8 12
(2) 小计
共 27 轮; 累计发送 330 个 MSS 段
ssthresh: 16 -> 12 (超时) -> 8 (3 个重复 ACK)
★ 超时: cwnd = 1 (回到慢开始)
★ 3 个重复 ACK: cwnd = ssthresh = 8 (快恢复, 不回 1)
(3) 两条增长律
慢开始: 每个 RTT 翻倍 (1 2 4 8 16)
拥塞避免: 每个 RTT 加 1 (17 18 ... 24)
★ 慢开始并不慢: 起点低, 但增长是指数的
(4) Tahoe 与 Reno
Tahoe: 超时或 3 个重复 ACK -> cwnd = 1
Reno : 超时 -> cwnd = 1; 3 个重复 ACK -> cwnd = ssthresh (快恢复)⚠️ 三点说明:
- 本机无 C 编译器:此段代码逐行人工审查,并用等价的 Python 实现实跑核对,输出逐字一致。
sent列的数值恰好等于cwnd:因为"本轮的 cwnd 是几个 MSS,本轮就能发出几个报文段"——把这一列加起来就是"到那一刻为止一共发了多少段"(27 轮共 330 段)。- 第 14 轮与第 23 轮都是"事件轮":它们的 cwnd 是"事件处理之后"的值(1 与 8)——画曲线时要把"事件前的 cwnd"(24 与 16)也标出来,否则锯齿形状画不对。
例 2:Python——锯齿曲线、Tahoe 对照与 AIMD 周期
def pad(s, w):
"""按"显示宽度"补空格: 汉字算 2 列, 否则终端里对不齐"""
return s + ' ' * max(0, w - sum(2 if ord(c) > 0x2000 else 1 for c in s))
MSS = 1460
def simulate(tahoe=False):
"""返回 [(round, event, cwnd, ssthresh)]; tahoe=True 时 3 个重复 ACK 也回 cwnd = 1"""
rows = []
cwnd, ssthresh, r = 1, 16, 0
while cwnd <= ssthresh:
r += 1; rows.append((r, 'slow-start', cwnd, ssthresh))
if cwnd == ssthresh:
break
cwnd *= 2
while cwnd < 24:
cwnd += 1; r += 1; rows.append((r, 'avoidance', cwnd, ssthresh))
ssthresh = cwnd // 2; cwnd = 1; r += 1
rows.append((r, 'timeout', cwnd, ssthresh))
while cwnd < ssthresh:
cwnd = min(cwnd * 2, ssthresh); r += 1
rows.append((r, 'slow-start', cwnd, ssthresh))
while cwnd < 16:
cwnd += 1; r += 1; rows.append((r, 'avoidance', cwnd, ssthresh))
ssthresh = cwnd // 2
if tahoe:
cwnd = 1; r += 1; rows.append((r, '3dupACK->slow-start', cwnd, ssthresh))
else:
cwnd = ssthresh; r += 1; rows.append((r, '3dupACK->fast-recovery', cwnd, ssthresh))
while cwnd < 12:
cwnd += 1; r += 1; rows.append((r, 'avoidance', cwnd, ssthresh))
return rows
rows = simulate(False)
print('=== ① cwnd 轨迹与锯齿 (Reno) ===')
print(' ' + pad('轮次', 6) + pad('cwnd', 6) + pad('ssthresh', 10) + '曲线 (每格 1 个 MSS)')
last = None
for r, ev, c, s in rows:
mark = ''
if ev.startswith('timeout'):
mark = ' <<< 超时, 减半回 1'
elif ev.startswith('3dupACK'):
mark = ' <<< 3 个重复 ACK -> 快恢复'
bar = '#' * c
if ev in ('timeout', '3dupACK->fast-recovery'):
last = c
print(' ' + pad(str(r), 6) + pad(str(c), 6) + pad(str(s), 10) + bar + mark)
print(' ★ 上升段平缓 (每轮 +1), 事件处陡降 (减半) -> AIMD 的锯齿')
print()
print('=== ② 三个阶段的分段统计 ===')
seg = [('慢开始 1~5', 5, '1 2 4 8 16'), ('拥塞避免 6~13', 8, '17 ... 24'),
('慢开始 15~18', 4, '2 4 8 12'), ('拥塞避免 19~22', 4, '13 14 15 16'),
('拥塞避免 24~27', 4, '9 10 11 12')]
print(' ' + pad('阶段', 18) + pad('轮数', 8) + 'cwnd 轨迹')
for a, n, t in seg:
print(' ' + pad(a, 18) + pad(str(n), 8) + t)
print(' ★ 慢开始 4 轮就从 1 涨到 16; 拥塞避免涨 8 才从 16 到 24')
print(' ★ 全程 %d 轮, 累计发送 %d 个 MSS 段' % (len(rows), sum(c for _, _, c, _ in rows)))
print()
print('=== ③ 两种拥塞信号的处理对照 ===')
sig = [('超时 (RTO 到期)', 'cwnd 减半', 'cwnd = 1, 重新慢开始', '回得太狠, 但最安全'),
('3 个重复 ACK', 'cwnd 减半', 'cwnd = ssthresh (Reno 快恢复), 直接拥塞避免', '少一次慢开始')]
print(' ' + pad('信号', 18) + pad('ssthresh', 12) + pad('cwnd 处理', 40) + '说明')
for a, b, c, d in sig:
print(' ' + pad(a, 18) + pad(b, 12) + pad(c, 40) + d)
print(' ★ 修改 ssthresh 的规则同一条: ssthresh := 拥塞发生时的 cwnd / 2')
print()
print('=== ④ Tahoe 与 Reno 的差别 ===')
t = simulate(True)
r_ = simulate(False)
print(' ' + pad('版本', 10) + pad('3 个重复 ACK 后 cwnd', 24) + pad('总轮数', 10) + '最终 cwnd')
print(' ' + pad('Tahoe', 10) + pad('1', 24) + pad(str(len(t)), 10) + str(t[-1][2]))
print(' ' + pad('Reno', 10) + pad(str(8), 24) + pad(str(len(r_)), 10) + str(r_[-1][2]))
print(' ★ Tahoe 多花 %d 轮才回到 cwnd = 12; Reno 靠快恢复省掉这次慢开始' % (len(t) - len(r_)))
print()
print('=== ⑤ 快重传的报文序列 (段 3 丢失) ===')
want = 201
print(' 发送: 段1[1,100] 段2[101,200] 段3[201,300] 段4[301,400] 段5[401,500]')
print(' ' + pad('收到', 8) + pad('连续?', 10) + pad('回 ACK', 10) + '计数')
dup = 0
for nm, ok in [('段1', True), ('段2', True), ('段4', False), ('段5', False), ('段6', False)]:
if ok:
note = '正常'
else:
dup += 1
note = '重复 ACK %d' % dup
print(' ' + pad(nm, 8) + pad('连续' if ok else '不连续', 10) + pad(str(want), 10) + note)
print(' ★ 段3 丢失, 但段4/5/6 都到了 -> 连回 3 个 ACK = 201')
print(' ★ 发送方数到第 3 个重复 ACK -> 立刻重传段3, 同时 ssthresh := cwnd/2, cwnd := ssthresh')
print()
print('=== ⑥ cwnd 与 rwnd 的单位换算与取小 (MSS = %d B) ===' % MSS)
print(' ' + pad('cwnd', 8) + pad('cwnd(B)', 12) + pad('rwnd(B)', 12)
+ pad('发送窗口(B)', 14) + '瓶颈')
for c, r in [(24, 65535), (12, 3000), (8, 65), (40, 65535)]:
cb = c * MSS
if cb < r:
owner = '网络 (cwnd 小)'
elif r < cb:
owner = '接收方 (rwnd 小)'
else:
owner = '持平'
print(' ' + pad(str(c), 8) + pad(str(cb), 12) + pad(str(r), 12)
+ pad(str(min(cb, r)), 14) + owner)
print(' ★ 单位不同是本题型最大的坑: cwnd 数"段", rwnd 数"字节"')
python 本站为静态站,不提供在线运行;可复制到本地用 gcc / python 执行
输出对照(真实运行结果):
=== ① cwnd 轨迹与锯齿 (Reno) ===
轮次 cwnd ssthresh 曲线 (每格 1 个 MSS)
1 1 16 #
2 2 16 ##
3 4 16 ####
4 8 16 ########
5 16 16 ################
6 17 16 #################
7 18 16 ##################
8 19 16 ###################
9 20 16 ####################
10 21 16 #####################
11 22 16 ######################
12 23 16 #######################
13 24 16 ########################
14 1 12 # <<< 超时, 减半回 1
15 2 12 ##
16 4 12 ####
17 8 12 ########
18 12 12 ############
19 13 12 #############
20 14 12 ##############
21 15 12 ###############
22 16 12 ################
23 8 8 ######## <<< 3 个重复 ACK -> 快恢复
24 9 8 #########
25 10 8 ##########
26 11 8 ###########
27 12 8 ############
★ 上升段平缓 (每轮 +1), 事件处陡降 (减半) -> AIMD 的锯齿
=== ② 三个阶段的分段统计 ===
阶段 轮数 cwnd 轨迹
慢开始 1~5 5 1 2 4 8 16
拥塞避免 6~13 8 17 ... 24
慢开始 15~18 4 2 4 8 12
拥塞避免 19~22 4 13 14 15 16
拥塞避免 24~27 4 9 10 11 12
★ 慢开始 4 轮就从 1 涨到 16; 拥塞避免涨 8 才从 16 到 24
★ 全程 27 轮, 累计发送 330 个 MSS 段
=== ③ 两种拥塞信号的处理对照 ===
信号 ssthresh cwnd 处理 说明
超时 (RTO 到期) cwnd 减半 cwnd = 1, 重新慢开始 回得太狠, 但最安全
3 个重复 ACK cwnd 减半 cwnd = ssthresh (Reno 快恢复), 直接拥塞避免少一次慢开始
★ 修改 ssthresh 的规则同一条: ssthresh := 拥塞发生时的 cwnd / 2
=== ④ Tahoe 与 Reno 的差别 ===
版本 3 个重复 ACK 后 cwnd 总轮数 最终 cwnd
Tahoe 1 34 12
Reno 8 27 12
★ Tahoe 多花 7 轮才回到 cwnd = 12; Reno 靠快恢复省掉这次慢开始
=== ⑤ 快重传的报文序列 (段 3 丢失) ===
发送: 段1[1,100] 段2[101,200] 段3[201,300] 段4[301,400] 段5[401,500]
收到 连续? 回 ACK 计数
段1 连续 201 正常
段2 连续 201 正常
段4 不连续 201 重复 ACK 1
段5 不连续 201 重复 ACK 2
段6 不连续 201 重复 ACK 3
★ 段3 丢失, 但段4/5/6 都到了 -> 连回 3 个 ACK = 201
★ 发送方数到第 3 个重复 ACK -> 立刻重传段3, 同时 ssthresh := cwnd/2, cwnd := ssthresh
=== ⑥ cwnd 与 rwnd 的单位换算与取小 (MSS = 1460 B) ===
cwnd cwnd(B) rwnd(B) 发送窗口(B) 瓶颈
24 35040 65535 35040 网络 (cwnd 小)
12 17520 3000 3000 接收方 (rwnd 小)
8 11680 65 65 接收方 (rwnd 小)
40 58400 65535 58400 网络 (cwnd 小)
★ 单位不同是本题型最大的坑: cwnd 数"段", rwnd 数"字节"五条结论:
- ★ 慢开始"起点低、增长快":cwnd 从 1 起,每 RTT 翻倍,4 个 RTT 就到 16——它并不慢,只是不敢一开始就发一大片。
- ★ 拥塞避免是线性试探:每 RTT 只加 1——从 16 到 24 花了 8 个 RTT,比慢开始那 5 轮慢得多。
- ★ 两种信号、两种反应:超时 →
、 (重新慢开始);3 个重复 ACK → 、 (快恢复,直接拥塞避免)。 - ★ 锚点全程 27 轮、累计 330 个 MSS 段;ssthresh 依次为 16 → 12 → 8——每一次拥塞事件都把它减半,这就是"乘性减"。
- ★ Tahoe 与 Reno 的分水岭就是"3 个重复 ACK":Tahoe 让 cwnd 回到 1(多一次慢开始);Reno 用快恢复把 cwnd 定在 ssthresh,省掉这一段。
考点
考点
1. 必背结论
- ★★ 发送窗口
:流量控制看 rwnd(接收方),拥塞控制看 cwnd(发送方)。 - ★ cwnd 与 ssthresh 的单位是 MSS(报文段个数);rwnd 的单位是字节——比较前必须换算。
- ★★ 慢开始:cwnd 初值 1 个 MSS,每经过 1 个 RTT 翻倍(指数增长)。
- ★★ 拥塞避免:cwnd ≥ ssthresh 后转入,每经过 1 个 RTT 加 1(线性增长)。
- ★★ 切换点:cwnd 增长到等于 ssthresh 时,从慢开始转入拥塞避免。
- ★★ "慢开始并不慢":它指"起点低"(从 1 个 MSS 开始),而不是增长慢——实际是指数增长,比拥塞避免快得多。
- ★★ 超时的反应:
, ,重新进入慢开始(Tahoe 与 Reno 在这一点上相同)。 - ★★ 3 个重复 ACK 的反应:快重传(立即重传缺失段)+ 快恢复(
, ,直接进拥塞避免)。 - ★ 快重传的门槛是"3 个"重复 ACK(用来排除乱序造成的假丢失)。
- ★ AIMD:加性增(每 RTT +1)、乘性减(减半) → cwnd 走出锯齿曲线。
- ★★ 锚点轨迹:ssthresh 初值 16 → cwnd
→ 拥塞避免 → 超时(ssthresh 12、cwnd 1)→ → → 3 个重复 ACK(ssthresh 8、cwnd 8)→ ;共 27 轮、累计 330 个 MSS 段。 - ★ ssthresh 三条历史值:16 → 12 → 8;每次都用"拥塞发生时的 cwnd"除以 2。
- ★ Tahoe vs Reno:Tahoe 在超时与 3 个重复 ACK 时都令 cwnd = 1;Reno 只在超时时令 cwnd = 1,3 个重复 ACK 时用快恢复。
- ★ 四个算法的触发顺序:连接建立 → 慢开始 → (到 ssthresh)拥塞避免 → (丢包)快重传/快恢复 或 超时 → 重来。
- ★ 快重传与快恢复是一对:前者补数据,后者定 cwnd。
2. 高频陷阱
- 把慢开始理解成"增长慢":错,这是最经典的误读。慢开始是指数增长;"慢"说的是起点低。
- 把慢开始的"翻倍"记成"加 1":错。翻倍发生在慢开始,加 1 发生在拥塞避免——两者正好互换就全错。
- 忘记"cwnd = ssthresh 那一轮属于慢开始":要记清。锚点里 cwnd = 16 那一轮仍是慢开始,第 6 轮才开始拥塞避免。
- 超时后把 ssthresh 算成新 cwnd 的一半:错。要用"拥塞发生时的 cwnd"除以 2——锚点是 24/2 = 12。
- 认为"超时后 cwnd 减半":错。超时后 cwnd 直接回 1;减半的是 ssthresh。
- 认为"3 个重复 ACK 后 cwnd 也回 1":错(Reno 口径)。cwnd = ssthresh,正是快恢复的意义所在。
- 把 ssthresh 减半的对象搞错:是 cwnd 减半赋给 ssthresh,不是 ssthresh 自身减半。
- 认为"重传就要回到慢开始":不准确。只有超时(RTO)才回慢开始;3 个重复 ACK 走快恢复。
- 把"重复 ACK"与"确认号相同的正常 ACK"混淆:要分清。重复 ACK 指"收到不连续的报文段,仍回同一个确认号"——这些 ACK 的确认号与上一次完全相同。
- 把快重传的门槛记成 2 个或 4 个:错。标准是 3 个。
- 用 rwnd 的单位去算 cwnd:错。cwnd 是"段数",要乘 MSS 才是字节。
- 认为"拥塞控制也由接收方决定":错。cwnd 是发送方自己维护的估计值,接收方根本不知道它。
- 把 AIMD 的"A"记成"减半":要分清。A = Additive(加性增,每 RTT +1);M = Multiplicative(乘性减,减半)。
- 认为"cwnd 越大越好":错。cwnd 过大正是拥塞崩溃的成因——它是在"试探上限"与"及时退让"之间找平衡。
3. 解题模板("拥塞控制曲线题")
① 画曲线的五步:
1) cwnd = 1 起步, 以 ssthresh 为界
2) cwnd < ssthresh -> 每轮翻倍; 到 ssthresh 那一轮算慢开始
3) cwnd >= ssthresh -> 每轮 +1
4) 遇到"超时" -> ssthresh := 当前 cwnd / 2, cwnd := 1 (曲线断崖到 1)
5) 遇到"3 个重复 ACK" -> ssthresh := 当前 cwnd / 2, cwnd := ssthresh
(曲线只降到一半, 不进慢开始)
② 求"第 N 轮 cwnd 是多少":
先从 1 翻倍数到 ssthresh (共 log2(ssthresh) 轮),
之后每轮 +1; 遇到事件就按上面的规则重置
③ 求"到第 N 轮一共发了多少段":
把每一轮的 cwnd 相加 (本轮的 cwnd = 本轮发的段数)
锚点: 27 轮共 330 段
④ 求 ssthresh 历史:
每发生一次拥塞就记一个值, 规则固定为"当时 cwnd 的一半"
锚点: 16 -> 12 -> 8
⑤ 判断瓶颈:
发送窗口 = min(rwnd, cwnd); 谁小谁就是瓶颈
★ 单位统一: cwnd x MSS 与 rwnd 比4. 与相邻章节的接口
net/43-flow.md(流量控制):本章的 cwnd 与上一章的 rwnd 取小才是真正能发的量;上一章的"零窗口 + 持续计时器"与本章的"拥塞退让"是两条独立并行的控制环。net/41-tcp.md(TCP 报文段):"3 个重复 ACK"的判据来自上一章的累积确认——正因为确认号只报"连续前缀",缺一段才会反复回同一个 ACK,快重传才有信号可用。net/42-handshake.md(连接管理):新建连接时 cwnd 从 1 个 MSS 起(初始窗口)——这解释了"为什么短连接的前几个报文都很小";TIME_WAIT 与 cwnd 无关,别混。net/22-window.md(链路层滑动窗口):GBN 与 SR 是"链路层怎么重传",本章是"传输层怎么重传 + 怎么限速"——两者都建立在滑动窗口上,但控制目标不同。net/33-routing.md(路由协议):拥塞控制是"端系统自救",路由器本身的队列管理(如 RED、ECN)是"网络侧配合"——本章只讲端系统这一半。os/24-thrashing.md(抖动):两者都是"系统在过载时性能崩塌"的现象——计算机的抖动靠"减少并发度(挂起进程)"缓解,网络的拥塞靠"减少注入量(收缩 cwnd)"缓解,思路完全一样。
小结
- ★ 拥塞控制是发送方自己的事:维护 cwnd,并把发送窗口定为
。 - ★★ 两条增长律:慢开始每 RTT 翻倍(起点 1 个 MSS);拥塞避免每 RTT 加 1;cwnd = ssthresh 时切换。
- ★★ "慢开始并不慢":它指起点低,增长是指数的——4 轮就到 16。
- ★★ 两种信号两种反应:超时 →
、 ;3 个重复 ACK → 、 (快恢复)。 - ★ 快重传门槛是 3 个重复 ACK;快重传补数据、快恢复定 cwnd。
- ★★ 锚点全程:
→ → 超时(12、1)→ → → 3 个重复 ACK(8、8)→ ;27 轮、330 段、ssthresh 16 → 12 → 8。 - ★ AIMD 走出锯齿;Tahoe 与 Reno 的分水岭就是"3 个重复 ACK 后 cwnd 回 1 还是回 ssthresh"。
- ★ 单位陷阱:cwnd 数"段"、rwnd 数"字节"。
评论(0)
当前浏览器不允许本地存储,评论无法保存。
还没有评论,来说两句。