skaiuijing

引言

在tcp的拥塞控制算法中,reno和bbr是两种不同的算法,它们代表的两种不同的设计哲学。

拥塞控制不同的设计之争已经持续了很多很多年,但是,终究是有一方看起来胜出了。

现代协议从老旧的ack驱动模型转化为了更加强大的时钟驱动。

结构体

reno结构体

在Linux6.6的tcp.h的tcp_sock结构体里面,有以下字段:

发送窗口(流量控制与拥塞控制)

  • snd_wnd — 对端通告窗口
  • max_window — 历史最大窗口
  • snd_cwnd — 拥塞窗口
  • snd_ssthresh — 慢启动阈值
  • snd_cwnd_cnt — cwnd 线性增长计数
  • snd_cwnd_clamp — cwnd 上限
  • snd_cwnd_used
  • snd_cwnd_stamp
  • prior_cwnd — 进入恢复前的 cwnd

RENO理论

reno代表的是ack驱动的设计哲学。

网络是一个“带时延 + 丢包 + 随机扰动”的动态系统。

把网络抽象成系统,就是:

y(t)=G(u(t−τ))+w(t)

其中:

  • u(t):发送端发出的数据
  • y(t):接收端返回的 ACK
  • τ:网络传播时延(固定 + 抖动)
  • w(t):随机扰动(丢包、排队、ACK 压缩、抖动)

在网络世界中,ack来了就是接收到了,ack很久没来就是丢包了,丢包就意味着网络可能发生了拥塞。

在一些老旧的TCP实现中,发送端根据 ACK 调整发送行为:u(t)=f(y(t))

这是一个单闭环反馈系统

1
2
3
4
5
6
7
8
9
10
11
12
13
┌──────────────┐
│ Sender │
│ u(t)=f(ACK) │
└───────┬──────┘
│ u(t)

┌────────────────┐
│ 网络系统 G │
│ (τ + 扰动 w) │
└───────┬────────┘
│ y(t)

ACK反馈

我们使用pacing,会发生什么呢?

pacing 的本质是:upacing(t)=P(t)

它依赖:本地时钟、估计速率、历史 RTT

但不依赖 ACK。

于是发送行为变成:u(t)=f(y(t))+P(t)

这就变成了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
      pacing(前馈)


┌──────────────┐
│ Sender │
│ u = f(ACK)+P │
└───────┬──────┘


┌────────────────┐
│ 网络系统 G │
└───────┬────────┘


ACK反馈

pacing会破坏 ACK 闭环的稳定性。

RENO实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
/*
* TCP Reno 拥塞控制算法
*
* Reno 是 TCP 的标准拥塞控制算法,实现了 Jacobson 的经典慢启动和拥塞避免算法。
* 该算法包含两个主要阶段:
* 1. 慢启动(Slow Start):指数级增加发送窗口
* 2. 拥塞避免(Congestion Avoidance):线性增加发送窗口
*
* 参考文献: Jacobson, V. "Congestion Avoidance and Control", SIGCOMM '88, p. 328.
*
* 这是系统的默认后备拥塞控制算法。
*/

/*
* tcp_reno_cong_avoid - RENO 拥塞避免主函数
* @sk: TCP 套接字
* @ack: 确认号
* @acked: 本次确认中新确认的字节数
*
* 该函数实现了 RENO 算法的核心逻辑,根据当前所处的阶段,分别采用不同的
* 窗口增长策略:
* - 在慢启动阶段:每收到一个 ACK,窗口增加 1 MSS(指数增长)
* - 在拥塞避免阶段:每个 RTT 内,窗口线性增加 1 MSS(加法增加)
*/
__bpf_kfunc void tcp_reno_cong_avoid(struct sock *sk, u32 ack, u32 acked)
{
struct tcp_sock *tp = tcp_sk(sk);

/* 检查是否真正受到拥塞窗口限制
* 如果接收窗口或应用层输出缓冲区是限制因素,则无需调整拥塞窗口 */
if (!tcp_is_cwnd_limited(sk))
return;

/* === 阶段1:慢启动(Slow Start) ===
* 在此阶段,cwnd < ssthresh,采用指数增长策略
* 每收到一个 ACK,cwnd 增加已确认字节数的大小 */
if (tcp_in_slow_start(tp)) {
/* 执行慢启动增长,返回未参与计算的确认字节数 */
acked = tcp_slow_start(tp, acked);

/* 若所有确认字节都已用于慢启动增长,则返回 */
if (!acked)
return;
}

/* === 阶段2:拥塞避免(Congestion Avoidance) ===
* 在此阶段,cwnd >= ssthresh,采用线性增长策略(加法增加)
* 每个 RTT 内,cwnd 增加 1 MSS,相当于每个 ACK 增加 1/cwnd */
tcp_cong_avoid_ai(tp, tcp_snd_cwnd(tp), acked);
}
EXPORT_SYMBOL_GPL(tcp_reno_cong_avoid);

/*
* tcp_reno_ssthresh - 计算慢启动阈值
* @sk: TCP 套接字
*
* 返回值:
* 新的慢启动阈值(Slow Start Threshold),为当前拥塞窗口大小的一半,
* 但不少于 2 MSS。
*
* 使用场景:
* 当检测到拥塞时(如收到 3 个重复的 ACK 或 RTO 超时),调用此函数
* 计算新的 ssthresh,用于区分慢启动和拥塞避免两个阶段。
*/
__bpf_kfunc u32 tcp_reno_ssthresh(struct sock *sk)
{
const struct tcp_sock *tp = tcp_sk(sk);

/* 拥塞时,将 ssthresh 设为当前 cwnd 的一半(乘法递减, Multiplicative Decrease)
* >> 1U 表示右移 1 位,即除以 2
* 至少保持 2 个 MSS,防止 ssthresh 过小 */
return max(tcp_snd_cwnd(tp) >> 1U, 2U);
}
EXPORT_SYMBOL_GPL(tcp_reno_ssthresh);

/*
* tcp_reno_undo_cwnd - 撤销拥塞窗口(用于虚假超时恢复)
* @sk: TCP 套接字
*
* 返回值:
* 恢复到的拥塞窗口大小,取当前 cwnd 和之前保存的 prior_cwnd 中较大值。
*
* 使用场景:
* 当连接从拥塞状态恢复时(例如通过快速重传/恢复算法),
* 如果检测到拥塞事件是虚假的,可以部分恢复原有的拥塞窗口。
*/
__bpf_kfunc u32 tcp_reno_undo_cwnd(struct sock *sk)
{
const struct tcp_sock *tp = tcp_sk(sk);

/* 恢复到包括当前值和之前值中较大的窗口
* 这保证了窗口不会过度回退 */
return max(tcp_snd_cwnd(tp), tp->prior_cwnd);
}
EXPORT_SYMBOL_GPL(tcp_reno_undo_cwnd);

/* === Reno 拥塞控制算法操作接口 ===
*
* 该结构体定义了 TCP Reno 算法对外暴露的所有操作函数。
* 内核通过此接口调用不同拥塞控制算法的具体实现。
*/
struct tcp_congestion_ops tcp_reno = {
/* Reno 是非受限算法,可由所有 TCP 连接使用 */
.flags = TCP_CONG_NON_RESTRICTED,

/* 算法名称,可通过 /proc/sys/net/ipv4/tcp_congestion_control 查看 */
.name = "reno",

/* 模块所有者,用于引用计数 */
.owner = THIS_MODULE,

/* 拥塞时计算新的慢启动阈值 */
.ssthresh = tcp_reno_ssthresh,

/* 拥塞避免主函数 */
.cong_avoid = tcp_reno_cong_avoid,

/* 撤销拥塞窗口(虚假超时恢复) */
.undo_cwnd = tcp_reno_undo_cwnd,
};

参考

“Congestion Avoidance and Control”
— Van Jacobson & Michael J. Karels, ACM Computer Communication Review, 1988
https://dl.acm.org/doi/10.1145/52325.52356