zoukankan      html  css  js  c++  java
  • 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;
    }
  • 相关阅读:
    sql-trace-10046-trcsess-and-tkprof
    教你深入理解软件包的配置、编译与安装过程
    Java RESTful 框架的性能比较
    gcc、arm-Linux-gcc和arm-elf-gcc的组成及区别
    Linux线上系统程序debug思路及方法
    使用systemtap调试Linux内核 :www.lenky.info
    SystemTap使用技巧 1
    gvfs
    Systemtap examples, Network
    .NET 大型信息化建设标准基础数据管理平台
  • 原文地址:https://www.cnblogs.com/aiwz/p/6333386.html
Copyright © 2011-2022 走看看