分类: LINUX
2013-11-23 22:06:12
作者:henrystark
Blog: http://henrystark.blog.chinaunix.net/
日期:20131123
本文可以自由拷贝,转载。但转载请保持文档的完整性,注明原作者及原链接。
如有错讹,烦请指出。
============================================================
好吧,这篇博文是之前答应一个同学要写的,前一段时间很忙,顾不上,现在整理出来分享给大家。
也许大家太过信奉《TCP/IP详解》了,所以认为TCP一遇到丢包,就立即降低一半窗口。实际上不是,仔细观察拥塞窗口随时间、丢包降低的曲线,可以发现,拥塞窗口是在一个RTT内逐步降低的。
实际上,按照内核的实现,一旦进入cwnd降低流程,每收到两个ACK,cwnd就降低1,而一个RTT内收到的ACK数目大致和cwnd相等,于是,一个RTT内cwnd就变成一半了。
LINUX TCP的cwnd降低流程不是很容易理解,涉及到TCP的核心——拥塞状态机。在前面的blog里,提到了拥塞状态机,大体说来,有OPEN(正常态)、disorder(拥塞初步状态)、recovery(丢包恢复状态)、CWR(标记降窗状态),其中recovery和cwr状态会调用相似的降窗流程。核心是tcpcwnddown函数,位于net/ipv4/tcp_input.c中,内核版本:linux-2.6.32.60.
/* Decrease cwnd each second ack. */ static void tcp_cwnd_down(struct sock *sk, int flag) { struct tcp_sock *tp = tcp_sk(sk); int decr = tp->snd_cwnd_cnt + 1; if ((flag & (FLAG_ANY_PROGRESS | FLAG_DSACKING_ACK)) || (tcp_is_reno(tp) && !(flag & FLAG_NOT_DUP))) { tp->snd_cwnd_cnt = decr & 1; decr >>= 1; if (decr && tp->snd_cwnd > tcp_cwnd_min(sk)) tp->snd_cwnd -= decr; tp->snd_cwnd = min(tp->snd_cwnd, tcp_packets_in_flight(tp) + 1); tp->snd_cwnd_stamp = tcp_time_stamp; } }
这个函数的核心是decr的数值和tcp_cwnd按照decr减多少。
tcp主流程是每个ACK到来就处理一次的,因此tcp_cwnd_down这个函数每个ACK都会调用。
tp->snd_cwnd_cnt = decr & 1;这一句决定了,tp->snd_cwnd_cnt的数值要么是1,要么是0。
相对地,decr = tp->snd_cwnd_cnt + 1;的数值要么是2,要么是1。
decr >>= 1;这一句决定了,只有偶数ACK到来时,才会减。
tp->snd_cwnd = min(tp->snd_cwnd, tcp_packets_in_flight(tp) + 1);这一句保证最多可以再发送一个数据包,实际情况中,根据我以前打印的数值看来,inflight数目和cwnd总是相近的,所以这一句也不会造成cwnd猛然降低。【注 1】
tcpcwnddown这个函数由trytoopen函数调用:
static void tcp_try_to_open(struct sock *sk, int flag) { struct tcp_sock *tp = tcp_sk(sk); tcp_verify_left_out(tp); if (!tp->frto_counter && !tcp_any_retrans_done(sk)) tp->retrans_stamp = 0; if (flag & FLAG_ECE) tcp_enter_cwr(sk, 1); if (inet_csk(sk)->icsk_ca_state != TCP_CA_CWR) { tcp_try_keep_open(sk); tcp_moderate_cwnd(tp); } else { tcp_cwnd_down(sk, flag); } }
可以看到,最后一个else指明了,如果是CWR状态,直接进入cwnd_down函数。 另一处调用在tcp_fastretrans_alert函数d的最后,recovery状态会调用这个函数。
static void tcp_fastretrans_alert(struct sock *sk, int pkts_acked, int flag) { ……………………………… /* F. Process state. */ switch (icsk->icsk_ca_state) { case TCP_CA_Recovery: if (!(flag & FLAG_SND_UNA_ADVANCED)) { if (tcp_is_reno(tp) && is_dupack) tcp_add_reno_sack(sk); } else do_lost = tcp_try_undo_partial(sk, pkts_acked); break; case TCP_CA_Loss: if (flag & FLAG_DATA_ACKED) icsk->icsk_retransmits = 0; if (tcp_is_reno(tp) && flag & FLAG_SND_UNA_ADVANCED) tcp_reset_reno_sack(tp); if (!tcp_try_undo_loss(sk)) { tcp_moderate_cwnd(tp); tcp_xmit_retransmit_queue(sk); return; } if (icsk->icsk_ca_state != TCP_CA_Open) return; /* Loss is undone; fall through to processing in Open state. */ default: ……………………………… } if (do_lost || (tcp_is_fack(tp) && tcp_head_timedout(sk))) tcp_update_scoreboard(sk, fast_rexmit); tcp_cwnd_down(sk, flag); tcp_xmit_retransmit_queue(sk); }
recovery状态在处理流程中并没有直接return,而是到达了最后调用tcp_cwnd_down的语句。
注解: 【1】in_flight包数目是指已经发出,但还没有得到确认的包数目,一般而言小于cwnd。堆积在发送端缓存队列中的包也算inflight,正常情况下,ACK的恢复要经历一个RTT,所以没有确认的包数目和cwnd相近也是可以理解的。形象的解释是:一大片数据包发出,等待一大片ACK到来。实际过程,打印in_flight和cwnd的数值,发现两者相差很小。