Forward-RTO超时确认

虚假的重传超时将导致TCP性能的降低,因为这将发送不必要的重传报文。Forward-RTO是一种用于检测虚假重传超时的一种算法,F-RTO仅针对发送端,其不需要任何TCP选项的支持。在超时重传第一个未确认的报文之后,TCP发送端的F-RTO检测回来的ACK报文,来确定此次超时是否是虚假的。进而,决定是否发送新的报文,或是重传未确认报文。

不同于通常的RTO处理,如果接收到的第一个ACK确认了新数据(还无法区分对端接收了原始数据还是重传数据),F-RTO将尝试发送之前未发送的数据。之后,在接收到超时后第二个ACK报文时,如果此ACK报文再次确认了之前的未重传报文,表明超时为由于链路延时增大引起,不是真实的超时发生。但是,如果在超时之后,接收到两个重复ACK(未确认数据),将不能够认定超时的虚假性,使用传统的RTO恢复算法。

以上是针对传统的ACK确认流程的F-RTO,对于支持SACK选项的F-RTO,当接收到重复ACK报文时,也可进行虚假重传的判断。对于超时之后的第一个ACK报文,如果其不是对重传报文的确认,而是一个重复ACK报文,记录其中的SACK信息,继续等待之后的ACK报文,如果接下来收到ACK报文确认了超时之前的报文,发送之前未发送过的报文(与非SACK-FRTO相同)。当再次接收到ACK报文时,无论是ACK还是SACK,如果其确认了超时之前未重传的报文,则判定超时是不必要的,与非SACK-FRTO不同,此时如果接收到重复ACK报文,但是其中的SACK如果确认了超时之前的未确认报文,也判定超时是不必要的。最后,如果在接收到的SACK块序号之间存在不连续情形,依据SACK恢复算法进行重传。

对于F-RTO,在接收到ACK报文时,如果其确认了超时之前的所有报文,将无法判断超时的必要性。例如,在快速重传(3dupACK)时,如果重传报文丢失,随后RTO处理中将再次重传此报文,但是丢失报文之后的报文已经在超时之前由接收端正确接收(触发了dupACK),在超时之后的ACK将确认所有报文,但是超时处理时必要的。但是,超时之后的ACK需要完整的确认重传的报文,否则,错误的接收端可通过确认部分报文,导致发送端判定超时不必要。

F-RTO初始化

如下内核函数tcp_sk_init,将sysctl_tcp_frto的值初始化为2,即默认情况下F-RTO处于开启状态。

static int __net_init tcp_sk_init(struct net *net)
{
    net->ipv4.sysctl_tcp_frto = 2;

开启F-RTO

在超时重传函数tcp_retransmit_timer中,调用以下tcp_enter_loss函数,设置拥塞状态TCP_CA_Loss及相关变量,之后,重新发送重传队列头部的报文。

void tcp_retransmit_timer(struct sock *sk)
{
    tcp_enter_loss(sk);

    if (tcp_retransmit_skb(sk, tcp_rtx_queue_head(sk), 1) > 0) {

如下tcp_enter_loss函数,如果之前的拥塞状态小于TCP_CA_Recovery,未处在拥塞恢复阶段,或者重传超时发生了嵌套。并且此时不是路径MTU探测,置位套接口的frto,开启F-RTO,将当前的SND.NXT值记录到high_seq中。

void tcp_enter_loss(struct sock *sk)
{
    const struct inet_connection_sock *icsk = inet_csk(sk);
    bool new_recovery = icsk->icsk_ca_state < TCP_CA_Recovery;

    ...
    tcp_set_ca_state(sk, TCP_CA_Loss);
    tp->high_seq = tp->snd_nxt;
    tcp_ecn_queue_cwr(tp);

    /* F-RTO RFC5682 sec 3.1 step 1: retransmit SND.UNA if no previous
     * loss recovery is underway except recurring timeout(s) on
     * the same SND.UNA (sec 3.2). Disable F-RTO on path MTU probing
     */
    tp->frto = net->ipv4.sysctl_tcp_frto &&
           (new_recovery || icsk->icsk_retransmits) &&
           !inet_csk(sk)->icsk_mtup.probe_size;

F-RTO处理

上节在重传之后,等待接收ACK报文,处理入口函数tcp_ack。子函数tcp_fastretrans_alert将调用tcp_process_loss处理拥塞状态TCP_CA_Loss的情形。

static void tcp_fastretrans_alert(struct sock *sk, const u32 prior_snd_una,
                  int num_dupack, int *ack_flag, int *rexmit)
{
    switch (icsk->icsk_ca_state) {
    case TCP_CA_Loss:
        tcp_process_loss(sk, flag, num_dupack, rexmit);
        tcp_identify_packet_loss(sk, ack_flag);
        if (!(icsk->icsk_ca_state == TCP_CA_Open || (*ack_flag & FLAG_LOST_RETRANS)))
            return;
        /* Change state if cwnd is undone or retransmits are lost */
        /* fall through */

以下TCP_CA_Loss拥塞状态处理函数tcp_process_loss,首先,如果ACK将SND.UNA进行了向后移动,对端确认接收了新数据,在函数tcp_try_undo_loss中,将根据Eifel检测算法判断此ACK是由原始报文还是重传报文所触发,如果由原始报文触发,尝试退出TCP_CA_Loss状态,结束处理。

随后是F-RTO的处理,判断ACK/SACK是否确认了并没有重传过的数据(序号位于high_seq之前,即开启F-RTO之前的发送数据),结果为真的话,表明超时重传时间之前发送的数据得到了确认,判定进行了不必要的超时重传,退出TCP_CA_Loss拥塞状态,函数tcp_try_undo_loss的第二个参数为true,即其不再进行判断,直接退出TCP_CA_Loss状态。

关于标志FLAG_ORIG_SACK_ACKED将在最后进行介绍。

static void tcp_process_loss(struct sock *sk, int flag, int num_dupack, int *rexmit)
{
    struct tcp_sock *tp = tcp_sk(sk);
    bool recovered = !before(tp->snd_una, tp->high_seq);

    if ((flag & FLAG_SND_UNA_ADVANCED) && tcp_try_undo_loss(sk, false))
        return;

    if (tp->frto) { /* F-RTO RFC5682 sec 3.1 (sack enhanced version). */
        /* Step 3.b. A timeout is spurious if not all data are
         * lost, i.e., never-retransmitted data are (s)acked.
         */
        if ((flag & FLAG_ORIG_SACK_ACKED) && tcp_try_undo_loss(sk, true))
            return;

之后,首先看一下elseif判断部分,如果ACK将SND.UNA的值向后移动,但是SND.UNA还位于high_seq(超时开始时记录的SND.NXT值)之前,即确认了部分数据,更新high_seq记录,如果发送队列还有数据(未曾发送过),并且发送窗口允许,发送新数据(按照Slow-Start的定义,在收到ACK之后,增加CWND为2,此时可最多发送2个报文)。否则,清零frto,执行传统的超时重传。

回过头看一下if判断部分,根据RFC5682,在以上新报文的发送后,接收到的ACK报文的处理中,再次进入此函数,由于发送了新报文,SND.NXT已经大于high_seq,如果此时的ACK为重复ACK,或者SACK未确认新数据,判定丢包确实发生,关闭frto。

        if (after(tp->snd_nxt, tp->high_seq)) {
            if (flag & FLAG_DATA_SACKED || num_dupack)
                tp->frto = 0; /* Step 3.a. loss was real */
        } else if (flag & FLAG_SND_UNA_ADVANCED && !recovered) {
            tp->high_seq = tp->snd_nxt;
            /* Step 2.b. Try send new data (but deferred until cwnd
             * is updated in tcp_ack()). Otherwise fall back to
             * the conventional recovery.
             */
            if (!tcp_write_queue_empty(sk) && after(tcp_wnd_end(tp), tp->snd_nxt)) {
                *rexmit = REXMIT_NEW;
                return;
            }
            tp->frto = 0;
        }
    }

如果ACK报文确认了超时重传发生时的所有报文,即SND.UNA>high_seq,表明已经由TCP_CA_Loss状态恢复,清除拥塞状态。

    if (recovered) {
        /* F-RTO RFC5682 sec 3.1 step 2.a and 1st part of step 3.a */
        tcp_try_undo_recovery(sk);
        return;
    }

在以上函数tcp_process_loss中,如果F-RTO需要发送新报文才能判断超时重传的虚假性,在tcp_ack报文中,由函数tcp_xmit_recovery执行发送操作。

static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag)
{
    ...
    if (tcp_ack_is_dubious(sk, flag)) {
        ...
        tcp_fastretrans_alert(sk, prior_snd_una, num_dupack, &flag, &rexmit);
    }
    tcp_xmit_recovery(sk, rexmit);
    return 1;

如果重传变量rexmit设置为REXMIT_NEW(2),发送新报文,并且关闭nagle。如果发出了新报文,SND.NXT的值必定大于high_seq,否则,未发送报文,关闭frto,进行通常的RTO报文重传。

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

    if (unlikely(rexmit == 2)) {
        __tcp_push_pending_frames(sk, tcp_current_mss(sk), TCP_NAGLE_OFF);
        if (after(tp->snd_nxt, tp->high_seq))
            return;
        tp->frto = 0;
    }
    tcp_xmit_retransmit_queue(sk);

后记

关于FLAG_ORIG_SACK_ACKED标志,内核中有两处置位。其一在tcp_clean_rtx_queue函数中,如果报文没有经过重传,也没有被SACK确认,并且其结束序号在high_seq之前,当接收到对此报文的确认时,设置FLAG_ORIG_SACK_ACKED标志,表明在重传超时之前发送的报文(未经过重传),在重传超时之后,由ACK确认。

static int tcp_clean_rtx_queue(struct sock *sk, u32 prior_fack, u32 prior_snd_una, struct tcp_sacktag_state *sack)
{
    for (skb = skb_rb_first(&sk->tcp_rtx_queue); skb; skb = next) {
        ...
        if (unlikely(sacked & TCPCB_RETRANS)) {
            ...
        } else if (!(sacked & TCPCB_SACKED_ACKED)) {
            ...
            if (!after(scb->end_seq, tp->high_seq))
                flag |= FLAG_ORIG_SACK_ACKED;
        }

另外一处是SACK块处理函数tcp_sacktag_one中,如果报文未经过SACK确认,并且也没有重传过,其结束序号位于high_seq之前(超时之前已发送),设置标志FLAG_ORIG_SACK_ACKED,表示在超时重传之后,其由当前SACK确认。

static u8 tcp_sacktag_one(struct sock *sk, struct tcp_sacktag_state *state, u8 sacked,
              u32 start_seq, u32 end_seq, int dup_sack, int pcount, u64 xmit_time)
{
    if (!(sacked & TCPCB_SACKED_ACKED)) {
        if (sacked & TCPCB_SACKED_RETRANS) {
            ...
        } else {
            if (!(sacked & TCPCB_RETRANS)) {
                /* New sack for not retransmitted frame, which was in hole. It is reordering.
                 */
                if (before(start_seq, tcp_highest_sack_seq(tp)) && before(start_seq, state->reord))
                    state->reord = start_seq;
                
                if (!after(end_seq, tp->high_seq))
                    state->flag |= FLAG_ORIG_SACK_ACKED;

内核版本 5.0

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