Chinaunix首页 | 论坛 | 博客
  • 博客访问: 49968
  • 博文数量: 37
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 0
  • 用 户 组: 普通用户
  • 注册时间: 2016-11-22 10:10
文章分类
文章存档

2016年(37)

我的朋友

分类: LINUX

2016-11-22 10:28:53

作者:henrystark
Blog: http://henrystark.blog.chinaunix.net/
日期:20131123
本文可以自由拷贝,转载。但转载请保持文档的完整性,注明原作者及原链接。 如有错讹,烦请指出。

============================================================

TCP降窗过程——逐步降低而非立即降低

好吧,这篇博文是之前答应一个同学要写的,前一段时间很忙,顾不上,现在整理出来分享给大家。
也许大家太过信奉《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的数值,发现两者相差很小。

阅读(827) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~