TCP拥塞窗口调整撤销剖析

由于当前网络不支持ECN,因此在追踪丢失包时需要推测。重新排序(reordering)对于发送方来说
通常是一个问题,因为它不能分清缺失的ACK是由于丢失还是被延迟了,所以TCP可能会做出错误的
判断,不必要的调整了拥塞窗口。这时就需要一种对错误的拥塞调整做出修正的机制——拥塞窗口
调整撤销。

 

检测能否撤销

 

在进行拥塞窗口调整撤销之前,必须先用tcp_may_undo()检测能否撤销。

static inline int tcp_may_undo(struct tcp_sock *tp)
{
        return tp->undo_marker && (!tp->undo_retrans || tcp_packet_delayed(tp));
}

/* Nothing was retransmitted or returned timestamp is less than timestamp of the 
 * first retransmission.
 */
static inline int tcp_packet_delayed(struct tcp_sock *tp)
{
        return !tp->retrans_stamp || 
              (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr &&
               before(tp->rx_opt.rcv_tsecr, tp->retrans_stamp));
}

怎么来探测是否不必要重传了数据包呢?

(1)D-SACK

在最近一次恢复期间重传的段都被D-SACK确认。这就说明了调整是不必要的。
最近一次恢复期间重传的数据包个数记为undo_retrans,如果收到一个D-SACK,则
undo_retrans--,直到undo_retrans为0,说明全部的重传都是没必要的,则需要撤销
窗口调整。

(2)Timestamp

使用该选项时,通过比较收到ACK的时间戳和重发数据包的时间戳,可以判断窗口调整
是否没必要。

tcp_may_undo()中的!tp->undo_retrans和tcp_packet_delayed(tp)分别对应以上两种方法。
tcp_packet_delayed()中其实也包含两种方法:Timestamp和F-RTO。!tp->retrans_stamp表
示已经使用F-RTO进行处理。
只有检查出至少有一种成立时,才能进行拥塞窗口调整撤销。

 

撤销拥塞调整

 

 /* 用来撤销“缩小拥塞窗口”,undo表示需要撤销慢启动阈值*/
static void tcp_undo_cwr(struct sock *sk, const int undo)
{
        struct tcp_sock *tp = tcp_sk(sk);

        if (tp->prior_ssthresh) {
                const struct inet_connection_sock *icsk = inet_csk(sk);

                if (icsk->icsk_ca_ops->undo_cwnd)
                        tp->snd_cwnd = icsk->icsk_ca_ops->undo_cwnd(sk);
                else
                        tp->snd_cwnd = max(tp->snd_cwnd, tp->snd_ssthresh<<1);

                if (undo && tp->prior_ssthresh > tp->snd_ssthresh) {
                        tp->snd_ssthresh = tp->prior_ssthresh;
                        TCP_ECN_withdraw_cwr(tp);
                }
        } else { /*没保存旧的阈值*/
                tp->snd_cwnd = max(tp->snd_cwnd, tp->snd_ssthresh);
        }

        tcp_moderate_cwnd(tp);
        tp->snd_cwnd_stamp = tcp_time_stamp;
}

/* CWND moderation, preventing bursts due to too big ACKs in 
 *dubious situations 
 */
static inline void tcp_moderate_cwnd(struct tcp_sock *tp)
{
        tp->snd_cwnd = min(tp->snd_cwnd, 
                   tcp_packets_in_flight(tp) + tcp_max_burst(tp));
        tp->snd_cwnd_stamp = tcp_time_stamp;
}

static __inline__ __u32 tcp_max_burst(const struct tcp_sock *tp)
{
        return tp->reordering;
}

如果当前的拥塞控制算法实现了undo_cwnd接口,则调用它来重设拥塞窗口的大小。

否则取当前拥塞窗口和2倍阈值之间的较大者为拥塞窗口的大小。

 

使用D-SACK撤销

 

D-SACK可以通知发送方,新到的段是已经接收过的。如果所有在最近的一次恢复期间重传的数据段

都被D-SACK确认了,发送方就知道恢复期被不必要的触发了。

struct sock {
        ...

        u32 retrans_stamp; /* Timestamp of the last retransmit.*/
        u32 undo_marker; /* tracking retrans started here.*/
        int undo_retrans; /* number of undoable retransmissions.*/
        u32 total_retrans; /* Total retransmits for entire connection */

        ...
}

发送方在探测到一个D-SACK块时,可使undo_retrans减一。如果D-SACK块最终确认了在最近窗口

中的每个不必要的重传,重传计数器因为D-SACK降为0,发送方增大拥塞窗口,恢复最新一次对

ssthresh的修改。

/* Try to undo cwnd reduction, because D-SACKs acked all retransmitted data */
static void tcp_try_undo_dsack(struct sock *sk)
{
        struct tcp_sock *tp = tcp_sk(sk);

        if (tp->undo_marker && !tp->undo_retrans) {
            DBGUNDO(sk, "D-SACK");
            tcp_undo_cwr(sk, 1); /*进行撤销操作*/
            tp->undo_marker = 0; 
           NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPDSACKUNDO);
        }
}

 

使用时间戳

 

TCP发送方可以使用附在每个TCP首部的时间戳选项来探测不必要的重传。当使用该选项时,TCP接收

方回显触发确认,返回发送方数据段的时间戳,允许发送方确定ACK是被原始的还是重传的触发。

Eifel算法使用类似方法来探测假重传。

当使用时间戳探测到一个不必要的重传时,如果发送方处于Loss状态,即在一个不必要被触发的RTO

之后正在重传,移除记分牌中所有段的Loss标志,从而使发送方继续发送新的数据而不再重传。此外,

调用tcp_undo_cwr来撤销拥塞窗口和阈值的调整。

 

从Loss状态撤销

 

static int tcp_try_undo_loss(struct sock *sk)
{
        struct tcp_sock *tp = tcp_sk(sk);
        if (tcp_may_undo(tp)) { /*检测能否撤销调整*/
                struct sk_buff *skb;

                tcp_for_write_queue(skb, sk) { /*遍历发送队列,直到snd.nxt*/
                        if (skb == tcp_send_head(sk))
                             break;
                        TCP_SKB_CB(skb)->sacked &= ~TCPCB_LOST; /*清除Loss标志*/
                }

                tcp_clear_all_retrans_hints(tp);
                DBGUNDO(sk, "partial loss");
                tp->lost_out = 0;
                tcp_undo_cwr(sk, 1);
                NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPLOSSUNDO);
                inet_csk(sk)->icsk_retransmits = 0;
                tp->undo_marker = 0;

                if (tcp_is_sack(tp)) /*为什么Reno不行,RFC2582*/
                tcp_set_ca_state(sk, TCP_CA_Open); /*返回Open态*/
                return 1; /*调整成功*/
        }
        return 0; /*调整失败*/
}

 

从Recovery/Loss状态撤销

 

static int tcp_try_undo_recovery(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);

    if (tcp_may_undo(tp)) { /*可以进行拥塞撤销*/
        int mib_idx;
        DBG(sk, inet_csk(sk)->icsk_ca_state == TCP_CA_Loss ? "loss" : "retrans");

        tcp_undo_cwr(sk, true); /* 具体撤销内容*/

        if (inet_csk(sk)->icsk_ca_state == TCP_CA_Loss)
            mib_idx = LINUX_MIB_TCPLOSSUNDO;
        else
            mib_idx = LINUX_MIB_TCPFULLUNDO;
        NET_INC_STATS_BH(sock_net(sk), mib_idx);

        tp->undo_marker = 0; /*复位撤销标志*/
    }

    /* Hold old state until above high_seq is ACKed. For Reno it is
     * MUST to prevent false fast retransmits (RFC2582). 
     * SACK TCP is safe. 
     * 防止虚假的快速重传?
      */
    if (tp->snd_una == tp->high_seq && tcp_is_reno(tp)) {
        tcp_moderate_cwnd(tp);
        return 1;
    }

    tcp_set_ca_state(sk, TCP_CA_Open);
    return 0;
}

 

从Recovery状态撤销

 

/* We can clear retrans_stamp when there are no retransmissions in the window.
 * It would seem that it is trivially available for us in tp->retrans_out, however,
 * that kind of assumptions doesn't consider what will happen if errors occur when 
 * sending retransmission for the second time...It could be that such segment has 
 * only TCPCB_EVER_RETRANS set at the present time. It seems that checking the head
 * skb is enough except for some reneging corner cases that are not worth the effort.
 *
 * Main reason for all this complexity is the fact that connection dying time now 
 * dpends on the validity of the retrans_stamp, in particular, that successive 
 * retransmissions of a segment must not advance retrans_stamp under any conditions.
 */
static int tcp_any_retrans_done(const struct sock *sk)
{
    const struct tcp_sock *tp = tcp_sk(sk);
    struct sk_buff *skb;
    if (tp->retrans_out)
        return 1;

    skb = tcp_write_queue_head(sk); /* 发送队列中第一个数据包*/
    if (unlikely(skb && TCP_SKB_CB(skb)->sacked & TCPCB_EVER_RETRANS)
        return 1;
    return 0;
}

在Recovery状态,收到部分确认,则调用此函数撤销拥塞调整。

static int tcp_try_undo_partial(struct sock *sk, int acked)
{
    struct tcp_sock *tp= tcp_sk(sk);
    /* Partial ACK arrived. Force hoe's retransmit. */
    /* 如果是使用reno,收到partial ACK则必须马上重传。
     * 如果此时非reorder,则也要重传。
     */
    int failed = tcp_is_reno(tp) || (tcp_fackets_out(tp) > tp->reordering);

    /* 需要进行拥塞调整撤销时*/
    if (tcp_may_undo(tp)) {
        /* Plain luck! Hole if filled with delayed packet,
         * rather than with a retransmit.
         */
          if (!tcp_any_retrans_done(sk))
              tp->retrans_stamp = 0;

          tcp_update_reordering(sk, tcp_fackets_out(tp) + acked, 1);
          DBGUNDO(sk, "Hoe");
          tcp_undo_cwr(sk, false);

          NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPPARTIALUNDO);
          /* So... Do not make Hoe's retransmit yet. If the first packet was delayed,
           * the rest ones are most probably delayed as well.
           */
            failed = 0; /*表示不用重传了,可以发送新的数据了。*/
    }
    return failed;
}

 

posted on 2012-04-17 09:36  张大大123  阅读(315)  评论(0编辑  收藏  举报

导航