zoukankan      html  css  js  c++  java
  • tcpack--3快速确认模式

    接收到数据报后,会调用tcp_event_data_recv(),不管是在慢速路径的tcp_data_queue中调用还是 在快速路径中处理接收数据后直接调用,注意(如果len <= tcp_header_len 则是没有载荷),不会调用tcp_event_date_recv处理。

    tcp_event_data_recv()中设置ICSK_ACK_SCHED标志来表明有ACK需要发送。如果接收到了小包,说明对端很可能暂时没有数据需要发送了,此时会设置ICSK_ACK_PUSHED标志,如果处于快速路径中,就允许马上发送ACK。如果不止一次接收到小包,就设置ICSK_ACK_PUSHED2标志,不管是否处于快速路径中,都允许立即发送ACK,以强调发送ACK的紧急程度。

    enum inet_csk_ack_state_t {
        ICSK_ACK_SCHED = 1,    /* 有ACK需要发送 */
        ICSK_ACK_TIMER = 2,     /* 延迟确认定时器已经启动 */
        ICSK_ACK_PUSHED = 4,   /* 如果处于快速发送模式,允许立即发送ACK */
        ICSK_ACK_PUSHED2 = 9    /* 无论是否处于快速发送模式,都可以立即发送ACK */
    
    }
    /* There is something which you must keep in mind when you analyze the
     * behavior of the tp->ato delayed ack timeout interval.  When a
     * connection starts up, we want to ack as quickly as possible.  The
     * problem is that "good" TCP's do slow start at the beginning of data
     * transmission.  The means that until we send the first few ACK's the
     * sender will sit on his end and only queue most of his data, because
     * he can only send snd_cwnd unacked packets at any given time.  For
     * each ACK we send, he increments snd_cwnd and transmits more of his
     * queue.  -DaveM
     struct inet_connection_sock {
        /* inet_sock has to be the first member! *//*
        struct inet_sock      icsk_inet;
        struct request_sock_queue icsk_accept_queue;
        struct inet_bind_bucket      *icsk_bind_hash;
        unsigned long          icsk_timeout;
         struct timer_list      icsk_retransmit_timer;
         struct timer_list      icsk_delack_timer;
        __u32              icsk_rto;
        __u32              icsk_pmtu_cookie;
        const struct tcp_congestion_ops *icsk_ca_ops;
        const struct inet_connection_sock_af_ops *icsk_af_ops;
        unsigned int          (*icsk_sync_mss)(struct sock *sk, u32 pmtu);
        __u8              icsk_ca_state:6,
                      icsk_ca_setsockopt:1,
                      icsk_ca_dst_locked:1;
        __u8              icsk_retransmits;
        __u8              icsk_pending;
        __u8              icsk_backoff;
        __u8              icsk_syn_retries;
        __u8              icsk_probes_out;
        __u16              icsk_ext_hdr_len;
        struct {
         /* ACK is pending.
             * ACK的发送状态标志,可以表示四种情况:
             * 1. ICSK_ACK_SCHED:目前有ACK需要发送
             * 2. ICSK_ACK_TIMER:延迟确认定时器已经启动
             * 3. ICSK_ACK_PUSHED:如果处于快速确认模式,允许立即发送ACK
             * 4. ICSK_ACK_PUSHED2:无论是否处于快速确认模式,都可以立即发送ACK
             
            __u8          pending;     /* ACK is pending               
        //快速确认模式下,最多能够发送多少个ACK,额度用完以后就退出快速确认模式
            __u8          quick;     /* Scheduled number of quick acks       */
            /* 值为1时,为延迟确认模式;值为0时,为快速确认模式。
             * 注意这个标志是不是永久性的,而是动态变更的
            __u8          pingpong;     /* The session is interactive           
            /*
             * 如果延迟确认定时器触发时,发现socket被用户进程锁住,就把blocked置为1。
             * 之后在接收到新数据、或者将数据复制到用户空间之后、或者再次超时时,会马上发送ACK
            
            __u8          blocked;     /* Delayed ACK was blocked by socket lock 
            /*
            * ACK的超时时间,是一个中间变量,根据接收到数据包的时间间隔来动态调整。
             * 用来计算延迟确认定时器的超时时间timeout。
             
            __u32          ato;         /* Predicted tick of soft clock      
            //延迟确认定时器的超时时刻。
            unsigned long      timeout;     /* Currently scheduled timeout          
            //最后一次收到带负荷的报文的时间点。
            __u32          lrcvtime;     /* timestamp of last received data packet
            __u16          last_seg_size; /* Size of last incoming segment segment上次收到的数据段大小      
            __u16          rcv_mss;     /* MSS used for delayed ACK decisions      
        } icsk_ack;
     ***/
    static inline void inet_csk_schedule_ack(struct sock *sk)
    {
        inet_csk(sk)->icsk_ack.pending |= ICSK_ACK_SCHED;
    }


    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; inet_csk_schedule_ack(sk);/* 接收到了数据,设置ACK需调度标志*/ /* 通过接收到的数据段,来估算对端的MSS。 * 如果接收到了小包,则设置ICSK_ACK_PUSHED标志。 * 如果之前接收过小包,本次又接收到了小包,则设置ICSK_ACK_PUSHED2标志。 */ tcp_measure_rcv_mss(sk, skb); /* 没有使用时间戳选项时的接收端RTT计算 */ tcp_rcv_rtt_measure(tp); now = tcp_time_stamp; /* 如果是第一次接收到带负荷的报文 */ if (!icsk->icsk_ack.ato) { /* The _first_ data packet received, initialize * delayed ACK engine. */ tcp_incr_quickack(sk);/* 设置在快速确认模式中可以发送的ACK数量 */ icsk->icsk_ack.ato = TCP_ATO_MIN;/* ato的初始值,为40ms */ } else { int m = now - icsk->icsk_ack.lrcvtime;/* 距离上次收到数据报的时间间隔 */ /* 同时根据距离上次接收到数据报的时间间隔,来动态调整icsk->icsk_ack.ato: 1. delta <= TCP_ATO_MIN /2时,ato = ato / 2 + TCP_ATO_MIN / 2。 2.?TCP_ATO_MIN / 2 < delta <= ato时,ato = min(ato / 2 + delta, rto)。 3. delta > ato时,ato值不变。 如果接收到的数据包的时间间隔变小,ato也会相应的变小。 如果接收到的数据包的时间间隔变大,ato也会相应的变大。 */ if (m <= TCP_ATO_MIN / 2) { /* The fastest case is the first. */ icsk->icsk_ack.ato = (icsk->icsk_ack.ato >> 1) + TCP_ATO_MIN / 2; } else if (m < icsk->icsk_ack.ato) { icsk->icsk_ack.ato = (icsk->icsk_ack.ato >> 1) + m; if (icsk->icsk_ack.ato > icsk->icsk_rto) icsk->icsk_ack.ato = icsk->icsk_rto; } else if (m > icsk->icsk_rto) { /* Too long gap. Apparently sender failed to * restart window, so that we send ACKs quickly. */ tcp_incr_quickack(sk); sk_mem_reclaim(sk); } } icsk->icsk_ack.lrcvtime = now;/* 更新最后一次接收到数据报的时间 */ tcp_ecn_check_ce(tp, skb); /* 如果发现显示拥塞了,就进入快速确认模式 */ /* 当报文段的负荷不小于128字节时,考虑增大接收窗口当前阈值 */ if (skb->len >= 128) tcp_grow_window(sk, skb); /* 根据接收到的数据段的大小,来调整接收窗口的阈值rcv_ssthresh */ }

       MSS,最大报文段长度

    在连接建立的时候,即在发送 SYN 段的时候,    同时会将 MSS 发送给对方(MSS 选项只能出现在 SYN 段中!!!),    告诉对端它期望接收的 TCP 报文段数据部分最大长度。

    • 建立MSS所基于的MTU的值基于路径MTU发现机制获取------------------------多好  

     那么MTU和MSS又有什么必然联系呢?

    虽然MTU限制了IP层的报文大小, 但分层网络模型本来不就是为了对上层提供透明的服务么?即使一个很大的TCP报文传递给IP层, IP层也应该可以经过分段等手段成功传输报文才对。理论上来说是没错的,UDP中就不存在MSS,UDP生成任意大的UDP报文,然后包装成IP报文根据底层网络的MTU分段进行发送。MSS存在的本质原因就是TCP和UDP的根本不同:TCP提供稳定的连接。假设生成了很大的TCP报文,经过IP分段进行发送,而其中一个IP分段丢失了,则TCP协议需要重发整个TCP报文,造成了严重的网络性能浪费,而相对的由于UDP无保证的性质,即使丢失了IP分段也不会进行重发。所以说,MSS存在的核心作用,就是避免由于IP层对TCP报文进行分段而导致的性能下降。

    /* Adapt the MSS value used to make delayed ack decision to the
     * real world.
     */
    static void tcp_measure_rcv_mss(struct sock *sk, const struct sk_buff *skb)
    {
        struct inet_connection_sock *icsk = inet_csk(sk);
        const unsigned int lss = icsk->icsk_ack.last_seg_size;/* 上次收到的数据段大小 */
        unsigned int len;
    
        icsk->icsk_ack.last_seg_size = 0;
    
        /* skb->len may jitter because of SACKs, even if peer
         * sends good full-sized frames.
         */
        len = skb_shinfo(skb)->gso_size ? : skb->len;/// 本次接收到数据的长度
        //如果本次接收到数据的长度,大于当前发送方的MSS
        /*
        MSS 是软件层的概念,它是由软件控制的
        MTU 是硬件(比如网卡出口)的属性,是指二层链路层帧携带的数据最大大小。
    
        */
        if (len >= icsk->icsk_ack.rcv_mss) {
            icsk->icsk_ack.rcv_mss = min_t(unsigned int, len,
                               tcp_sk(sk)->advmss);
            /* Account for possibly-removed options */
            if (unlikely(len > icsk->icsk_ack.rcv_mss +
                       MAX_TCP_OPTION_SPACE))
                tcp_gro_dev_warn(sk, skb, len);
        } else {
            /* Otherwise, we make more careful check taking into account,
             * that SACKs block is variable.
             *
             * "len" is invariant segment length, including TCP header.
             *///之前的len表示数据的长度,现在加上TCP首部的长度,这才是总的长度
            len += skb->data - skb_transport_header(skb);
            if (len >= TCP_MSS_DEFAULT + sizeof(struct tcphdr) ||
                /* If PSH is not set, packet should be
                 * full sized, provided peer TCP is not badly broken.
                 * This observation (if it is correct 8)) allows
                 * to handle super-low mtu links fairly.
                 
                 满足以下条件时,说明接收到的数据段还是比较正常的,尝试更精确的计算MSS,
                          * 排除SACK块的影响,更新last_seg_size和rcv_mss。
                 */
                (len >= TCP_MIN_MSS + sizeof(struct tcphdr) &&
                 !(tcp_flag_word(tcp_hdr(skb)) & TCP_REMNANT))) {
                /* Subtract also invariant (if peer is RFC compliant),
                 * tcp header plus fixed timestamp option length.
                 * Resulting "len" is MSS free of SACK jitter.
                 *///减去报头和时间戳选项的长度,剩下的就是数据和SACK块(如果有的话)
                len -= tcp_sk(sk)->tcp_header_len;
                icsk->icsk_ack.last_seg_size = len;//更新最近一次接收到的数据段的长度 
                if (len == lss) {//说明这次收到的还是full-sized,而不是小包
                    icsk->icsk_ack.rcv_mss = len;
                    return;
                }
            }//如果之前已经收到了小包,则进入更紧急的ACK发送模式,接下来无论是否处于快速确认模式,
             // 都可以马上发送ACK。
            if (icsk->icsk_ack.pending & ICSK_ACK_PUSHED)
                icsk->icsk_ack.pending |= ICSK_ACK_PUSHED2;
            icsk->icsk_ack.pending |= ICSK_ACK_PUSHED;
        }
    }

    此函数的原理:

    我们知道发送端不可能在一个RTT期间发送大于一个通告窗口的数据量。那么接收端可以把接收一个确认窗口的数据量(rcv_wnd)所用的时间作为RTT。接收端收到一个数据段,然后发送确认(确认号为rcv_nxt,通告窗口为rcv_wnd),开始计时,RTT就是收到序号为rcv_nxt + rcv_wnd的数据段所用的时间。很显然,这种假设并不准确,测量所得的RTT会偏大一些。所以这种方法只有当没有采用时间戳选项时才使用,而内核默认是采用时间戳选项的(tcp_timestamps为1)。

    /*
    此函数的原理:我们知道发送端不可能在一个RTT期间发送大于一个通告窗口的数据量。
    那么接收端可以把接收一个确认窗口的数据量(rcv_wnd)所用的时间作为RTT。接收端收到一个数据段,
    然后发送确认(确认号为rcv_nxt,通告窗口为rcv_wnd),开始计时,RTT就是收到序号为rcv_nxt + rcv_wnd的数据段所用的时间。
    很显然,这种假设并不准确,测量所得的RTT会偏大一些。所以这种方法只有当没有采用时间戳选项时才使用,
    而内核默认是采用时间戳选项的(tcp_timestamps为1)。
    下面是一段对此方法的评价:
    If the sender is being throttled by the network, this estimate will be valid. However, if the sending application did nothave any data to send, 
    the measured time could be much larger than the actual round-trip time. Thus this measurementacts only as an upper-bound on the round-trip time.
    ————————————————
    
    
    */
    static inline void tcp_rcv_rtt_measure(struct tcp_sock *tp)
    {
        u32 delta_us;
        /* 第一次接收到数据时,需要对相关变量初始化*/
    
        if (tp->rcv_rtt_est.time.v64 == 0)
            goto new_measure;
         /* 收到指定的序列号后,才能获取一个RTT测量样本*/
        if (before(tp->rcv_nxt, tp->rcv_rtt_est.seq))
            return;
         /* RTT的样本:jiffies - tp->rcv_rtt_est.time */
        delta_us = skb_mstamp_us_delta(&tp->tcp_mstamp, &tp->rcv_rtt_est.time);
        tcp_rcv_rtt_update(tp, delta_us, 1);
    
    new_measure:
        tp->rcv_rtt_est.seq = tp->rcv_nxt + tp->rcv_wnd;
        tp->rcv_rtt_est.time = tp->tcp_mstamp;
    }
    static void __tcp_ecn_check_ce(struct tcp_sock *tp, const struct sk_buff *skb)
    {
        switch (TCP_SKB_CB(skb)->ip_dsfield & INET_ECN_MASK) {
        case INET_ECN_NOT_ECT:/* IP层不支持ECN */
            /* Funny extension: if ECT is not set on a segment,
             * and we already seen ECT on a previous segment,
             * it is probably a retransmit.
             */
            if (tp->ecn_flags & TCP_ECN_SEEN)
                tcp_enter_quickack_mode((struct sock *)tp);
            break;
        case INET_ECN_CE:/* 数据包携带拥塞标志 */
            if (tcp_ca_needs_ecn((struct sock *)tp))
                tcp_ca_event((struct sock *)tp, CA_EVENT_ECN_IS_CE);
    
            if (!(tp->ecn_flags & TCP_ECN_DEMAND_CWR)) {
                /* Better not delay acks, sender can have a very low cwnd */
                tcp_enter_quickack_mode((struct sock *)tp);//进入快速确认模式
                tp->ecn_flags |= TCP_ECN_DEMAND_CWR;/* 用于让对端感知拥塞的标志 */
            }
            tp->ecn_flags |= TCP_ECN_SEEN;
            break;
        default:
            if (tcp_ca_needs_ecn((struct sock *)tp))
                tcp_ca_event((struct sock *)tp, CA_EVENT_ECN_NO_CE);
            tp->ecn_flags |= TCP_ECN_SEEN;
            break;
        }
    }
    
    static void tcp_ecn_check_ce(struct tcp_sock *tp, const struct sk_buff *skb)
    {
        if (tp->ecn_flags & TCP_ECN_OK) /* 如果连接不支持ECN  则不处理*/ 
            __tcp_ecn_check_ce(tp, skb);
    }
    • #define TCP_MSS_DEFAULT 536U
    • #define TCP_MIN_MSS 88U /* Minimal accepted MSS. It is (60+60+8) - (20+20). */ 
       
       

     关于QUICK计数器

    Linux为TCP的非Delay ACK维护了一个计数器QUICK,该计数器表示在非Delay ACK模式下,发送的即时ACK的数量,也就是即时ACK的配额,在pingpong为0的前提下,只有QUICK持有配额(不为0),该即时ACK才可以被发送出去,否则该ACK会被Delay!

    QUICK计数器值在什么情况下会增减

    1. 在连接初始化后,当第一次收到数据的时候
      此时会将QUICK配额增加到最多16个段。配备16个段的配额是为了照顾慢启动,保证发送端在慢启动阶段时,接收端有足够的QUIICK配额发送即时的ACK。
    2. 当自从上一次接收到数据到现在又一次收到数据的时间间隔太久的时候
      此时会将QUICK配额增加到最多16个段。此时配备足量的QUICK配额是为了发送即时的ACK以促使发送端尽快发送数据。
    3. 当接收端窗口缩减的时候
      此时会将QUIICK配额清零。这种情况下,内存可以已经吃紧,尽量延缓接收数据是有益的,所以要减缓TCP时钟,延迟ACK的发送。
    4. 当接收端窗口小于接收端缓存一半的时候
      此时会将QUIICK配额清零。


  • 相关阅读:
    玩聚SD:感谢曹增辉的博客点评
    Social Dialogue征集IT意见领袖和优秀博客的RSS地址
    微软+Powerset>GoogleAdSense还是>GoogleSearch?
    1989旧金山地震:动物预测成功的非经典案例
    随手小记·危机来了与贪婪恐惧
    玩聚SD:感谢风言疯语之IT罗盘对玩聚SD的推荐
    独立思考之慎用孤例
    08软件技术英雄会:一次比一次接近完美
    独立思考之手动check
    MyBatisSpring MapperScannerConfigurer
  • 原文地址:https://www.cnblogs.com/codestack/p/11920828.html
Copyright © 2011-2022 走看看