SACK壓縮

變量tcp_comp_sack_nr定義在接收到連續的亂序報文時,可壓縮的最大SACK回覆報文數量,即低於tcp_comp_sack_nr值時,延時回覆對端ACK報文。但是,前TCP_FASTRETRANS_THRESH(3)數量的亂序報文,要立即回覆ACK報文,以幫助對端完成快速重傳。

Compressed-ACK初始化

static int __net_init tcp_sk_init(struct net *net)
{
    net->ipv4.sysctl_tcp_comp_sack_delay_ns = NSEC_PER_MSEC;
    net->ipv4.sysctl_tcp_comp_sack_nr = 44;

Compressed-ACK定時器

void tcp_init_xmit_timers(struct sock *sk)
{
    ...
    hrtimer_init(&tcp_sk(sk)->compressed_ack_timer, CLOCK_MONOTONIC,
             HRTIMER_MODE_REL_PINNED_SOFT);
    tcp_sk(sk)->compressed_ack_timer.function = tcp_compressed_ack_kick;
}

如下超時處理函數tcp_compressed_ack_kick,如果套接口沒有被用戶佔用,並且compressed_ack值大於TCP_FASTRETRANS_THRESH(3),發送ACK報文。否則,設置TSQ標誌的延時ACK位TCP_DELACK_TIMER_DEFERRED,隨後在用戶釋放套接口時執行。

static enum hrtimer_restart tcp_compressed_ack_kick(struct hrtimer *timer)
{
    struct tcp_sock *tp = container_of(timer, struct tcp_sock, compressed_ack_timer);
    struct sock *sk = (struct sock *)tp;

    bh_lock_sock(sk);
    if (!sock_owned_by_user(sk)) {
        if (tp->compressed_ack > TCP_FASTRETRANS_THRESH)
            tcp_send_ack(sk);
    } else {
        if (!test_and_set_bit(TCP_DELACK_TIMER_DEFERRED, &sk->sk_tsq_flags))
            sock_hold(sk);
    }
    bh_unlock_sock(sk);

    sock_put(sk);
    return HRTIMER_NORESTART;

如下函數在release_sock中調用,根據TCPF_DELACK_TIMER_DEFERRED標誌,調用tcp_delack_timer_handler發送ACK報文。

void tcp_release_cb(struct sock *sk)
{   
    ...
    /* perform an atomic operation only if at least one flag is set */
    do {
        flags = sk->sk_tsq_flags;
        if (!(flags & TCP_DEFERRED_ALL)) return;
        nflags = flags & ~TCP_DEFERRED_ALL;
    } while (cmpxchg(&sk->sk_tsq_flags, flags, nflags) != flags);

    sock_release_ownership(sk);
    
    if (flags & TCPF_DELACK_TIMER_DEFERRED) {
        tcp_delack_timer_handler(sk);
        __sock_put(sk);
    }

接收路徑與Compressed-ACK

首先看一下tcp_rcv_established函數中ACK報文的發送,快速路徑與慢速路徑基本一致,但是快速路徑中,如果本地作爲數據接收方,也可能發送少量的數據到對端,在這種情況下,處理對端回覆的ACK確認報文,檢查是否還有數據要發送,並且隨後判斷是否發送了數據,如果爲真就不用單獨發送ACK報文了。否則調用__tcp_ack_snd_check函數檢查ACK報文是否需要發送。

對於慢速路徑,調用tcp_ack_snd_check函數檢查ACK的發送,其中封裝了__tcp_ack_snd_check函數。

void tcp_rcv_established(struct sock *sk, struct sk_buff *skb)
{
    if ((tcp_flag_word(th) & TCP_HP_BITS) == tp->pred_flags &&
        TCP_SKB_CB(skb)->seq == tp->rcv_nxt &&
        !after(TCP_SKB_CB(skb)->ack_seq, tp->snd_nxt)) {
        ...
        if (len <= tcp_header_len) {
            ...
        } else {
            ...
            if (TCP_SKB_CB(skb)->ack_seq != tp->snd_una) {
                /* Well, only one small jumplet in fast path... */
                tcp_ack(sk, skb, FLAG_DATA);
                tcp_data_snd_check(sk);
                if (!inet_csk_ack_scheduled(sk))
                    goto no_ack;
            }
            __tcp_ack_snd_check(sk, 0);
no_ack:
    }
slow_path:
    ...
    tcp_data_snd_check(sk);
    tcp_ack_snd_check(sk);

如下tcp_ack_snd_check函數,其實現與快速路徑的基本一致。

static inline void tcp_ack_snd_check(struct sock *sk)
{
    if (!inet_csk_ack_scheduled(sk)) {
        /* We sent a data segment already. */
        return;
    }
    __tcp_ack_snd_check(sk, 1);
}

如下__tcp_ack_snd_check函數,首先判斷需要立即回覆ACK報文的情況,例如,已經接收到超過RMSS值大小的報文;接收緩存中的數據量低於RCVLOWAT;套接口處於quickack模式等。

static void __tcp_ack_snd_check(struct sock *sk, int ofo_possible)
{
        /* More than one full frame received... */
    if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss &&
        (tp->rcv_nxt - tp->copied_seq < sk->sk_rcvlowat ||
         __tcp_select_window(sk) >= tp->rcv_wnd)) ||
        tcp_in_quickack_mode(sk) ||
        inet_csk(sk)->icsk_ack.pending & ICSK_ACK_NOW) {
send_now:
        tcp_send_ack(sk);
        return;
    }

對於保序報文,或者亂序隊列爲空的情況,延時ACK發送。反之,對於不支持SACK的連接,或者compressed_ack數量大於等於sysctl_tcp_comp_sack_nr(默認44)的情況,立即發送ACK報文。

    if (!ofo_possible || RB_EMPTY_ROOT(&tp->out_of_order_queue)) {
        tcp_send_delayed_ack(sk);
        return;
    }
    if (!tcp_is_sack(tp) ||
        tp->compressed_ack >= sock_net(sk)->ipv4.sysctl_tcp_comp_sack_nr)
        goto send_now;

最後,如果上次記錄的compressed_ack_rcv_nxt不等於當前套接口的RCV.NXT,更新compressed_ack_rcv_nxt,清理compressed_ack計數。當compressed_ack自加一之後小於等於TCP_FASTRETRANS_THRESH(3)時,立即發送ACK報文。否則,啓動compressed_ack_timer定時器,時長爲RTT的二十分之一,但是不超過sysctl_tcp_comp_sack_delay_ns(默認1ms)值。

    if (tp->compressed_ack_rcv_nxt != tp->rcv_nxt) {
        tp->compressed_ack_rcv_nxt = tp->rcv_nxt;
        if (tp->compressed_ack > TCP_FASTRETRANS_THRESH)
            NET_ADD_STATS(sock_net(sk), LINUX_MIB_TCPACKCOMPRESSED, tp->compressed_ack - TCP_FASTRETRANS_THRESH);
        tp->compressed_ack = 0;
    }

    if (++tp->compressed_ack <= TCP_FASTRETRANS_THRESH)
        goto send_now;

    if (hrtimer_is_queued(&tp->compressed_ack_timer))
        return;

    /* compress ack timer : 5 % of rtt, but no more than tcp_comp_sack_delay_ns */

    rtt = tp->rcv_rtt_est.rtt_us;
    if (tp->srtt_us && tp->srtt_us < rtt)
        rtt = tp->srtt_us;

    delay = min_t(unsigned long, sock_net(sk)->ipv4.sysctl_tcp_comp_sack_delay_ns,
              rtt * (NSEC_PER_USEC >> 3)/20);
    sock_hold(sk);
    hrtimer_start(&tp->compressed_ack_timer, ns_to_ktime(delay), HRTIMER_MODE_REL_PINNED_SOFT);

亂序報文與Compressed-ACK

對於接收到的亂序報文,函數tcp_sack_new_ofo_skb負責生成SACK序號塊。對於當前SACK序號塊數量爲零的情況,直接添加即可。否則,遍歷當前序號塊數組,看一下亂序報文的起止序號能否與已有序號塊進行擴展,將擴展之後的序號塊移動到數組首部。並嘗試進行序號塊數組的合併。

static void tcp_sack_new_ofo_skb(struct sock *sk, u32 seq, u32 end_seq)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct tcp_sack_block *sp = &tp->selective_acks[0];
    int cur_sacks = tp->rx_opt.num_sacks;

    if (!cur_sacks) goto new_sack;

    for (this_sack = 0; this_sack < cur_sacks; this_sack++, sp++) {
        if (tcp_sack_extend(sp, seq, end_seq)) {
            /* Rotate this_sack to the first one. */
            for (; this_sack > 0; this_sack--, sp--)
                swap(*sp, *(sp - 1));
            if (cur_sacks > 1)
                tcp_sack_maybe_coalesce(tp);
            return;
        }
    }

以上都不成立的話,繼續後面的處理。如果SACK塊數量大於等於限制值TCP_NUM_SACKS(4-由TCP選項空間大小決定),並且compressed_ack也大於了限制值TCP_FASTRETRANS_THRESH(3),發送ACK報文。接下來丟棄SACK數組的最後序號塊,將亂序報文的序號範圍添加到SACK數組首部。

需要注意的是,如果compressed_ack數量不大於TCP_FASTRETRANS_THRESH,本端將在不發送ACK的情況下,丟棄SACK序號塊。有可能導致對端對此段數據的重傳。

    if (this_sack >= TCP_NUM_SACKS) {
        if (tp->compressed_ack > TCP_FASTRETRANS_THRESH)
            tcp_send_ack(sk);
        this_sack--;
        tp->rx_opt.num_sacks--;
        sp--;
    }
    for (; this_sack > 0; this_sack--, sp--)
        *sp = *(sp - 1);

new_sack:
    /* Build the new head SACK, and we're done. */
    sp->start_seq = seq;
    sp->end_seq = end_seq;
    tp->rx_opt.num_sacks++;

發送路徑與Compressed-ACK

在發送函數__tcp_transmit_skb中,如果TCP設置了TCPHDR_ACK標誌,調用tcp_event_ack_sent處理ACK相關操作。

static int __tcp_transmit_skb(struct sock *sk, struct sk_buff *skb,
                  int clone_it, gfp_t gfp_mask, u32 rcv_nxt)
{
    ...
    if (likely(tcb->tcp_flags & TCPHDR_ACK))
        tcp_event_ack_sent(sk, tcp_skb_pcount(skb), rcv_nxt);

如果Comp-ACK數量已經超過TCP_FASTRETRANS_THRESH,將compressed_ack設置爲TCP_FASTRETRANS_THRESH,並嘗試取消停止compressed_ack_timer定時器。

static inline void tcp_event_ack_sent(struct sock *sk, unsigned int pkts, u32 rcv_nxt)
{
    struct tcp_sock *tp = tcp_sk(sk);

    if (unlikely(tp->compressed_ack > TCP_FASTRETRANS_THRESH)) {
        NET_ADD_STATS(sock_net(sk), LINUX_MIB_TCPACKCOMPRESSED, tp->compressed_ack - TCP_FASTRETRANS_THRESH);
        tp->compressed_ack = TCP_FASTRETRANS_THRESH;
        if (hrtimer_try_to_cancel(&tp->compressed_ack_timer) == 1)
            __sock_put(sk);
    }

    if (unlikely(rcv_nxt != tp->rcv_nxt))
        return;  /* Special ACK sent by DCTCP to reflect ECN */
    tcp_dec_quickack_mode(sk, pkts);
    inet_csk_clear_xmit_timer(sk, ICSK_TIME_DACK);
}

內核版本 5.0

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