• tcpack--4延时ack


    TCP在收到数据后必须发送ACK给对端,但如果每收到一个包就给一个ACK的话会使得网络中被注入过多报文。TCP的做法是在收到数据时不立即发送ACK,而是设置一个定时器,如果在定时器超时之前有数据发送给对端,则ACK会被携带在数据中捎带过去;超时则由定时器发送ACK。这样就减少了报文的发送,提高了协议的效率。

    延迟确认

    在tcp_transmit_skb 中如果skb->len != tcp_header_size 也就是有载荷时,就会调用下述函数处理延迟确认事宜

    /* Congestion state accounting after a packet has been sent.
    如果此时距离最近接收到数据包的时间间隔足够短,
    说明双方处于你来我往的双向数据传输中,就进入延迟确认模式
    icsk->icsk_ack.ato在ACK的发送过程中扮演了重要角色,作用是??
    A:ato为ACK Timeout,指ACK的超时时间。但延迟确认定时器的超时时间为icsk->icsk_ack.timeout,
    ato只是计算timeout的一个中间变量,会根接收到的数据包的时间间隔来做动态调整。
    一般如果接收到的数据包的时间间隔变小,ato也会相应的变小。
    如果接收到的数据包的时间间隔变大,ato也会相应的变大。
    ato的最小值为40ms,ato的最大值一般为200ms或一个RTT。
    所以在实际传输过程中,我们看到的ACK的超时时间,是处于40ms ~ min(200ms, RTT)之间的
    在tcp_send_delayed_ack()中会把ato赋值给icsk->icsk_ack.timeout,用作延迟确认定时器的超时时间。
    */
    static void tcp_event_data_sent(struct tcp_sock *tp,
                    struct sock *sk)
    {
        struct inet_connection_sock *icsk = inet_csk(sk);
        const u32 now = tcp_time_stamp;
    
        if (tcp_packets_in_flight(tp) == 0)
            tcp_ca_event(sk, CA_EVENT_TX_START);/* first transmit when no packets in flight */
    
        tp->lsndtime = now;/* 更新最近发送数据包的时间*/
    
        /* If it is a reply for ato after last received
         * packet, enter pingpong mode.
         *///如果距离上次接收到数据包的时间在ato内,则进入延迟确认模式
        if ((u32)(now - icsk->icsk_ack.lrcvtime) < icsk->icsk_ack.ato)
            icsk->icsk_ack.pingpong = 1;
    }

    延时确认定时器

    icsk->icsk_delack_timer的激活函数为inet_csk_reset_xmit_timer(),此函数共负责了5个定时器的激活工作。延迟确认定时器的另一个激活函数为tcp_send_delayed_ack(),用于判断发送快速确认还是延迟确认。

    else if (what == ICSK_TIME_DACK) {
            icsk->icsk_ack.pending |= ICSK_ACK_TIMER;/* 延迟确认定时器启动标志 */
            icsk->icsk_ack.timeout = jiffies + when;//Delay ACK定时器超时时刻
            sk_reset_timer(sk, &icsk->icsk_delack_timer, icsk->icsk_ack.timeout);
        }
    /**
     *  tcp_delack_timer() - The TCP delayed ACK timeout handler
     *  @data:  Pointer to the current socket. (gets casted to struct sock *)
     *
     *  This function gets (indirectly) called when the kernel timer for a TCP packet
     *  of this socket expires. Calls tcp_delack_timer_handler() to do the actual work.
     *
     *  Returns: Nothing (void)
     */
    static void tcp_delack_timer(unsigned long data)
    {
        struct sock *sk = (struct sock *)data;
    
        bh_lock_sock(sk); /* 传输控制块未被锁定 */
    // 处于下半部 if (!sock_owned_by_user(sk)) { tcp_delack_timer_handler(sk);/* 调用超时处理函数 */ } else { /* 如果延迟确认定时器触发时,发现用户进程正在使用此socket,就把blocked置为1。 * 之后在接收到新数据、或者将数据复制到用户空间之后,会马上发送ACK。 */ inet_csk(sk)->icsk_ack.blocked = 1; __NET_INC_STATS(sock_net(sk), LINUX_MIB_DELAYEDACKLOCKED); /* deleguate our work to tcp_release_cb()
    if (flags & TCPF_DELACK_TIMER_DEFERRED) {
            tcp_delack_timer_handler(sk);
            __sock_put(sk);
        }
    *///设置标记,等应用程序release_sock的时候调用tcp_delack_timer_handler if (!test_and_set_bit(TCP_DELACK_TIMER_DEFERRED, &sk->sk_tsq_flags))//如果delack推迟执行,也会处理prequeue队列 sock_hold(sk); } bh_unlock_sock(sk); sock_put(sk); }
    /* Called with BH disabled */
    void tcp_delack_timer_handler(struct sock *sk)
    {
        struct tcp_sock *tp = tcp_sk(sk);
        struct inet_connection_sock *icsk = inet_csk(sk);
    
        sk_mem_reclaim_partial(sk);
        /* 关闭或者监听状态 || 未启动延迟ack定时器*/
        if (((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_LISTEN)) ||
            !(icsk->icsk_ack.pending & ICSK_ACK_TIMER))
            goto out;
        /* 尚未达到超时时间,重新设定定时器 */
        if (time_after(icsk->icsk_ack.timeout, jiffies)) {
            sk_reset_timer(sk, &icsk->icsk_delack_timer, icsk->icsk_ack.timeout);
            goto out;
        }
        icsk->icsk_ack.pending &= ~ICSK_ACK_TIMER; /* 清除延迟ack标记 */
    
        if (!skb_queue_empty(&tp->ucopy.prequeue)) { /* prequeue队列不为空 */
            struct sk_buff *skb;
    
            __NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPSCHEDULERFAILED);
            /* 数据包出队,调用接收函数tcp_v4_do_rcv处理包 */
            while ((skb = __skb_dequeue(&tp->ucopy.prequeue)) != NULL)
                sk_backlog_rcv(sk, skb);
            /* 消耗的内存清0 */
            tp->ucopy.memory = 0;
        }
        /* 有ack需要发送 */
        if (inet_csk_ack_scheduled(sk)) {
            if (!icsk->icsk_ack.pingpong) {//不能延时,但却有ack被推迟了
                /* Delayed ACK missed: inflate ATO.   即时ack模式  计算超时时间*/
                icsk->icsk_ack.ato = min(icsk->icsk_ack.ato << 1, icsk->icsk_rto);
            } else { /* 延迟ack模式 */
                /* Delayed ACK missed: leave pingpong mode and
                 * deflate ATO.
                 */ /* 重置超时时间以及设置为快速ack模式 */
                icsk->icsk_ack.pingpong = 0;
                icsk->icsk_ack.ato      = TCP_ATO_MIN;
            }
            tcp_send_ack(sk); /* 发送ack */
            __NET_INC_STATS(sock_net(sk), LINUX_MIB_DELAYEDACKS);
        }
    
    out:
        if (tcp_under_memory_pressure(sk))
            sk_mem_reclaim(sk);
    }

      注意 tcp_recvmsg读完sk_receive_queue之后,如果没有读满应用程序缓存,则会处理prequeue和backlog队列,并从中读取。
    另外,及时读满了缓存,也会在函数退出前,处理prequeue和backlog队列。

    • 为什么不在协议栈中直接处理?
      1. cache的角度
        prequeue和backlog都是为了在应用程序上下文去处理数据包。
        因为softirq上下文和应用程序上下文之间切换,会造成cache刷新。
        比如ksoftirqd和应用程序不在一个cpu上; 或是协议栈处理完后通知应用程序,应用程序被唤醒,同样会造成造成cache刷新

      2. ack的角度
        如果直接在协议栈快速处理包,则很可能导致快速ack,使对方快速达到很大的发送速率,但是本地用户进程可能并不活跃,就会导致接收缓存满了,对方瞬间停止发送。 造成很明显的抖动。
        因此在应用程序处理数据包和ack的发送,可以反映用户进程的实时情况。
        但同时对突发的小包和需要低延迟的消息,放入prequeue中,也会造成非常不好的影响

      3. 锁的角度
        softirq中不能睡眠,也不应该跟应用程序抢锁, 如果tcp_recvmsg读取一大块内存,tcp_v4_rcv就需要很久才能抢到锁。
        因此使用prequeue就可以避免这样的情况。

    http://www.cnhalo.net/2016/07/13/linux-tcp-prequeue-backlog/

    综上所述:应用层也会支持处理数据包,处理数据包后reales_sock时 也会调用 延时确认定时器

    if (flags & TCPF_DELACK_TIMER_DEFERRED) {
            tcp_delack_timer_handler(sk);
            __sock_put(sk);
        }

      设置延迟ACK的时机主要有以下几个:

    (1)发送SYN后收到SYN|ACK时:tcp_rcv_synsent_state_process==》inet_csk_reset_xmit_timer 设置延迟ACK定时器,超时时间200ms

    (2)发送ACK时无法申请skb:tcp_send_ack---》

     (3)有数据放入prequeue队列中时:

    (4)调用__tcp_ack_snd_check函数发送ACK时满足一下条件

    (1)收到少于一个MSS的数据或通告窗口缩小

    (2)没有处于快速ACK模式

    (3)无乱序数据

     

  • 相关阅读:
    后台菜单权限设计实现思路
    Laravel创建模型到指定目录
    laravel框架加载静态资源注意事项
    mysql 快速生成测试数据小技巧
    Lavaral基础实践——文件上传报错
    PHP常量详解:define和const的区别
    开发常用的工具网站
    windows下用navicat远程链接虚拟机Linux下MySQL数据库(测试可行)
    java异常处理机制思路总结
    记录一些比较容易含糊的概念
  • 原文地址:https://www.cnblogs.com/codestack/p/11920678.html
Copyright © 2020-2023  润新知