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

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