1. 程式人生 > >TCP的定時器系列 — 零視窗探測定時器

TCP的定時器系列 — 零視窗探測定時器

主要內容:零視窗探測定時器的實現。

核心版本:3.15.2

出現以下情況時,TCP接收方的接收緩衝區將被塞滿資料:

傳送方的傳送速度大於接收方的接收速度。

接收方的應用程式未能及時從接收緩衝區中讀取資料。

當接收方的接收緩衝區滿了以後,會把響應報文中的通告視窗欄位置為0,從而阻止傳送方的繼續傳送,

這就是TCP的流控制。當接收方的應用程式讀取了接收緩衝區中的資料以後,接收方會發送一個ACK,通過

通告視窗欄位告訴傳送方自己又可以接收資料了,傳送方收到這個ACK之後,就知道自己可以繼續傳送資料了。

Q:那麼問題來了,當接收方的接收視窗重新開啟之後,如果它傳送的ACK丟失了,傳送方還能得知這一訊息嗎?

A:答案是不能。正常的ACK報文不需要確認,因而也不會被重傳,如果這個ACK丟失了,傳送方將無法得知對端

的接收視窗已經打開了,也就不會繼續傳送資料。這樣一來,會造成傳輸死鎖,接收方等待對端傳送資料包,而傳送

方等待對端的ACK,直到連線超時關閉。

為了避免上述情況的發生,傳送方實現了一個零視窗探測定時器,也叫做持續定時器:

當接收方的接收視窗為0時,每隔一段時間,傳送方會主動傳送探測包,通過迫使對端響應來得知其接收視窗有無開啟。

這就是山不過來,我就過去:)

啟用

(1) 傳送資料包時

在傳送資料包時,如果傳送失敗,會檢查是否需要啟動零視窗探測定時器。

tcp_rcv_established

    |--> tcp_data_snd_check

               |--> tcp_push_pending_frames

static inline void tcp_push_pending_frames(struct sock *sk)
{
    if (tcp_send_head(sk)) { /* 傳送佇列不為空 */
        struct tcp_sock *tp = tcp_sk(sk);
        __tcp_push_pending_frames(sk, tcp_current_mss(sk), tp->nonagle);
    }
}

/* 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.
 */
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_atomic(sk, GFP_ATOMIC)))
        tcp_check_probe_timer(sk); /* 檢查是否需要啟用0視窗探測定時器*/
}

當網路中沒有傳送且未確認的資料包,且本端有待發送的資料包時,啟動零視窗探測定時器。

為什麼要有這兩個限定條件呢?

如果網路中有傳送且未確認的資料包,那這些包本身就可以作為探測包,對端的ACK即將到來。

如果沒有待發送的資料包,那對端的接收視窗為不為0根本不需要考慮。

static inline void tcp_check_probe_timer(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
    const struct inet_connection_sock *icsk = inet_csk(sk);

    /* 如果網路中沒有傳送且未確認的資料段,並且零視窗探測定時器尚未啟動,
     *  則啟用0視窗探測定時器。
     */
    if (! tp->packets_out && ! icsk->icsk_pending)
        inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
                 icsk->icsk_rto, TCP_RTO_MAX);
}

(2) 接收到ACK時

tcp_ack()用於處理接收到的帶有ACK標誌的段,會檢查是否要刪除或重置零視窗探測定時器。

static int tcp_ack (struct sock *sk, const struct sk_buff *skb, int flag)
{
    ...
    icsk->icsk_probes_out = 0; /* 清零探測次數,所以如果對端有響應ACK,實際上是沒有次數限制的 */
    tp->rcv_tstamp = tcp_time_stamp; /* 記錄最近接收到ACK的時間點,用於保活定時器 */
    /* 如果之前網路中沒有傳送且未確認的資料段 */
    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, prior_unsacked, is_dupack, flag);

    /* 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導致的,這時候便進行零視窗探測定時器的處理。
     */
    if (tcp_send_head(sk)) 
        /* 如果ACK打開了接收視窗,則刪除零視窗探測定時器。否則根據退避指數,給予重置 */
        tcp_ack_probe(sk);
}

接收到一個ACK的時候,如果之前網路中沒有傳送且未確認的資料段,本端又有待發送的資料段,

說明可能遇到對端接收視窗為0的情況。

這個時候會根據此ACK是否打開了接收視窗來進行零視窗探測定時器的處理:

1. 如果此ACK開啟接收視窗。此時對端的接收視窗不為0了,可以繼續傳送資料包。

    那麼清除超時時間的退避指數,刪除零視窗探測定時器。

2. 如果此ACK是接收方對零視窗探測報文的響應,且它的接收視窗依然為0。那麼根據指數退避演算法,

    重新設定零視窗探測定時器的下次超時時間,超時時間的設定和超時重傳定時器的一樣。

#define ICSK_TIME_PROBE0 3 /* Zero window probe timer */

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 { /* 否則根據退避指數重置零視窗探測定時器 */
        inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
                  min(icsk->icsk_rto << icsk->icsk_backoff, TCP_RTO_MAX), TCP_RTO_MAX);
    }
}

/* 返回傳送視窗的最後一個位元組序號 */
/* Returns end sequence number of the receiver's advertised window */
static inline u32 tcp_wnd_end(const struct tcp_sock *tp)
{
    return tp->snd_una + tp->snd_wnd;
}

超時處理函式

icsk->icsk_retransmit_timer可同時作為:超時重傳定時器、ER延遲定時器、PTO定時器,

還有零視窗探測定時器,它們的超時處理函式都為tcp_write_timer_handler(),在函式內則

根據超時事件icsk->icsk_pending來做區分。

具體來說,當網路中沒有傳送且未確認的資料段時,icsk->icsk_retransmit_timer才會用作零視窗探測定時器。

而其它三個定時器的使用場景則相反,只在網路中有傳送且未確認的資料段時使用。  

和超時重傳定時器一樣,零視窗探測定時器也使用icsk->icsk_rto和退避指數來計算超時時間。

void tcp_write_timer_handler(struct sock *sk)
{
    struct inet_connection_sock *icsk = inet_csk(sk);
    int event;

    /* 如果連線處於CLOSED狀態,或者沒有定時器在計時 */
    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; /* 用於表明是哪種定時器 */
    switch(event) {
        case ICSK_TIME_EARLY_RETRANS: /* ER延遲定時器觸發的 */
            tcp_resume_early_retransmit(sk); /* 進行early retransmit */
            break;

        case ICSK_TIME_LOSS_PROBE: /* PTO定時器觸發的 */
            tcp_send_loss_probe(sk); /* 傳送TLP探測包 */
            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);
}

可見零視窗探測定時器的真正處理函式為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;

    /* 如果網路中有傳送且未確認的資料包,或者沒有待發送的資料包。
     * 這個時候不需要使用零視窗探測定時器。前一種情況時已經有現成的探測包了,
     * 後一種情況中根本就不需要傳送資料了。
     */
    if (tp->packets_out || ! tcp_send_head(sk)) {
        icsk->icsk_probes_out = 0; /* 清零探測包的傳送次數 */
        return;
    }

    /* icsk_probes_out is zeroed by incoming ACKs even if they advertise zero window.
     * Hence, connection is killed only if we received no ACKs for normal connection timeout.
     * It is not killed only because window stays zero for some time, window may be zero until
     * armageddon and even later. We are full accordance with RFCs, only probe timer combines
     * both retransmission timeout and probe timeout in one bottle.
     */

    max_probes = sysctl_tcp_retries2; /* 當沒有收到ACK時,執行傳送探測包的最大次數,之後連線超時 */

    if (sock_flag(sk, SOCK_DEAD)) { /* 如果套介面即將關閉 */
        const int alive = ((icsk->icsk_rto << icsk->icsk_backoff) < TCP_RTO_MAX);
        max_probes = tcp_orphan_retries(sk, alive); /* 決定重傳的次數 */

        /* 如果當前的孤兒socket數量超過tcp_max_orphans,或者記憶體不夠時,關閉此連線 */
        if (tcp_out_of_resource(sk, alive || icsk->icsk_probes_out <= max_probes))
            return;
    }

    /* 如果傳送出的探測報文的數目達到最大值,卻依然沒有收到對方的ACK時,關閉此連線 */
    if (icsk->icsk_probes_out > max_probes) { /* 實際上每次收到ACK後,icsk->icsk_probes_out都會被清零 */
        tcp_write_err(sk);

    } else {
        /* Only send another probe if we didn't close things up. */
        tcp_send_probe0(sk); /* 傳送零視窗探測報文 */
    }
}

傳送0 window探測報文和傳送Keepalive探測報文用的是用一個函式tcp_write_wakeup():

1. 有新的資料段可供傳送,且對端接收視窗還沒被塞滿。傳送新的資料段,來作為探測包。

2. 沒有新的資料段可供傳送,或者對端的接收視窗滿了。傳送序號為snd_una - 1、長度為0的ACK包作為探測包。

和保活探測定時器不同,零視窗探測定時器總是使用第二種方法,因為此時對端的接收視窗為0。

所以會發送一個序號為snd_una - 1、長度為0的ACK包,對端收到此包後會傳送一個ACK響應。

如此一來本端就能夠知道對端的接收視窗是否打開了。

/* 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);
    int err;

    /* 傳送一個序號為snd_una - 1,長度為0的ACK包作為零視窗探測報文 */
    err = tcp_write_wakeup(sk);

    /* 如果網路中有傳送且未確認的資料包,或者沒有待發送的資料包。
     * 這個時候不需要使用零視窗探測定時器。前一種情況時已經有現成的探測包了,
     * 後一種情況中根本就不需要傳送資料了。check again 8)
     */
    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;
    }

    /* err:0成功,-1失敗 */
    if (err < = 0) {
        if (icsk->icsk_backoff < sysctl_tcp_retries2)
            icsk->icsk_backoff++; /* 退避指數 */

        icsk->icsk_probes_out++; /* 探測包的傳送次數 */
        inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0, min(icsk->icsk_rto << icsk->icsk_backoff, 
            TCP_RTO_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.
         */
         if (! icsk->icsk_probes_out)
             icsk->icsk_probes_out = 1;

         /* 使零視窗探測定時器更快的超時 */
         inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0, 
            min(icsk->icsk_rto << icsk->icsk->icsk_backoff, TCP_RESOURCE_PROBE_INTERVAL),
            TCP_RTO_MAX);
    }
}