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 

水平有限,如果有错误,请帮忙提醒我。如果您觉得本文对您有帮助,可以点击下面的 推荐 支持一下我。版权所有,需要转发请带上本文源地址,博客一直在更新,欢迎 关注 。
原文地址:https://www.cnblogs.com/10087622blog/p/10315410.html