• tcp中delay_ack的理解


    内核版本,3.10。 首先,我们需要知道,在一个sock中,维护ack的就有很多变量,多种状态:

    struct inet_connection_sock {
    。。。。
        __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 {
            __u8          pending;     /* ACK is pending               */-----------------------pending有很多标志,如IACK_ACK_TIMER,IACK_ACK_PUSHED
            __u8          quick;     /* Scheduled number of quick acks       */
            __u8          pingpong;     /* The session is interactive           */--------------为1,说明是交互型tcp流,为0则意味着基本是单向流
            __u8          blocked;     /* Delayed ACK was blocked by socket lock */-------------如果delayed ack在timer中被用户阻塞,则设置为1
            __u32          ato;         /* Predicted tick of soft clock       */----------------用来计算delay_ack超时的中间变量
            unsigned long      timeout;     /* Currently scheduled timeout           */---------当前delay_ack的超时定时时长
            __u32          lrcvtime;     /* timestamp of last received data packet */-----------最新收到的报文的时戳
            __u16          last_seg_size; /* Size of last incoming segment       */
            __u16          rcv_mss;     /* MSS used for delayed ACK decisions       */ 
        } icsk_ack;

     

    其中,icsk_ack.pending 就有多种状态组合:
    enum inet_csk_ack_state_t {
        ICSK_ACK_SCHED    = 1,---------------说明ack需要被快速发送而没有被发送,但这个标志在设置timer的时候也会设置
        ICSK_ACK_TIMER  = 2,-----------------说明设置了delay_ack的timer
        ICSK_ACK_PUSHED = 4,-----------------说明需要将ack快点发送
        ICSK_ACK_PUSHED2 = 8-----------------在已经设置了ICSK_ACK_PUSHED的情况下,tcp_mesure_rcv_mss会设置这个标志
    };
    tcp_rcv_state_process 在当tcp收到数据的时候,如果正常,会经历两个函数:
        /* tcp_data could move socket to TIME-WAIT */
        if (sk->sk_state != TCP_CLOSE) {
            tcp_data_snd_check(sk);-----------看是否有数据也需要发送出去
            tcp_ack_snd_check(sk);------------看是否需要发送ack
        }

    因为收到数据,有两种选择,要么立刻回复ack,要么进行delay_ack的。

    在具体实现中,用pingpong来区分这两种模式:

    icsk->icsk_ack.pingpong == 0,表示使用快速确认,因为既然不是pingpong模式,说明ack没必要等,但是如果quick的阈值用完了,那么还是会延迟确认,哪怕pingpong =0.

    icsk->icsk_ack.pingpong == 1,表示使用延迟确认。

    delay_ack的好处是可以在网络上减少一点小包,比如可以和本端数据一起发送,比如可以收到N个报文,但只回复1个ack。当然任何一个特性,有好处自然也会带来坏处。delay_ack也不例外,毕竟增加了时延。

    我们来看正常情况下发送ack的条件:

    static void __tcp_ack_snd_check(struct sock *sk, int ofo_possible)
    {
        struct tcp_sock *tp = tcp_sk(sk);
    
            /* More than one full frame received... */
        if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss &&//我们收到的报文量大于一个mss了,
             /* ... and right edge of window advances far enough.
              * (tcp_recvmsg() will send ACK otherwise). Or...
              */
             __tcp_select_window(sk) >= tp->rcv_wnd) ||//---------------我们需要更新接收窗口,也就是我们接收窗口在不断变化的期间,一般就是链路将建立没多久的时候
            /* We ACK each frame or... */
            tcp_in_quickack_mode(sk) ||---------------------------------我们处于quickack状态
            /* We have out of order data. */
            (ofo_possible && skb_peek(&tp->out_of_order_queue))) {------我们收到了乱序报文
            /* Then ack it now */
            tcp_send_ack(sk);-------------------------------------------立即发送ack,不等待
        } else {
            /* Else, send delayed ack. */
            tcp_send_delayed_ack(sk);-----------------------------------否则,我们会发送delay_ack
        }
    }

    那么是不是tcp_send_delay_ack就一定不会立刻发送ack呢,也不是的,不要被这个函数名称骗了:

    void tcp_send_delayed_ack(struct sock *sk)
    {
        struct inet_connection_sock *icsk = inet_csk(sk);
        int ato = icsk->icsk_ack.ato;-------------计算下次应该超时的时间
        unsigned long timeout;
    
        tcp_ca_event(sk, CA_EVENT_DELAYED_ACK);
    
        if (ato > TCP_DELACK_MIN) {----------------------如果大于40ms的话
            const struct tcp_sock *tp = tcp_sk(sk);
            int max_ato = HZ / 2;------------------------500ms
    
            if (icsk->icsk_ack.pingpong ||---------------------处于交互模式,
                (icsk->icsk_ack.pending & ICSK_ACK_PUSHED))-------------ack需要立刻发送,这个地方很奇怪,按道理此时max_ato应该设置小点才对。
                max_ato = TCP_DELACK_MAX;----------------------能delay的话尽量delay,所以修改该值为200ms,
    
            /* Slow path, intersegment interval is "high". */
    
            /* If some rtt estimate is known, use it to bound delayed ack.
             * Do not use inet_csk(sk)->icsk_rto here, use results of rtt measurements
             * directly.
             */
            if (tp->srtt_us) {-----------------能利用rtt的话
                int rtt = max_t(int, usecs_to_jiffies(tp->srtt_us >> 3),-------这个>>3就是算法里面的计算rtt时的1/8权值,也就是rtt=old_rtt*7/8+new_rtt*1/8 
                        TCP_DELACK_MIN);----------最大也就是40ms
    
                if (rtt < max_ato)
                    max_ato = rtt;-----------------------rtt小于max_ato,再次修改max_ato
            }
    
            ato = min(ato, max_ato);---------------------确认最终ato
        }
    
        /* Stay within the limit we were given */
        timeout = jiffies + ato;--------------------------定了超时时间了,
    
        /* Use new timeout only if there wasn't a older one earlier. */
        if (icsk->icsk_ack.pending & ICSK_ACK_TIMER) {-----------之前还有一个延迟ack的定时器没到期
            /* If delack timer was blocked or is about to expire,
             * send ACK now.
             */
            if (icsk->icsk_ack.blocked ||---------------------被阻塞过,这个只在延迟确认定时器到期时,如果sock被user给lock住,则会设置会1,表示本该发送的ack没发
                time_before_eq(icsk->icsk_ack.timeout, jiffies + (ato >> 2))) {//timer快到期了,也就是小于当前时间+ato/4的时间的话,干脆不等了。
                tcp_send_ack(sk);----------------立刻发送ack,别等了,可以看到delay_ack的定时器也没有取消
                return;
            }
    
            if (!time_before(timeout, icsk->icsk_ack.timeout))
                timeout = icsk->icsk_ack.timeout;
        }
        icsk->icsk_ack.pending |= ICSK_ACK_SCHED | ICSK_ACK_TIMER;-----------------设置标志,表明有一个延迟ack的定时器被设置了。
        icsk->icsk_ack.timeout = timeout;
        sk_reset_timer(sk, &icsk->icsk_delack_timer, timeout);------------重新设置延迟ack的定时器,超时时间是每次算出来的,
    }

     从上面的计算可以看出,延迟ack的timeout时间不是简单地设置为40ms拉倒,虽然它默认值在HZ大于100的时候是设置为40ms。所以如果你分析报文的时候,如果抓包发现

    delay_ack不是40ms,不要慌,看看上面这个函数计算timeout的方式,它其实是一个40ms ~ min(200ms, RTT)的动态值,不过我抓包看到过delay_ack有时候不到10ms,跟算法

    不匹配,不知道为啥。

    Q:tcp_send_delayed_ack 函数中,delay_ack的timeout 是由ato决定的,那么icsk_ack.ato 的计算方式是?
    A:主要的函数在tcp_event_data_recv 中,
    icsk_ack.ato 初始化为0,然后在第一次收包的时候修改为 TCP_ATO_MIN,也就是40ms,之后每次收包的时候计算,
    假设delta为这次收包到上次收包的间隔,则算法为:

    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的值,最大也不会超过rto。rto的最小的默认值是1s,所以ato最大不会超过1s。

    当然这个ato的值并不是直接作用于delay_ack的timeout,具体可以 参照 tcp_send_delayed_ack 函数。

    Q:发送ack的函数为?

    A:发送ack的函数是:tcp_send_ack-->tcp_transmit_skb-->icsk->icsk_af_ops->queue_xmit,到ip层就离开了tcp了

    设置delay_ack的timer:

    负责设置延时ack的timer的函数为tcp_delack_timer_handler:

    static inline void inet_csk_reset_xmit_timer(struct sock *sk, const int what,
                             unsigned long when,
                             const unsigned long max_when)
    {
    。。。
        } else if (what == ICSK_TIME_DACK) {
            icsk->icsk_ack.pending |= ICSK_ACK_TIMER;
            icsk->icsk_ack.timeout = jiffies + when;
            sk_reset_timer(sk, &icsk->icsk_delack_timer, icsk->icsk_ack.timeout);
        }
    。。。。
    }

    由于 tcp_transmit_skb 是一个公共函数,所以在判断是发送ack的时候,用的是这个判断:

    if (likely(tcb->tcp_flags & TCPHDR_ACK))//发送的是带ack
            tcp_event_ack_sent(sk, tcp_skb_pcount(skb));

    tcp_event_ack_sent主要做什么?
    static inline void tcp_event_ack_sent(struct sock *sk, unsigned int pkts)
    {
        tcp_dec_quickack_mode(sk, pkts);//每发送一次ack,会减少quick的值,也就是系统倾向于delayack的。
        inet_csk_clear_xmit_timer(sk, ICSK_TIME_DACK);
    }

     主要就是减少quick的计数。在非quickack模式下。除此之外, inet_csk_clear_xmit_timer 函数并不仅仅是删除timer,还需要做一个跟qiuckack相关的东西:

    static inline void inet_csk_clear_xmit_timer(struct sock *sk, const int what)
    {。。。。
    else if (what == ICSK_TIME_DACK) {
            icsk->icsk_ack.blocked = icsk->icsk_ack.pending = 0;
    #ifdef INET_CSK_CLEAR_TIMERS
            sk_stop_timer(sk, &icsk->icsk_delack_timer);
    #endif
        }
    。。。。}

    可以看到,会将  icsk->icsk_ack.blocked 和 icsk->icsk_ack.pending 都设置为0。



    tcp_data_snd_check对delayack的影响:
    要知道,一个tcp流要满足pingpong模式,显然应该有收有发,而且收发的频率还应该相当才对,就像人们打乒乓球一样。我们来看 tcp_data_snd_check 对delayack相关状态的影响:
    /* 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
     */
    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状态
    
        tcp_measure_rcv_mss(sk, skb);//计算mss
    
        tcp_rcv_rtt_measure(tp);//计算rtt
    
        now = tcp_time_stamp;
    
        if (!icsk->icsk_ack.ato) {
            /* The _first_ data packet received, initialize
             * delayed ACK engine.
             */
            tcp_incr_quickack(sk);
            icsk->icsk_ack.ato = TCP_ATO_MIN;
        } else {
            int m = now - icsk->icsk_ack.lrcvtime;
    
            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);//增加快速ack的计数,前提非常难得,就是收包间隔大于重传定时器才进这个分支
                sk_mem_reclaim(sk);
            }
        }
        icsk->icsk_ack.lrcvtime = now;//更新收到报文的最新时间
    
        TCP_ECN_check_ce(tp, skb);
    
        if (skb->len >= 128)
            tcp_grow_window(sk, skb);//更新窗口
    }

    从这个函数的注释可以看出,当我们收到报文的时候,如果是一个链路的发起阶段,由于很多对端会启动慢启动流程,这样我们的ack需要快速发回,

    这样对端可以增加它的snd_cwnd,然后更快地发包,所以说一个tcp连接,在没有明确setsockopt调用关闭quickack的情况下,应该是默认处于quickack的回复状态。

    不过这种状态是有一定的阈值的,也就是 icsk->icsk_ack.quick 的值是有一个上限,一般最大为TCP_MAX_QUICKACKS=16,这么做主要就是为了加速slowstart的发包,因为ack回得越快,越能告诉服务器端客户端的最新情况和网络的情况。当然也更用户设置的

    ,且每发送一个ack,还会减少若干个阈值,,慢慢过渡到delay_ack流程,为了防止避免进入delay_ack,在 tcp_incr_quickack 中,而负责减少quick计数的函数是:

    static inline void tcp_dec_quickack_mode(struct sock *sk,
                         const unsigned int pkts)
    {
        struct inet_connection_sock *icsk = inet_csk(sk);
    
        if (icsk->icsk_ack.quick) {//处于quick模式下,更新quickack的计数,递减,
            if (pkts >= icsk->icsk_ack.quick) {
                icsk->icsk_ack.quick = 0;-------------------阈值不够了,进入delay_ack状态
                /* Leaving quickack mode we deflate ATO. */
                icsk->icsk_ack.ato   = TCP_ATO_MIN;---------初始timer设置为40ms
            } else
                icsk->icsk_ack.quick -= pkts;//递减
        }
    }

    既然quickack是一个动态值,而且是慢慢减少,说明系统是倾向于delay_ack的

    Q:如何关闭delay_ack

    A:如果用户明确知道这条tcp链路是非pingpong模式,那么可以使用 TCP_QUICKACK 来设置socket属性,

    case TCP_QUICKACK:
            if (!val) {
                icsk->icsk_ack.pingpong = 1;
            } else {
                icsk->icsk_ack.pingpong = 0;
                if ((1 << sk->sk_state) &
                    (TCPF_ESTABLISHED | TCPF_CLOSE_WAIT) &&
                    inet_csk_ack_scheduled(sk)) {
                    icsk->icsk_ack.pending |= ICSK_ACK_PUSHED;--------设置要求推送ack的标志,说明
                    tcp_cleanup_rbuf(sk, 1);
                    if (!(val & 1))
                        icsk->icsk_ack.pingpong = 1;
                }
            }

    但由于delay_ack并不是一个固定值,是不停在计算的,所以在用户态程序需要不断设置TCP_QUICKACK  ,当然我也觉得这个不合理,完全可以持久化。

    总结一下:

    Q:什么时候进行快速确认?

    A:总结如下:

    1、 接收到数据包,检查是否需要发送ACK时 (__tcp_ack_snd_check):

    1. 接收缓冲区中有一个以上的全尺寸数据段仍然是NOT ACKed,并且接收窗口变大了。

        所以一般收到了两个数据包后,会发送ACK,而不是对每个数据包都进行确认,但是如果我们收到的是2个小包,尺寸加起来还是小于MSS,也不会继续等,因为收到小包的话,

    则会设置ICSK_ACK_PUSHED标志,第二次再收到小包,则设置ICSK_ACK_PUSHED2,这个在 tcp_measure_rcv_mss 函数中实现,则极大概率会立刻发送ack,此处的极大概率是指,

    当我们发送ack的时候,如果出现内存不足,skb申请失败,则只能再次设置delay_ack,真tm复杂。还有,这个跟内核版本也有关系,不要混淆了前提,比如2.6的内核这个行为又不同。

    2.  接收到数据包时,仍然处于快速确认模式中。也就是icsk_ack.quick配额还没有消耗完。

    3. 接收到数据包时,乱序队列不为空,且传给 __tcp_ack_snd_check 的乱序与否的参数为1.

    4.当接收队列中有数据复制到用户空间时,会判断是否要立即发送ACK,tcp_cleanup_rbuf 函数,

    if (inet_csk_ack_scheduled(sk)) {
            const struct inet_connection_sock *icsk = inet_csk(sk);
               /* Delayed ACKs frequently hit locked sockets during bulk
                * receive. */
            if (icsk->icsk_ack.blocked ||
                /* Once-per-two-segments ACK was not sent by tcp_input.c */
                tp->rcv_nxt - tp->rcv_wup > icsk->icsk_ack.rcv_mss ||
                /*
                 * If this read emptied read buffer, we send ACK, if
                 * connection is not bidirectional, user drained
                 * receive buffer and there was a small segment
                 * in queue.
                 */
                (copied > 0 &&
                 ((icsk->icsk_ack.pending & ICSK_ACK_PUSHED2) ||
                  ((icsk->icsk_ack.pending & ICSK_ACK_PUSHED) &&
                   !icsk->icsk_ack.pingpong)) &&
                  !atomic_read(&sk->sk_rmem_alloc)))
                time_to_ack = true;
        }
    
        /* We send an ACK if we can now advertise a non-zero window
         * which has been raised "significantly".
         *
         * Even if window raised up to infinity, do not send window open ACK
         * in states, where we will not receive more. It is useless.
         */
        if (copied > 0 && !time_to_ack && !(sk->sk_shutdown & RCV_SHUTDOWN)) {
            __u32 rcv_window_now = tcp_receive_window(tp);
    
            /* Optimize, __tcp_select_window() is not cheap. */
            if (2*rcv_window_now <= tp->window_clamp) {
                __u32 new_window = __tcp_select_window(sk);
    
                /* Send ACK now, if this read freed lots of space
                 * in our buffer. Certainly, new_window is new window.
                 * We can advertise it now, if it is not less than current one.
                 * "Lots" means "at least twice" here.
                 */
                if (new_window && new_window >= 2 * rcv_window_now)
                    time_to_ack = true;
            }
        }
        if (time_to_ack)
            tcp_send_ack(sk);

    从这几个条件看,由于每发送ack都会进行quick配额的减少,即 tcp_dec_quickack_mode 函数,所以快速确认的几率其实不高的。

    Q:什么时候进行delay_ack

    1. 快速确认模式中的ACK额度用完了,一般在快速确认了半个接收窗口的数据后,进入延迟确认模式。

    2. 发送ACK时,因为内存分配失败,启动延迟确认定时器,希望过一会能申请到内存,我觉得这个应该优化为使用mem_pool,至少让服务器端知道这边内存不够,延迟那么几十毫秒意义不大,因为内存不会变化那么剧烈的。

    3. 接收到数据包,检查是否需要发送ACK时(__tcp_ack_snd_check),如果无法进行快速确认。

    4. 使用TCP_QUICKACK选项禁用快速确认,设置的值为0。

    Q:什么时候进入quickack模式?

    A:主要搜索 tcp_enter_quickack_mode 函数,要注意进入quickack模式和quickack的区别。在quickack模式下,不一定能立刻发ack,因为可能申请不到内存,在delay_ack模式下,也有可能不等定时器超时而立刻发送ack,所以我理解立刻发送ack和quickack模式是有关联的,而不是必然的关系。

    1、TCP_ECN_check_ce 函数,数据包含有路由器的显式拥塞通知,进入快速确认模式。

    2、应用进程显式设置TCP_QUICKACK选项之后:进入快速确认模式,并立即发送一个ACK。

    3、在收到重复的带负荷的数据段时,这个需要认为我们的ack服务器没有收到,则立刻dup_ack给发送方,tcp_send_dupack 函数中,并立即发送一个ack。

    参考资料:

    https://www.rfc-editor.org/rfc/rfc5681.txt

    https://blog.csdn.net/zhangskd/article/details/45127565 

    水平有限,如果有错误,请帮忙提醒我。如果您觉得本文对您有帮助,可以点击下面的 推荐 支持一下我。版权所有,需要转发请带上本文源地址,博客一直在更新,欢迎 关注 。
  • 相关阅读:
    python--函数的返回值、函数的参数
    python--字典,解包
    Vue--ElementUI实现头部组件和左侧组件效果
    Vue--整体页面布局
    jmeter--non GUI
    python--切片,字符串操作
    celery--调用异步任务的三种方法和task参数
    celery--实现异步任务
    celery--介绍
    开发问题记录
  • 原文地址:https://www.cnblogs.com/10087622blog/p/10315410.html
Copyright © 2020-2023  润新知