TCP的ACK確認系列 — 快速確認

主要內容:TCP的快速確認、TCP_QUICKACK選項的實現。

內核版本:3.15.2

我的博客:http://blog.csdn.net/zhangskd

 

快速確認模式

 

(1) 進入快速確認模式

設置快速確認模式標誌,設置在快速確認模式中可以發送的ACK數量。

static void tcp_enter_quickack_mode (struct sock *sk)
{
    struct inet_connection_sock *icsk = inet_csk(sk);

    tcp_incr_quickack(sk); /* 設置在快速確認模式中可以發送的ACK數量 */
    icsk->icsk_ack.pingpong = 0; /* 快速確認模式的標誌 */
    icsk->icsk_ack.ato = TCP_ATO_MIN; /* ACK超時時間 */
}

在快速確認模式中,可以發送的ACK數量是有限制的,具體額度爲icsk->icsk_ack.quick。

所以進入快速確認模式時,需要設置可以快速發送的ACK數量,一般允許快速確認半個接收窗口的數據量,

但最多不能超過16個,最少爲2個。

static void tcp_incr_quickack (struct sock *sk)
{
    struct inet_connection_sock *icsk = inet_csk(sk);

    /* 可以快速確認半個接收窗口的數據量 */
    unsigned int quickacks = tcp_sk(sk)->rcv_wnd / (2 * icsk->icsk_ack.rcv_mss);

    if (quickacks == 0)
        quckacks = 2; /* 最少爲2個 */

    if (quickacks > icsk->icsk_ack.quick)
        icsk->icsk_ack.quick = min(quickacks, TCP_MAX_QUICKACKS); /* 最多不能超過16個 */
}

/* Maximal number of ACKs sent quickly to accelerate slow-start. */
#define TCP_MAX_QUICKACKS 16U

 

(2) 檢查是否處於快速確認模式。

如果設置了快速確認標誌,且快速確認模式中可以發送的ACK數量不爲0,就判斷連接處於快速確認模式中,

允許立即發送ACK。

/* Send ACKs quickly, if "quick" count is not exhausted and the session is not interactive. */
static inline bool tcp_in_quickack_mode (const struct sock *sk)
{
    const struct inet_connectionsock *icsk = inet_csk(sk);

    /* 如果快速確認模式中可以發送的ACK數量不爲0,且設置了快速確認標誌 */
    return icsk->icsk_ack.quick && ! icsk->icsk_ack.pingpong;
}

 

快速ACK的發送

 

在tcp_rcv_established()中,如果沒有通過TCP的首部預測,就會執行慢速路徑來處理接收到的報文。

處理完接收到的報文之後,會調用tcp_data_snd_check()來檢查是否需要發送數據,以及是否需要擴大發送緩存。

然後調用tcp_ack_snd_check()來檢查是否需要發送ACK,以及是使用快速確認還是延遲確認。

同樣的在通過TCP首部預測的快速路徑中,也會調用__tcp_ack_snd_check()來發送快速確認或延遲確認。

static inline void tcp_ack_snd_check(struct sock *sk)
{
    /* 如果沒有ACK需要發送 */
    if (! inet_csk_ack_scheduled(sk)) {
        /* We sent a data segment already. */
        return;
    }

    __tcp_ack_snd_check(sk, 1); /* 決定要發送快速確認還是延遲確認 */
}

 

如果此時符合以下任一條件,可以立即發送ACK,即進行快速確認:

1. 接收緩衝區中有一個以上的全尺寸數據段仍然是NOT ACKed,並且接收窗口變大了。

    所以一般收到了兩個數據包後,會發送ACK,而不是對每個數據包都進行確認。

2.  此時處於快速確認模式中。

3. 亂序隊列不爲空。

/* Check if sending an ack is needed. */

static void __tcp_ack_snd_check (struct sock *sk, int ofo_possible)
{
    struct tcp_sock *tp = tcp_sk(sk);

    /* 符合以下任一條件,可以立即發送ACK:
     * 1. 接收緩衝區中有一個以上的全尺寸數據段仍然是NOT ACKed,並且接收窗口變大了。
     * 2.  此時處於快速確認模式中。
     * 3. 亂序隊列不爲空。
     */
    /* More than one full frame received... */
    if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss && 
        /* ... and right edge of window advances far enough.
         * (tcp_recvmsg() will send ACK otherwise). Or ...
         */
         __tcp_select_window(sk) >= tp->rcv_wnd) || 

         /* We ACK each frame or ... */
         tcp_in_quickack_mode(sk) ||

         /* We have out of order data. */
         (ofo_possible && skb_peek(&tp->out_of_order_queue))) {

        /* Then ack it now. */
        tcp_send_ack(sk); /* 立即發送ACK */

    } else {
        /* Else, send delayed ack. */
        tcp_send_delayed_ack(sk); /* 延遲ACK的發送,見下一篇blog:) */
    }
}

 

ACK的發送函數爲tcp_send_ack(),如果發送失敗會啓動ACK延遲定時器。

/* This routine sends an ack and also updates the window. */

void tcp_send_ack (struct sock *sk)
{
    struct sk_buff *buff;

    /* If we have been reset, we may not send again. */
    if (sk->sk_state == TCP_CLOSE)
        return;

    /* We are not putting this on the write queue, so tcp_transmit_skb()
     * will set the ownership to this sock.
     */
    buff = alloc_skb(MAX_TCP_HEADER, sk_gfp_atomic(sk, GFP_ATOMIC));

    if (buff == NULL) { /* 分配skb失敗 */
        inet_csk_schedule_ack(sk); /* 設置標誌位,表示有ACK需要發送 */
        inet_csk(sk)->icsk_ack.ato = TCP_ATO_MIN; /* 重置ATO */

        /* 設置延遲確認定時器,超時時間爲200ms */
        inet_csk_reset_xmit_timer(sk, ICSK_TIME_DACK, TCP_DELACK_MAX, TCP_RTO_MAX);
        return;
    }

    /* Reserve space for headers and prepare control bits. */
    skb_reserve(buff, MAX_TCP_HEADER); /* 設置報文頭部的空間 */

    /* 初始化不攜帶數據的skb的一些控制字段 */
    tcp_init_nondata_skb(buff, tcp_acceptable_seq(sk), TCPHDR_ACK);

    /* Send it off, this clears delayed acks for us. */
    TCP_SKB_CB(buff)->when = tcp_time_stamp;

    tcp_transmit_skb(sk, buff, 0, sk_gfp_atomic(sk, GFP_ATOMIC));
}

/* 設置標誌位,表示有ACK需要發送。*/
static inline void inet_csk_schedule_ack (struct sock *sk)
{
    inet_csk(sk)->icsk_ack.pending |= ICSK_ACK_SCHED;
}

/* Maximal time to delay before sending an ACK.
 * Delayed ACK的最大延遲時間,一般爲200ms
 */
#define TCP_DELACK_MAX ((unsigned) (HZ/5))

/* Delayed ACK的最小延遲時間,一般爲40ms */
#define TCP_DELACK_MIN ((unsigned) (HZ/25))

 

TCP_QUICKACK選項

 

TCP_QUICKACK用於讓本端立即發送ACK,而不進行延遲確認。

需要注意的是,這個選項並不是持久的,之後還是有可能進入延遲確認模式的。

所以如果需要一直進行快速確認,要在每次調用接收函數後都進行選項設置。

 

int quickack = 1; /* 啓用快速確認,如果賦值爲0表示使用延遲確認 */

setsockopt(fd, SOL_TCP, TCP_QUICKACK, &quickack, sizeof(quickack));

#define TCP_QUICKACK 12 /* Block / reenable quick acks */

static int do_tcp_setsockopt(struct sock *sk, int level, int optname, char __user *optval,
    unsigned int optlen)
{
    ...
    case TCP_QUICKACK:
        if (! val) {
            icsk->icsk_ack.pingpong = 1; /* 禁用快速確認模式 */

        } else {
            icsk->icsk_ack.pingpong = 0; /* 啓用快速確認模式 */

            /* 如果當前有ACK需要發送,就立即發送 */
            if (1 << sk->sk_state) & (TCPF_ESTABLISHED | TCPF_CLOSE_WAIT) && 
                inet_csk_ack_scheduled(sk)) {

                icsk->icsk_ack.pending |= ICSK_ACK_PUSHED; /* 允許在快速模式中立即發送 */

                /* 通常當接收隊列中有數據複製到用戶空間時,會調用此函數來判斷是否需要立即發送ACK。
                 * 這裏的用法比較特殊,由於設置了ICSK_ACK_PUSHED標誌,且處於快速確認模式中,
                 * 必然會立即發送ACK。
                 */
                tcp_cleanup_rbuf(sk, 1);             

                /* 如果選項的值爲偶數,那麼立即退出快速確認模式。
                 * 原來選項值不限於0和1,還分奇偶的:)
                 */
                if (! (val & 1)) 
                    icsk->icsk_ack.pingpong = 1;
            }
        }
        break;
        ...
}

 

當接收隊列中有數據複製到用戶空間時,會調用tcp_cleanup_rbuf()來判斷是否要立即發送ACK。

 

(1) 如果現在有ACK需要發送,滿足以下條件之一,就可以立即發送:

1. icsk->icsk_ack.blocked爲1,之前有Delayed ACK被用戶進程阻塞了。

2. 接收緩衝區中有一個以上的全尺寸數據段仍然是NOT ACKed (所以經常是收到2個全尺寸段後發送ACK)

3. 本次複製到用戶空間的數據量大於0,且滿足以下條件之一:

    3.1 設置了ICSK_ACK_PUSHED2標誌

    3.2 設置了ICSK_ACK_PUSHED標誌,且處於快速確認模式中

 

(2) 如果原來沒有ACK需要發送,但是現在的接收窗口顯著增大了,也需要立即發送ACK通知對端。

這裏的顯著增大是指:新的接收窗口大小不爲0,且比原來接收窗口的剩餘量增大了一倍。

/* Clean up the receive buffer for full frames taken by the user,
 * then send an ACK if necessary. COPIED is the number of bytes
 * tcp_recvmsg has given to the user so far, it speeds up the calculation
 * of whether or not we must ACK for the sake of a window update.
 */

void tcp_cleanup_rbuf (struct sock *sk, int copied)
{
    struct tcp_sock *tp = tcp_sk(sk);
    bool time_to_ack = false;

    /* 獲取接收隊列的頭一個數據段 */
    struct sk_buff *skb = skb_peek(&sk->sk_receive_queue);

    /* copied_seq: Head of yet unread data,應用程序下次從這裏開始複製數據。
     * 這裏檢查在發送隊列中,已複製到用戶空間的數據段是否被清理了。
     */
    WARN(skb && !before(tp->copied_seq, TCP_SKB_CB(skb)->end_seq), 
        "cleanup rbuf bug: copied %X seq %X rcvnxt %X\n", tp->copied_seq,
        TCP_SKB_CB(skb)->end_seq, tp->rcv_nxt);

    /* 如果現在有ACK需要發送,滿足以下條件之一則可以立即發送 */
    if (inet_csk_ack_scheduled(sk)) {
        const struct inet_connection_sock *icsk = inet_csk(sk);

        /* 1. Delayed ACKs frequently hit locked sockets during bulk receive.
         * 2. Once-per-two-segments ACK was not sent by tcp_input.c.
         * 3. copied >0 and ICSK_ACK_PUSHED2 set.
         * 4. copied > 0 and ICSK_ACK_PUSHED and in quickack mode.
         */
        if (icsk->icsk_ack.blocked || tp->rcv_nxt - tp->rcv_wup > icsk->icsk_ack.rcv_mss ||
            (copied > 0 && ((icsk->icsk_ack.pending & ICSK_ACK_PUSHED2) || 
             ((icsk->icsk_ack.pending & ICSK_ACK_PUSHED) && ! icsk->icsk_ack.pingpong)) &&
               ! atomic_read(&sk->sk_rmem_alloc)))
            time_to_ack = true;
    } 

    /* We send an ACK if we can now advertise a non-zero window which has
     * been raised "significantly" - at least twice bigger.
     * Even if window raised up to infinity, do not send window open ACK in states,
     * where we will not receive more. It is useless.
     */
    if (copied > 0 && ! time_to_ack && ! (sk->sk_shutdown & RCV_SHUTDOWN)) {
        __u32 rcv_window_now = tcp_receive_window(tp); /* 當前接收窗口的剩餘量 */

        /* 如果當前接收窗口的剩餘量小於最大值的一半 */
        if (2 * rcv_window_now <= tp->window_clamp) {

           /* 根據剩餘的接收緩存,計算新的接收窗口的大小。
            * 因爲這次複製,很可能空出不少接收緩存,所以新的接收窗口也會相應增大。
            */
            __u32 new_window = __tcp_select_window(sk);

           /* Send ACK now, if this read freed lots of space in our buffer.
            * Certainly, new_window is new window.
            * We can advertise it now, if it is not less than current one.
            * "Lots" means "at least twice" here.
            */

            /* 如果新的接收窗口不爲0,且比原來接收窗口的剩餘量大了一倍以上,就說接收窗口顯著增大了。
             * 而當接收窗口顯著增大時,也需要立即發送ACK告知對端。
             */
            if (new_window && new_window >= 2 * rcv_window_now)
                time_to_ack = true;
        }
    }

    if (time_to_ack)
        tcp_send_ack(sk); /* 發送ACK給對端 */
}
/* 計算當前接收窗口的剩餘量 */
/* Compute the actual receive window we are currently advertising.
 * Rcv_nxt can be after the window if our peer push more data than
 * the offered window.
 */
static inline u32 tcp_receive_window (const struct tcp_sock *tp)
{
    s32 win = tp->rcv_wup + tp->rcv_wnd - tp->rcv_nxt;

    if (win < 0)
        win = 0;

    return (u32) win;
}

 

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