作者:henrystark
Blog: http://henrystark.blog.chinaunix.net/
日期:20131130
本文可以自由拷贝,转载。但转载请保持文档的完整性,注明原作者及原链接。
如有错讹,烦请指出。
============================================================
TCP窗口行为
这一篇blog讲述TCP在整个传输流程中的窗口变化行为。
TCP窗口行为的关键点有:初始窗口和阈值;何时增窗、增多少;何时减窗、减多少。
1.初始窗口
TCP的拥塞窗口,congestion window,简称cwnd,是拥塞控制的核心,决定一个RTT内发送多少数据包。
在旧的内核版本中,cwnd的初始值设置为2*MSS。在较新的内核版本中,根据Google的建议,设置为10*MSS。
至于为什么这么设置,请参见另一篇blog《增大初始窗口的优劣》。
下面这一段源码是3.2.18内核版本net/ipv4/tcp_input.c中的初始化函数。
__u32 tcp_init_cwnd(const struct tcp_sock *tp, const struct dst_entry *dst)
{
__u32 cwnd = (dst ? dst_metric(dst, RTAX_INITCWND) : 0);
if (!cwnd)
cwnd = TCP_INIT_CWND;
return min_t(__u32, cwnd, tp->snd_cwnd_clamp);
}
TCPINITCWND在tcp.h中定义。
/* TCP initial congestion window as per draft-hkchu-tcpm-initcwnd-01 */ #define TCP_INIT_CWND 10
2.6.32.60版本的源码:
/* Numbers are taken from RFC3390.
*
* John Heffner states:
*
* The RFC specifies a window of no more than 4380 bytes
* unless 2*MSS > 4380. Reading the pseudocode in the RFC
* is a bit misleading because they use a clamp at 4380 bytes
* rather than use a multiplier in the relevant range.
*/
__u32 tcp_init_cwnd(struct tcp_sock *tp, struct dst_entry *dst)
{
__u32 cwnd = (dst ? dst_metric(dst, RTAX_INITCWND) : 0);
if (!cwnd) {
if (tp->mss_cache > 1460)
cwnd = 2;
else
cwnd = (tp->mss_cache > 1095) ? 3 : 4;
}
return min_t(__u32, cwnd, tp->snd_cwnd_clamp);
}
初始阈值ssthresh被设置为无穷大。
#define TCP_INFINITE_SSTHRESH 0x7fffffff
这里要着重解释一下ssthresh的意义,它的全称是slow start threshold,字面意思就是慢启动阈值。
但这个阈值在拥塞避免阶段也起到关键作用。
2.何时增窗
2.1 慢启动阶段
这个阶段中,cwnd呈指数增长,每收到一个正常ACK,cwnd增加1,经过一个RTT,cwnd增加一倍。
源码位于tcp_cong.c中:
/*
298 * Slow start is used when congestion window is less than slow start
299 * threshold. This version implements the basic RFC2581 version
300 * and optionally supports:
301 * RFC3742 Limited Slow Start - growth limited to max_ssthresh
302 * RFC3465 Appropriate Byte Counting - growth limited by bytes acknowledged
303 */
304void tcp_slow_start(struct tcp_sock *tp)
305{
306 int cnt; /* increase in packets */
307
308 /* RFC3465: ABC Slow start
309 * Increase only after a full MSS of bytes is acked
310 *
311 * TCP sender SHOULD increase cwnd by the number of
312 * previously unacknowledged bytes ACKed by each incoming
313 * acknowledgment, provided the increase is not more than L
314 */
315 if (sysctl_tcp_abc && tp->bytes_acked < tp->mss_cache)
316 return;
317
318 if (sysctl_tcp_max_ssthresh > 0 && tp->snd_cwnd > sysctl_tcp_max_ssthresh)
319 cnt = sysctl_tcp_max_ssthresh >> 1; /* limited slow start */
320 else
321 cnt = tp->snd_cwnd; /* exponential increase */
322
323 /* RFC3465: ABC
324 * We MAY increase by 2 if discovered delayed ack
325 */
326 if (sysctl_tcp_abc > 1 && tp->bytes_acked >= 2*tp->mss_cache)
327 cnt <<= 1;
328 tp->bytes_acked = 0;
329
330 tp->snd_cwnd_cnt += cnt;
331 while (tp->snd_cwnd_cnt >= tp->snd_cwnd) {
332 tp->snd_cwnd_cnt -= tp->snd_cwnd;
333 if (tp->snd_cwnd < tp->snd_cwnd_clamp)
334 tp->snd_cwnd++;
335 }
336}
337EXPORT_SYMBOL_GPL(tcp_slow_start);
这段代码的重点在while循环,前面的逻辑都是对增窗数目做处理。正常情况下,每收到一个ACK,就调用一次函数,在阈值之下,cwnd指数增长。
2.2 拥塞避免阶段
线性增长,每收到一个ACK,cwnd增加1/cwnd,经过一个RTT,cwnd增加1。因为一个RTT对应一个整窗数据和ACK。
/* In theory this is tp->snd_cwnd += 1 / tp->snd_cwnd (or alternative w) */
340void tcp_cong_avoid_ai(struct tcp_sock *tp, u32 w)
341{
342 if (tp->snd_cwnd_cnt >= w) {
343 if (tp->snd_cwnd < tp->snd_cwnd_clamp)
344 tp->snd_cwnd++;
345 tp->snd_cwnd_cnt = 0;
346 } else {
347 tp->snd_cwnd_cnt++;
348 }
349}
350EXPORT_SYMBOL_GPL(tcp_cong_avoid_ai);
可以看到,当窗口增长因子cnt大于w,即cwnd时,窗口才增长。
以上这些增长都要判断cwnd是否超过阈值。
3.何时减窗
降窗发生的条件是丢包或者RTT增大。传统TCP在拥塞判断上分为两大阵营:基于丢包,基于RTT变化。
丢包通过重复ACK来反映。而RTT变化通过采样获得。
为了论述简单,这里只讲述基于丢包的窗口降低。至于诸多TCP变种协议如何降窗,在后续blog中论述。
当收到三次重复ACK或者SACK时,sender认为数据包已经丢失,此时开始降窗。cwnd降低为当前数值的一般,ssthresh设定为降低后的cwnd+3。
假设初始阈值为无限大,在cwnd=1000时发生了丢包,则cwnd变为500,ssthresh变为503。
这里需要强调,cwnd并非立即降低,而是逐渐降低,每收到两个ACK,cwnd减1。详情参见另一篇blog《TCP降窗过程》。这里不再列举函数。
基于RTT的协议在RTT有较大变动时,就开始降低,至于如何降低,那是另一个问题了。
此外,可能会有同学疑问,不同版本的TCP协议降窗算法不同,如何实现?
这里简介一下,TCP提供了可以重写的函数,其中包括阈值调节函数,cwnd降低过程由TCP主流程维护,但是诸多版本的拥塞控制算法可以设定降窗下限幅度。
这样就可以控制降窗范围了。
超时的情况另当别论,不属于拥塞控制算法的范围了。
4.概述
从正常TCP流程论述一遍:
TCP初始窗口为2,每收到一个ACK,cwnd+1,以RTT来看,呈现指数增长。此时的阈值为无限大。
遇到拥塞,cwnd降低,阈值重新调节。开始拥塞避免。
发生超时,cwnd重置为1,开始新一轮慢启动。

图片来自