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

已标记关键词 清除标记
©️2020 CSDN 皮肤主题: 编程工作室 设计师:CSDN官方博客 返回首页