TCP零窗口探測

TCP零窗口探測用於獲取觸發對端的窗口更新報文,防止在窗口更新報文丟失之後,導致的死循環。其也有助於本端Qdisc滿或者數據被髮送節奏(Pacing)阻止導致的發送停滯。

窗口探測開啓

在TCP報文發送函數tcp_write_xmit的處理中,如果最終未能發送任何報文,而且網絡中報文爲空(packets_out),套接口的發送隊列中有數據,將返回true。造成此情況可能是由於惰性窗口綜合徵(SWS),或者其它原因,如擁塞窗口限制、接收窗口限制等。

static bool tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
               int push_one, gfp_t gfp)
{
    ...
    if (likely(sent_pkts)) {
        ...
        return false;
    }
    return !tp->packets_out && !tcp_write_queue_empty(sk);
}

在發送暫緩的報文時,如果以上函數tcp_write_xmit返回true,調用函數tcp_check_probe_timer檢查探測定時器。

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);
}

如果此時網絡中沒有任何發送的報文,packets_oout爲空,並且本地也沒有啓動任何定時器,icsk_pending爲空意味着重傳定時器、亂序定時器、TLP定時器和窗口探測定時器都沒有啓動(這四個定時器由內核中的一個定時器結構實現,以icsk_pending中的標誌位區分)。此種情況下啓動零窗口探測定時器。

static inline void tcp_check_probe_timer(struct sock *sk)
{
    if (!tcp_sk(sk)->packets_out && !inet_csk(sk)->icsk_pending)
        tcp_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
                     tcp_probe0_base(sk), TCP_RTO_MAX,
                     NULL);
}

零窗口定時器的時長由函數tcp_probe0_base決定,取值爲當前的RTO時長,但是最短不低於TCP_RTO_MIN(200毫秒)。如下函數的註釋可見,其定時器除了用於零窗口探測,也會因本端的Qdisc滿或者發送節奏導致的發送失敗,而啓動。

/* Something is really bad, we could not queue an additional packet,
 * because qdisc is full or receiver sent a 0 window, or we are paced.
 * We do not want to add fuel to the fire, or abort too early,
 * so make sure the timer we arm now is at least 200ms in the future,
 * regardless of current icsk_rto value (as it could be ~2ms)
 */
static inline unsigned long tcp_probe0_base(const struct sock *sk)
{
    return max_t(unsigned long, inet_csk(sk)->icsk_rto, TCP_RTO_MIN);
}

窗口探測定時器

定時器的超時處理由函數tcp_probe_timer完成。如果網絡中存在發送的報文,packets_out有值,或者套接口發送隊列中沒有數據,退出不進行處理。

static void tcp_probe_timer(struct sock *sk)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct sk_buff *skb = tcp_send_head(sk);
    struct tcp_sock *tp = tcp_sk(sk);

    if (tp->packets_out || !skb) {
        icsk->icsk_probes_out = 0;
        return;
    }

如果用戶設置了UTO(變量icsk_user_timeout的值),並且發送隊列中還有待發送報文,此報文等待的時長不能超過UTO,否則,認爲此連接已經出錯。

    /* 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(skb);
    if (!start_ts)
        skb->skb_mstamp_ns = tp->tcp_clock_cache;
    else if (icsk->icsk_user_timeout &&
         (s32)(tcp_time_stamp(tp) - start_ts) > icsk->icsk_user_timeout)
        goto abort;

以下代碼涉及到兩個PROC文件中的控制變量:tcp_retries2和tcp_orphan_retries,前者表示在對端不響應時,進行的最大重傳次數;後者表示本地已經關閉的套接,接收不到對端響應時的最大重傳次數。

$ cat /proc/sys/net/ipv4/tcp_retries2
15 
$ cat /proc/sys/net/ipv4/tcp_orphan_retries 
0

如果套接口設置了SOCK_DEAD標誌,表明本端已經關閉(Orphaned套接口),按照重傳退避係數計數的當前RTO值小於最大值TCP_RTO_MAX(120秒),說明此連接還不應斷開。之後,檢查內核設置的孤兒套接口的重傳次數(tcp_orphan_retries),如果alive爲零並且退避次數已經超出最大的Orphaned套接口探測次數,斷開連接。否則檢查TCP資源使用是否超限,如果超限,將在tcp_out_of_resources函數中斷開連接,第二個參數true表明將向對端發送RESET復位報文。

注意,內核默認的tcp_orphan_retries值爲零,所以如果alive爲零,即當前RTO值超出TCP_RTO_MAX,以下的if判斷條件成立,將導致連接的立即斷開。這種情況下,連接超時依賴於初始(第一次)RTO的值,如果其值爲最小值TCP_RTO_MIN(200ms),那麼在經過9次退避之後,RTO值將超過TCP_RTO_MAX。

    max_probes = sock_net(sk)->ipv4.sysctl_tcp_retries2;
    if (sock_flag(sk, SOCK_DEAD)) {
        const bool alive = inet_csk_rto_backoff(icsk, TCP_RTO_MAX) < TCP_RTO_MAX;

        max_probes = tcp_orphan_retries(sk, alive);
        if (!alive && icsk->icsk_backoff >= max_probes)
            goto abort;
        if (tcp_out_of_resources(sk, true))
            return;
    }

如果以上都沒有成立,並且,當前的探測次數小於等於以上計算的最大探測次數(tcp_retries2或者Orphan套接口探測次數),調用tcp_send_probe0發送探測報文。否則,使用tcp_write_err終止連接。

    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);
    }
}

如下tcp_orphan_retries函數,如果alive爲零,並且接收到ICMP報錯報文(如ICMP_PARAMETERPROB、ICMP_DEST_UNREACH等),不再進行重傳,將重傳次數設置爲零。否則,如果tcp_orphan_retries設置爲零,並且alive爲真,將重傳次數設置爲8,對於RTO最小值TCP_RTO_MIN(200ms)而言,經過8此退避之後的值將大於100秒(2**9 * 200 = 102.4秒),符合RFC1122中的規定。

static int tcp_orphan_retries(struct sock *sk, bool alive)
{
    int retries = sock_net(sk)->ipv4.sysctl_tcp_orphan_retries; /* May be zero. */

    /* We know from an ICMP that something is wrong. */
    if (sk->sk_err_soft && !alive)
        retries = 0;

    /* However, if socket sent something recently, select some safe
     * number of retries. 8 corresponds to >100 seconds with minimal
     * RTO of 200msec. */
    if (retries == 0 && alive)
        retries = 8;
    return retries;
}

發送窗口探測

調用tcp_write_wakeup函數時,套接口的發送隊列中一定是有數據,不然沒有必要進行窗口探測,如果隊列中的首報文序號位於發送窗口範圍內,表明一定數量的數據可發送(窗口不爲零,可能由發送端的SWS預防導致)。此情況下將發送新數據作爲探測報文,首先更新pushed_seq,之後發送的數據報文將設置TCPHDR_PSH控制位,對端接收到後應儘快將接收數據提交到應用,以便釋放可用的接收空間,打開接收窗口。

int tcp_write_wakeup(struct sock *sk, int mib)
{
    struct tcp_sock *tp = tcp_sk(sk);

    skb = tcp_send_head(sk);
    if (skb && before(TCP_SKB_CB(skb)->seq, tcp_wnd_end(tp))) {
        unsigned int mss = tcp_current_mss(sk);
        unsigned int seg_size = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq;

        if (before(tp->pushed_seq, TCP_SKB_CB(skb)->end_seq))
            tp->pushed_seq = TCP_SKB_CB(skb)->end_seq;

接下來,看一下允許發送的報文長度,如果發送窗口允許的發送長度小於發送隊列中首報文的長度,或者首報文長度大於當前的發送MSS長度,將對首報文進行分片處理,得到一個長度爲seg_size長度(不大於MSS)的報文,接下來將發送此報文。

如果以上兩個條件都不成立,即可發送窗口允許發送首報文,並且首報文長度小於MSS,此情況下,如果首報文的tcp_gso_segs分段爲零,使用函數tcp_set_skb_tso_segs設置GSO參數,由於此時報文長度小於等於mss,分段數量tcp_gso_segs將設置爲1,接下來發送一個gso分段。但是,如果發送隊列中的首報文由多個小報文分段組成,將發送多個小報文做探測。

注意,在上一種情況中,在tcp_fragment函數中調用了tcp_set_skb_tso_segs函數進行了gso相關設置。

        /* We are probing the opening of a window
         * but the window size is != 0
         * must have been a result SWS avoidance ( sender )
         */
        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, TCP_FRAG_IN_WRITE_QUEUE,
                     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;

在發送隊列中沒有數據,或者對端接收窗口變爲零時,以下嘗試發送ACK探測報文,由函數tcp_xmit_probe_skb完成。如果緊急指針SND.UP包含在(SND.UNA, SND.UNA+64K)範圍內,第二個參數urgent設置爲1。

    } else {
        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);
    }

如下探測報文發送函數tcp_xmit_probe_skb,發送ACK報文,如果urgent不爲真,ACK報文序號爲SND.UNA減去1(ACK報文不佔用新序號),由於此序號已經使用過,並且對端已經接收並確認,所以對端在接收到此重複序號的ACK報文之後,將丟棄此報文,並回復ACK報文通告正確的序號。

否則,如果緊急指針urgent爲真,ACK報文序號爲SND.UNA,對端並沒有確認此序號,所以,對端可能將正常接收此ACK報文,並儘快進行Urgent數據的處理(前提是已經接收到了Urgent數據),釋放接收緩存並打開接收窗口。

static int tcp_xmit_probe_skb(struct sock *sk, int urgent, int mib)
{
    struct tcp_sock *tp = tcp_sk(sk);

    /* We don't queue it, tcp_transmit_skb() sets ownership. */
    skb = alloc_skb(MAX_TCP_HEADER, sk_gfp_mask(sk, GFP_ATOMIC | __GFP_NOWARN));
    if (!skb) return -1;

    /* Reserve space for headers and set control bits. */
    skb_reserve(skb, MAX_TCP_HEADER);

    /* Use a previous sequence.  This should cause the other
     * end to send an ack.  Don't queue or clone SKB, just send it.
     */
    tcp_init_nondata_skb(skb, tp->snd_una - !urgent, TCPHDR_ACK);
    NET_INC_STATS(sock_net(sk), mib);
    return tcp_transmit_skb(sk, skb, 0, (__force gfp_t)0);

在上一節介紹的函數tcp_probe_timer的最後,調用tcp_send_probe0函數發送探測報文,如果在發送探測報文之後,檢測到用戶層發送了新報文,或者套接口發送隊列爲空,不在需要進行探測,清空探測計數,清空退避計數。

/* A window probe timeout has occurred.  If window is not closed send
 * a partial packet else a zero probe.
 */
void tcp_send_probe0(struct sock *sk)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct tcp_sock *tp = tcp_sk(sk);

    err = tcp_write_wakeup(sk, LINUX_MIB_TCPWINPROBE);

    if (tp->packets_out || tcp_write_queue_empty(sk)) {
        /* Cancel probe timer, if it is not required. */
        icsk->icsk_probes_out = 0;
        icsk->icsk_backoff = 0;
        return;
    }

如果tcp_write_wakeup成功發送探測報文,增加探測計數,如果退避計數小於tcp_retries2中限定的值,增加退避計數(icsk_backoff)。

    if (err <= 0) {
        if (icsk->icsk_backoff < net->ipv4.sysctl_tcp_retries2)
            icsk->icsk_backoff++;
        icsk->icsk_probes_out++;
        probe_max = TCP_RTO_MAX;

否則,如果探測報文未能成功發送,不增加退避計數和探測計數,而是將探測定時器的超時時長限定在TCP_RESOURCE_PROBE_INTERVAL(500ms)內。以上探測報文發送成功時,此限定值爲TCP_RTO_MAX(120秒)。再次啓動探測定時器。

    } 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.
         */
        if (!icsk->icsk_probes_out)
            icsk->icsk_probes_out = 1;
        probe_max = TCP_RESOURCE_PROBE_INTERVAL;
    }
    tcp_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
                 tcp_probe0_when(sk, probe_max),
                 TCP_RTO_MAX,
                 NULL);

由以上的介紹可知,tcp_probe0_base取得的是連接的RTO時長(不低於200ms),以下tcp_probe0_when函數,執行退避操作設置探測超時時長。最長不超過限定值參數max_when。

/* Variant of inet_csk_rto_backoff() used for zero window probes */
static inline unsigned long tcp_probe0_when(const struct sock *sk, unsigned long max_when)
{
    u64 when = (u64)tcp_probe0_base(sk) << inet_csk(sk)->icsk_backoff;

    return (unsigned long)min_t(u64, when, max_when);
}

處理探測響應

如果對端回覆了ACK報文,但是本端套接口發送隊列無數據,直接返回不做處理。只有在發送窗口大小足以容納發送隊列的首個報文時,內核纔會停止窗口探測定時器。否則,重設探測定時器超時時間,時長由上節介紹的tcp_probe0_when函數計算而得。

static void tcp_ack_probe(struct sock *sk)
{
    struct inet_connection_sock *icsk = inet_csk(sk); 
    struct sk_buff *head = tcp_send_head(sk);
    const struct tcp_sock *tp = tcp_sk(sk);

    /* Was it a usable window open? */
    if (!head) return;

    if (!after(TCP_SKB_CB(head)->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);
    
        tcp_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
                     when, TCP_RTO_MAX, NULL);

內核版本 5.0

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章