本文内容:分析TCP接收窗口的调整算法,主要是接收窗口当前阈值的调整算法。
内核版本:3.2.12
作者:zhangskd @ csdn blog
我们知道,在拥塞控制中,有个慢启动阈值,控制着拥塞窗口的增长。在流控制中,也有个接收窗口的
当前阈值,控制着接收窗口的增长。可见TCP的拥塞控制和流控制,在某些地方有异曲同工之处。
接收窗口当前阈值tp->rcv_ssthresh的主要功能:
On reception of data segment from the sender, this value is recalculated based on the size of the
segment, and later on this value is used as upper limit on the receive window to be advertised.
可见,接收窗口当前阈值对接收窗口的大小有着重要的影响。
接收窗口当前阈值调整算法的基本思想:
When we receive a data segment, we need to calculate a receive window that needs to be
advertised to the sender, depending on the segment size received.
The idea is to avoid filling the receive buffer with too many small segments when an application
is reading very slowly and packets are transmitted at a very high rate.
在接收窗口当前阈值的调整算法中,收到数据报的负荷是个关键因素,至于它怎么影响接收窗口当前
阈值的增长,来看下代码吧。
当接收到一个报文段时,调用处理函数:
static void tcp_event_data_recv (struct sock *sk, struct sk_buff *skb) { struct tcp_sock *tp = tcp_sk(sk); struct inet_connection_sock *icsk = inet_csk(sk); u32 now; ... /* 当报文段的负荷不小于128字节时,考虑增大接收窗口当前阈值rcv_ssthresh */ if (skb->len >= 128) tcp_grow_window(sk, skb); }
下面这个函数决定是否增长rcv_ssthresh,以及增长多少。
static void tcp_grow_window (struct sock *sk, const struct sk_buff *skb) { struct tcp_sock *tp = tcp_sk(sk); /* Check #1,关于这三个判断条件的含义可见下文分析 */ if (tp->rcv_ssthresh < tp->window_clamp && (int) tp->rcv_ssthresh < tcp_space(sk) && ! tcp_memory_pressure) { int incr; /* Check #2. Increase window, if skb with such overhead will fit to rcvbuf in future. * 如果应用层数据占这个skb总共消耗内存的75%以上,则说明这个数据报是大的数据报, * 内存的额外开销较小。这样一来我们可以放心的增长rcv_ssthresh了。 */ if (tcp_win_from_space(skb->truesize) <= skb->len) incr = 2 * tp->advmss; /* 增加两个本端最大接收MSS */ else /* 可能增大rcv_ssthresh,也可能不增大,具体视额外内存开销和剩余缓存而定*/ incr = __tcp_grow_window(sk, skb); if (incr) { /* 增加后不能超过window_clamp */ tp->rcv_ssthresh = min(tp->rcv_ssthresh + incr, tp->window_clamp); inet_csk(sk)->icsk_ack.quick |= 1; /* 允许快速ACK */ } } } /* Slow part of check#2. */ static int __tcp_grow_window (const struct sock *sk, const struct sk_buff *skb) { struct tcp_sock *tp = tcp_sk(sk); /* Optimize this! */ int truesize = tcp_win_from_space(skb->truesize) >> 1; int window = tcp_win_from_space(sysctl_tcp_rmem[2]) >> 1; /* 接收缓冲区长度上限的一半*/ /* rcv_ssthresh不超过一半的接收缓冲区上限才有可能*/ while (tp->rcv_ssthresh <= window) { if (truesize <= skb->len) return 2 * inet_csk(sk)->icsk_ack.rcv_mss; /* 增加两个对端发送MSS的估计值*/ truesize >>= 1; window >>= 1; } return 0;/*不增长*/ }
这个算法可能不太好理解,我们来分析一下。
只有当数据段长度大于128字节时才会考虑增长rcv_ssthresh,并且有以下大前提(就是check #1):
a. 接收窗口当前阈值不能超过接收窗口的上限。
b. 接收窗口当前阈值不能超过剩余接收缓存的3/4,即network buffer。
c. 没有内存压力。TCP socket系统总共使用的内存过大。
check#2是根据额外开销的内存占的比重,来判断是否允许增长。额外的内存开销(overhead)指的是:
sk_buff、skb_shared_info结构体,以及协议头。有效的内存开销指的是数据段的长度。
(1) 额外开销小于25%,则rcv_ssthresh增长两个本端最大接收MSS。
(2)额外开销大于25%,分为两种情况。
算法如下:
把3/4的剩余接收缓存,即剩余network buffer均分为2^n块。把额外开销均分为2^n份。
如果均分后每块缓存的大小大于rcv_ssthresh,且均分后的每份开销小于数据段的长度,则:
允许rcv_ssthresh增大2个对端发送MSS的估计值。
否则,不允许增大rcv_ssthresh。
我们注意到在(1)和(2)中,rcv_ssthresh的增长幅度是不同的。在(1)中,由于收到大的数据段,额外
开销较低,所以增长幅度较大(2 * tp->advmss)。在(2)中,由于收到中等数据段,额外开销较高,所以
增长幅度较小(2 * icsk->icsk_ack.rcv_mss)。这样做是为了防止额外开销过高,而耗尽接收窗口。
rcv_ssthresh增长算法的基本思想:
This algorithm works on the basis that we do not want to increase the advertised window if we
receive lots of small segments (i.e. interactive data flow), as the per-segment overhead (headers
and the buffer control block) is very high.
额外开销大小,取决于数据段的大小。我们从这个角度来分析下当接收到一个数据报时,rcv_ssthresh
的增长情况:
(1)Small segment (len < 128)
如果接收到的数据段很小,这时不允许增大rcv_ssthresh,防止额外内存开销过大。
(2)Medium segment (128 <= len <= 647)
如果接收到中等长度的数据段,符合条件时,rcv_ssthresh += 2 * rcv_mss。
(3)Large segment (len > 647)
如果接收到数据段长度较大的报文,符合条件时(rcv_ssthresh不超过window_clamp和3/4剩余接收缓存等),
rcv_ssthresh += 2 * advmss。这是比较常见的情况,这时接收窗口阈值一般增加2 * 1460 = 2920字节。
这个值还可能有细微波动,这是由于对齐窗口扩大因子的关系。