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\n", 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\n", 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类似!!!!

posted @ 2020-05-02 16:41  codestacklinuxer  阅读(522)  评论(0编辑  收藏  举报