zoukankan      html  css  js  c++  java
  • TCP Persist 坚持定时器

    1、坚持定时器在接收方通告接收窗口为0,阻止发送端继续发送数据时设定。

    由于连接接收端的发送窗口通告不可靠(只有数据才会确认),如果一个确认丢失了,双方就有可能因为等待对方而使连接终止:

    接收放等待接收数据(因为它已经向发送方通过了一个非0窗口),而发送方在等待允许它继续发送数据的窗口更新。

    为了防止上面的情况,发送方在接收到0窗口通告后,启动一个坚持定时器来周期的发送1字节的数据,以便发现接收方窗口是否已经增大。

    这些从发送方发出的报文段称为窗口探测;

    Q1:什么时候启动persist 定时器

    1、收到ack的时候-----------------

    static void tcp_ack_probe(struct sock *sk)
    {
        const struct tcp_sock *tp = tcp_sk(sk);
        struct inet_connection_sock *icsk = inet_csk(sk);
    
        /* Was it a usable window open? 
             * 对端是否有足够的接收缓存,即我们能否发送一个包。
             */
    
        if (!after(TCP_SKB_CB(tcp_send_head(sk))->end_seq, tcp_wnd_end(tp))) {
            icsk->icsk_backoff = 0;
            inet_csk_clear_xmit_timer(sk, ICSK_TIME_PROBE0);
            /* Socket must be waked up by subsequent tcp_data_snd_check().
             * This function is not for random using!
             */
        } else { /* 否则根据退避指数重置零窗口探测定时器 */
            unsigned long when = tcp_probe0_when(sk, TCP_RTO_MAX);
    
            inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
                          when, TCP_RTO_MAX);
        }
    }

     

    /* This routine deals with incoming acks, but not outgoing ones. */
    /*
    接收到一个ACK的时候,如果之前网络中没有发送且未确认的数据段,
    本端又有待发送的数据段,说明可能遇到对端接收窗口为0的情况。
    这个时候会根据此ACK是否打开了接收窗口来进行零窗口探测定时器的处理:
    1. 如果此ACK打开接收窗口。此时对端的接收窗口不为0了,可以继续发送数据包。??? 那么清除超时时间的退避指数,删除零窗口探测定时器。
    2. 如果此ACK是接收方对零窗口探测报文的响应,且它的接收窗口依然为0。那么根据指数退避算法,??? 重新设置零窗口探测定时器的下次超时时间,超时时间的设置和超时重传定时器的一样。
    */
    //tcp_ack()用于处理接收到的带有ACK标志的段,会检查是否要删除或重置零窗口探测定时器。
    static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag)
    {
        s/* We very likely will need to access write queue head. *//* We passed data and got it acked, remove any soft error
         * log. Something worked...
         */
         ///* 清零探测次数,所以如果对端有响应ACK,实际上是没有次数限制的 */
        sk->sk_err_soft = 0;
        icsk->icsk_probes_out = 0;
        tp->rcv_tstamp = tcp_time_stamp;
        if (!prior_packets) /* 如果之前网络中没有发送且未确认的数据段 */
            goto no_queue;
    
      
    
    no_queue:
        /* If data was DSACKed, see if we can undo a cwnd reduction. */
        if (flag & FLAG_DSACKING_ACK)
            tcp_fastretrans_alert(sk, acked, is_dupack, &flag, &rexmit);
        /* If this ack opens up a zero window, clear backoff.  It was
         * being used to time the probes, and is probably far higher than
         * it needs to be for normal retransmission.
         */    /* 如果还有待发送的数据段,而之前网络中却没有发送且未确认的数据段,
         * 很可能是因为对端的接收窗口为0导致的,这时候便进行零窗口探测定时器的处理。
         */        /* 如果ACK打开了接收窗口,则删除零窗口探测定时器。否则根据退避指数,给予重置 */
        if (tcp_send_head(sk))
            tcp_ack_probe(sk);
    
        if (tp->tlp_high_seq)
            tcp_process_tlp_ack(sk, ack, flag);
        return 1;
    
    invalid_ack:
        SOCK_DEBUG(sk, "Ack %u after %u:%u
    ", ack, tp->snd_una, tp->snd_nxt);
        return -1;
    
    old_ack:
        /* If data was SACKed, tag it and see if we should send more data.
         * If data was DSACKed, see if we can undo a cwnd reduction.
         */
        if (TCP_SKB_CB(skb)->sacked) {
            flag |= tcp_sacktag_write_queue(sk, skb, prior_snd_una,
                            &sack_state);
            tcp_fastretrans_alert(sk, acked, is_dupack, &flag, &rexmit);
            tcp_xmit_recovery(sk, rexmit);
        }
    
        SOCK_DEBUG(sk, "Ack %u before %u:%u
    ", ack, tp->snd_una, tp->snd_nxt);
        return 0;
    }

     

    2、TCP使用__tcp_push_pending_frames发送数据时:

    /* Push out any pending frames which were held back due to
     * TCP_CORK or attempt at coalescing tiny packets.
     * The socket must be locked by the caller.
     把sk发送队列中所有的skb全部发送出去   
     只发送队列上的第一个SKB采用tcp_push_one 最终都要调用tcp_write_xmit
     */
    void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,
                       int nonagle)
    {
        /* If we are closed, the bytes will have to remain here.
         * In time closedown will finish, we empty the write queue and
         * all will be happy.
         */
        if (unlikely(sk->sk_state == TCP_CLOSE))
            return;
    
        if (tcp_write_xmit(sk, cur_mss, nonagle, 0,
                   sk_gfp_mask(sk, GFP_ATOMIC)))
            tcp_check_probe_timer(sk);
        /*
    当网络中没有发送且未确认的数据包,且本端有待发送的数据包时,启动零窗口探测定时器。
    
    为什么要有这两个限定条件呢?
    
    如果网络中有发送且未确认的数据包,那这些包本身就可以作为探测包,对端的ACK即将到来。
    
    如果没有待发送的数据包,那对端的接收窗口为不为0根本不需要考虑。
        */
    }

     对porbe的分析如下:

    static inline void tcp_check_probe_timer(struct sock *sk)
    {
        if (!tcp_sk(sk)->packets_out && !inet_csk(sk)->icsk_pending)
            inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
                          tcp_probe0_base(sk), TCP_RTO_MAX);
    }
    /* Called with BH disabled */
    void tcp_write_timer_handler(struct sock *sk)
    {
        struct inet_connection_sock *icsk = inet_csk(sk);
        int event;
     /*
             * TCP状态为CLOSE或未定义定时器事件,则
             * 无需作处理。
             */
        if (sk->sk_state == TCP_CLOSE || !icsk->icsk_pending)
            goto out;
    
        if (time_after(icsk->icsk_timeout, jiffies)) {
            sk_reset_timer(sk, &icsk->icsk_retransmit_timer, icsk->icsk_timeout);
            goto out;
        }
    
        event = icsk->icsk_pending;
    
        /*
         * 由于重传定时器和持续定时器功能是共用了
         * 一个定时器实现的,因此需根据定时器事件
         * 来区分激活的是哪种定时器;如果event为
         * ICSK_TIME_RETRANS,则调用tcp_retransmit_timer()进行重传
         * 处理;如果为ICSK_TIME_PROBE0,则调用tcp_probe_timer()
         * 进行持续定时器的处理.
         */
        switch (event) {
        case ICSK_TIME_EARLY_RETRANS:
            tcp_resume_early_retransmit(sk);
            break;
        case ICSK_TIME_LOSS_PROBE:
            tcp_send_loss_probe(sk);
            break;
        case ICSK_TIME_RETRANS:
            icsk->icsk_pending = 0;
            tcp_retransmit_timer(sk);
            break;
        case ICSK_TIME_PROBE0:
            icsk->icsk_pending = 0;
            tcp_probe_timer(sk);
            break;
        }
    
    out:
        sk_mem_reclaim(sk);
    }
    /*
    /*
     * "持续"定时器在对端通告接收窗口为0,阻止TCP继续发送
     * 数据时设定。由于连接对端发送的窗口通告不可靠(只有
     * 数据才会确认,ACK不会确认),允许TCP继续发送数据的后
     * 续窗口更新有可能丢失,因此,如果TCP有数据发送,而
     * 对端通告接收窗口为0,则持续定时器启动,超时后向
     * 对端发送1字节的数据,以判断对端接收窗口是否已打开。
     * 与重传定时器类似,持续定时器的超时值也是动态计算的,
     * 取决于连接的往返时间,在5~60s之间取值。
     * tcp_probe_timer()为持续定时器超时的处理函数。探测定时器就是当接收到对端的window为0的时候,需要探测对端窗口是否变大,
     */ //真正的probe报文发送在tcp_send_probe0中的tcp_write_wakeup             探测定时器在tcp_ack函数中激活, 或者在__tcp_push_pending_frames中的tcp_check_probe_timer激活
    //tcp_write_timer包括数据报重传tcp_retransmit_timer和窗口探测定时器tcp_probe_timer
    static void tcp_probe_timer(struct sock *sk)
    {
        struct inet_connection_sock *icsk = inet_csk(sk);
        struct tcp_sock *tp = tcp_sk(sk);
        int max_probes;
        u32 start_ts;
    /* 
            (1)如果存在发送出去未被确认的段,
            要么被确认返回窗口,要么重传,无需额外构造探测包
            (2)或者发送队列有待发送的段,无数据需要发,
            不关心窗口情况
            则无需另外组织探测数据
        */
        if (tp->packets_out || !tcp_send_head(sk)) {
            icsk->icsk_probes_out = 0;
            return;
        }
    
        /* RFC 1122 4.2.2.17 requires the sender to stay open indefinitely as
         * long as the receiver continues to respond probes. We support this by
         * default and reset icsk_probes_out with incoming ACKs. But if the
         * socket is orphaned or the user specifies TCP_USER_TIMEOUT, we
         * kill the socket when the retry count and the time exceeds the
         * corresponding system limit. We also implement similar policy when
         * we use RTO to probe window in tcp_retransmit_timer().
         */
        start_ts = tcp_skb_timestamp(tcp_send_head(sk));
        if (!start_ts)
            skb_mstamp_get(&tcp_send_head(sk)->skb_mstamp);
        else if (icsk->icsk_user_timeout &&/* 有时间戳则判断是否超过了用户设置时间 */
             (s32)(tcp_time_stamp - start_ts) > icsk->icsk_user_timeout)
            goto abort;
    /* 最大探测次数设置为连接状态的重试次数 */
        max_probes = sock_net(sk)->ipv4.sysctl_tcp_retries2;
         /*
             * TCP协议规定RTT的最大值为120s(TCP_RTO_MAX),因此
             * 可以通过将指数退避算法得出的超时时间与
             * RTT最大值相比,来判断是否需要给对方发送
             * RST。
             *////这里的处理和上面的tcp_write_timeout很类似。
        if (sock_flag(sk, SOCK_DEAD)) {
            const bool alive = inet_csk_rto_backoff(icsk, TCP_RTO_MAX) < TCP_RTO_MAX;
    
            /*
             * 如果连接已断开,套接字即将关闭,则获取在
             * 关闭本端TCP连接前重试次数的上限。
             */  /* 获取在本端关闭tcp前重试次数上限 */
            max_probes = tcp_orphan_retries(sk, alive);
            if (!alive && icsk->icsk_backoff >= max_probes)
                goto abort;
             /*
             * 释放资源,如果该套接字在释放过程中被关闭,
             * 就无需再发送持续探测段了。
             */
            if (tcp_out_of_resources(sk, true))
                return;
        }
     /* 探测次数超过了最大探测次数,错误处理,关闭连接 */
        if (icsk->icsk_probes_out > max_probes) {
    abort:        tcp_write_err(sk);
        } else {
            /* Only send another probe if we didn't close things up. */
            tcp_send_probe0(sk);
        }
    }
    /* Initiate keepalive or window probe from timer. */
    /*
     * tcp_write_wakeup()用来输出持续探测段。如果传输
     * 控制块处于关闭状态,则直接返回失败,否
     * 则传输持续探测段,过程如下:
     * 1)如果发送队列不为空,则利用那些待发送
     *    段来发送探测段,当然这些待发送的段至
     *     少有一部分在对方的接收窗口内。
     * 2)如果发送队列为空,则构造需要已确认,
     *    长度为零的段发送给对端。也就是否则最终会发送序号为snd_una-1,长度为0的ack包
     * 其返回值如下:
     *  0: 表示发送持续探测段成功
     *  小于0: 表示发送持续探测段失败
     *  大于0: 表示由于本地拥塞而导致发送持续探测段失败。
     */
    int tcp_write_wakeup(struct sock *sk, int mib)
    {
        struct tcp_sock *tp = tcp_sk(sk);
        struct sk_buff *skb;
    
        if (sk->sk_state == TCP_CLOSE)
            return -1;
    
        skb = tcp_send_head(sk);
        if (skb && before(TCP_SKB_CB(skb)->seq, tcp_wnd_end(tp))) {
            int err;
            /*
             * 如果发送队列中有段需要发送,并且最先
             * 待发送的段至少有一部分在对端接收窗口
             * 内,那么可以直接利用该待发送的段来发
             * 送持续探测段。
             */
            unsigned int mss = tcp_current_mss(sk);
                /*
             * 获取当前的MSS以及待分段的段长。分段得到
             * 的新段必须在对方接收窗口内,待分段的段
             * 长初始化为SND.UNA-SND_WND-SKB.seq.
             */
            unsigned int seg_size = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq;
    /*
             * 如果该段的序号已经大于pushed_seq,则需要
             * 更新pushed_seq。
             */
            if (before(tp->pushed_seq, TCP_SKB_CB(skb)->end_seq))
                tp->pushed_seq = TCP_SKB_CB(skb)->end_seq;
    
            /* We are probing the opening of a window
             * but the window size is != 0
             * must have been a result SWS avoidance ( sender )
             */
              /*
             * 如果待分段段长大于剩余等待发送数据,或者段长度
             * 大于当前MSS,则对该段进行分段,分段段长取待分段
             * 段长与当前MSS两者中的最小值,以保证只发送出一个
             * 段到对方。
             */
            if (seg_size < TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq ||
                skb->len > mss) {
                seg_size = min(seg_size, mss);
                TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH;
                if (tcp_fragment(sk, skb, seg_size, mss, GFP_ATOMIC))
                    return -1;
            } else if (!tcp_skb_pcount(skb))
                tcp_set_skb_tso_segs(skb, mss);
     /*
             * 将探测段发送出去,如果发送成功,
             * 则更新发送队首等标志。
             */
            TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH;
            err = tcp_transmit_skb(sk, skb, 1, GFP_ATOMIC);
            if (!err)
                tcp_event_new_data_sent(sk, skb);
            return err;
        } else {
     /*
             * 如果发送队列为空,则构造并发送一个需要已确认、
             * 长度为零的段给对端。如果处于紧急模式,则多发送
             * 一个序号为SND.UNA的段给对端。
             */
            if (between(tp->snd_up, tp->snd_una + 1, tp->snd_una + 0xFFFF))
                tcp_xmit_probe_skb(sk, 1, mib);
            return tcp_xmit_probe_skb(sk, 0, mib);
        }
    }
    /* A window probe timeout has occurred.  If window is not closed send
     * a partial packet else a zero probe.
     */
      /*
     * 当持续定时器超时之后,会调用tcp_send_probe0()
     * 进行探测。
     */
    void tcp_send_probe0(struct sock *sk)
    {
        struct inet_connection_sock *icsk = inet_csk(sk);
        struct tcp_sock *tp = tcp_sk(sk);
        struct net *net = sock_net(sk);
        unsigned long probe_max;
        int err;
        /*
         * 输出持续探测段。
         */    /* 发送一个序号为snd_una - 1,长度为0的ACK包作为零窗口探测报文 */
        err = tcp_write_wakeup(sk, LINUX_MIB_TCPWINPROBE);
    /*
         * 如果有已发送但未确认的段,或者发送队列为空,
         * 这两种情况都无需再发送持续探测段了,因此需要
         * 将icsk_probes_out和icsk_backoff清零,然后返回。
         */
        if (tp->packets_out || !tcp_send_head(sk)) {
            /* Cancel probe timer, if it is not required. */
            icsk->icsk_probes_out = 0;
            icsk->icsk_backoff = 0;
            return;
        }
    
        if (err <= 0) {
             /*
         * 如果重传成功或并非由于本地拥塞而发送失败,
         * 则更新icsk_backoff和icsk_probes_out,然后复位持续定时器。
         */
            if (icsk->icsk_backoff < net->ipv4.sysctl_tcp_retries2)
                icsk->icsk_backoff++;
            icsk->icsk_probes_out++;
            probe_max = TCP_RTO_MAX;
        } else {
            /* If packet was not sent due to local congestion,
             * do not backoff and do not remember icsk_probes_out.
             * Let local senders to fight for local resources.
             *
             * Use accumulated backoff yet.
             */
              /*
             * 如果由于本地拥塞而导致发送失败,则不需要累计
             * icsk_probes_out,同时复位持续定时器,缩短超时时间,
             * 尽可能争取资源。
             */
            if (!icsk->icsk_probes_out)
                icsk->icsk_probes_out = 1;
            probe_max = TCP_RESOURCE_PROBE_INTERVAL;
        }
        inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
                      tcp_probe0_when(sk, probe_max),
                      TCP_RTO_MAX);
    }

    坚持定时器发送探测报文并期望对端能对探测报文发送ACK,这样TCP就能得到最新的窗口信息。一旦窗口增加到可以发送数据,则正常的数据交互就可以尽快恢复。这个和keepalive类似!!!!

  • 相关阅读:
    SQL易错总结1
    线程池使用总结
    多线程的上下文切换
    SQL 排序按指定内容优先排序
    System x 服务器制作ServerGuide U盘安装Windows Server 2008 操作系统 --不格式化盘
    错误“该伙伴事务管理器已经禁止了它对远程/网络事务的支持”解决方案
    sql server 2012 链接服务器不能链接sql server 2000的解决方案 ,
    sqlserver2005版本的mdf文件,还没有log文件,
    BCP SQL导出EXCEL常见问题及解决方法;数据导出存储过程
    Nginx
  • 原文地址:https://www.cnblogs.com/codestack/p/12818582.html
Copyright © 2011-2022 走看看