TCP回撤擁塞狀態

由於網絡路徑的變化或者延時的突然增加等,引發亂序並觸發快速恢復或者RTO超時,TCP將進入TCP_CA_Recovery或者TCP_CA_Loss擁塞狀態,如果隨後檢測到報文並沒有丟失,TCP將撤銷擁塞狀態,恢復到之前的擁塞狀態。

擁塞撤銷初始化

其一,在進入快速恢復階段時,不管是基於Reno或者SACK的快速恢復,還是RACK觸發的快速恢復,都將使用函數tcp_enter_recovery進入TCP_CA_Recovery擁塞階段。其中調用tcp_init_undo函數,初始化撤銷操作,以便在檢測到非必要進入恢復狀態後(如亂序加重導致的誤判、延時等),返回到原本的擁塞狀態。

void tcp_enter_recovery(struct sock *sk, bool ece_ack)
{
    struct tcp_sock *tp = tcp_sk(sk);

    tp->prior_ssthresh = 0;
    tcp_init_undo(tp);
    ...
    tcp_set_ca_state(sk, TCP_CA_Recovery);

其二,或者在RTO超時之後,滿足以下任一條件:

  • 當前擁塞狀態小於等於TCP_CA_Disorder,即還未進入恢復或者丟失擁塞狀態;
  • 或者high_seq不在SND.UNA之後,即當前沒有未完成的擁塞處理;
  • 或者當前擁塞狀態已經爲TCP_CA_Loss,但是還沒有重傳過報文;

調用tcp_init_undo函數初始化擁塞撤銷操作,以便FRTO或者Eifel算法檢測到爲不必要的超時(RTO值過小)後,恢復之前的擁塞狀態。注意,隨後將high_seq設置爲當前最大的發送序號SND.NXT,以示RTO超時恢復正在進行。

以上的三個判斷條件,暗示瞭如果RTO發生在TCP_CA_Recovery擁塞狀態,不初始化擁塞撤銷操作,因爲此時表明TCP_CA_Recovery狀態的撤銷未執行,發生的RTO超時,必定更加不能撤銷。

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

    tcp_timeout_mark_lost(sk);

    /* Reduce ssthresh if it has not yet been made inside this window. */
    if (icsk->icsk_ca_state <= TCP_CA_Disorder ||
        !after(tp->high_seq, tp->snd_una) ||
        (icsk->icsk_ca_state == TCP_CA_Loss && !icsk->icsk_retransmits)) {
        tp->prior_ssthresh = tcp_current_ssthresh(sk);
        tp->prior_cwnd = tp->snd_cwnd;
        tp->snd_ssthresh = icsk->icsk_ca_ops->ssthresh(sk);
        tcp_ca_event(sk, CA_EVENT_LOSS);
        tcp_init_undo(tp);
    }
	tp->high_seq = tp->snd_nxt;

擁塞撤銷初始化函數tcp_init_undo如下,記錄進入TCP_CA_Recovery或者TCP_CA_Loss狀態時的SND.UNA值到undo_marker變量中。變量undo_retrans記錄可撤銷的重傳報文數量(非必要的重傳數量)。

static inline void tcp_init_undo(struct tcp_sock *tp)
{
    tp->undo_marker = tp->snd_una;
    /* Retransmission still in flight may cause DSACKs later. */
    tp->undo_retrans = tp->retrans_out ? : -1;

進入TCP_CA_Recovery擁塞狀態

如下tcp_fastretrans_alert函數所示,根據函數tcp_time_to_recover的返回結果判斷何時進入TCP_CA_Recovery擁塞狀態。

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) {
    default:
        if (tcp_is_reno(tp)) {
            ...
            tcp_add_reno_sack(sk, num_dupack);
        }
		tcp_identify_packet_loss(sk, ack_flag);
        if (!tcp_time_to_recover(sk, flag)) {
            tcp_try_to_open(sk, flag);
            return;
        }
        ...
        tcp_enter_recovery(sk, (flag & FLAG_ECE));

函數tcp_time_to_recover依據丟包或者dupack/sack數量來判斷是否進入快速恢復階段。對於Reno算法,使用函數tcp_add_reno_sack增加dupacks數量;對於SACK算法使用tcp_sacktag_write_queue函數計算sack確認報文數量。以上調用的函數tcp_identify_packet_loss負責設置Reno和RACK算法的丟包數量。

如下判斷,如果有丟包(lost_out),或者dupacks數量超出亂序級別,需要進入TCP_CA_Recovery狀態。不同於基於dupack的算法,RACK基於時間判斷丟包。

static bool tcp_time_to_recover(struct sock *sk, int flag)
{
    struct tcp_sock *tp = tcp_sk(sk);

    /* Trick#1: The loss is proven. */
    if (tp->lost_out)
        return true;

    /* Not-A-Trick#2 : Classic rule... */
    if (!tcp_is_rack(sk) && tcp_dupack_heuristics(tp) > tp->reordering)
        return true;

    return false;

對於RACK算法,在其超時處理函數中,如果tcp_rack_detect_loss檢測到了丟包,網絡中的報文數量(flightsize)必然發生了變化(原始報文或者重傳報文丟失),套接口進入TCP_CA_Recovery狀態。

void tcp_rack_reo_timeout(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
    u32 timeout, prior_inflight;

    prior_inflight = tcp_packets_in_flight(tp);
    tcp_rack_detect_loss(sk, &timeout);
    if (prior_inflight != tcp_packets_in_flight(tp)) {
        if (inet_csk(sk)->icsk_ca_state != TCP_CA_Recovery) {
            tcp_enter_recovery(sk, false);

撤銷TCP_CA_Recovery狀態一(full)

如果在TCP_CA_Recovery擁塞狀態接收到ACK報文,其ack_seq序號確認了high_seq之前的所有報文(SND.UNA >= high_seq),如上節所述,high_seq記錄了進入擁塞時的最大發送序號SND.NXT,故表明對端接收到了SND.NXT之前的所有報文,未發生丟包,需要撤銷擁塞狀態,由函數tcp_try_undo_recovery實現。

static void tcp_fastretrans_alert(struct sock *sk, const u32 prior_snd_una,
                  int num_dupack, int *ack_flag, int *rexmit)
{
    int fast_rexmit = 0, flag = *ack_flag;
    bool do_lost = num_dupack || ((flag & FLAG_DATA_SACKED) &&
                      tcp_force_fast_retransmit(sk));
    ...
    if (icsk->icsk_ca_state == TCP_CA_Open) {
        ...
    } else if (!before(tp->snd_una, tp->high_seq)) {
        switch (icsk->icsk_ca_state) {
        ...
        case TCP_CA_Recovery:
            if (tcp_is_reno(tp))
                tcp_reset_reno_sack(tp);
            if (tcp_try_undo_recovery(sk))
                return;
            tcp_end_cwnd_reduction(sk);
            break;
        }
    }

函數tcp_try_undo_recovery完成擁塞撤銷,首先由tcp_may_undo進一步確認是否需要恢復擁塞窗口。undo_marker表明套接口進入了擁塞狀態(TCP_CA_Recovery/TCP_CA_Loss),調整了擁塞窗口,否則沒有必要進行恢復窗口操作。並且需要滿足以下條件中的一個:

  1. undo_retrans等於0. 報文重傳之後被D-SACK確認,表明這些重傳爲不必要的,原始報文未丟失。
  2. retrans_stamp等於0(重傳報文時間戳retrans_stamp等於零). 在進入擁塞狀態後還沒有進行過任何重傳,或者重傳報文都已送達。
  3. 接收到的ACK確認報文中的回覆時間戳(rcv_tsecr)在重傳報文的時間戳之前,表明是對於原始報文的確認,而不是對重傳報文。
static inline bool tcp_may_undo(const struct tcp_sock *tp)
{
    return tp->undo_marker && (!tp->undo_retrans || tcp_packet_delayed(tp));
}
static inline bool tcp_packet_delayed(const struct tcp_sock *tp)
{
    return !tp->retrans_stamp || tcp_tsopt_ecr_before(tp, tp->retrans_stamp);
}
static bool tcp_tsopt_ecr_before(const struct tcp_sock *tp, u32 when)
{
    return tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr &&
           before(tp->rx_opt.rcv_tsecr, when);
}

以上條件確信網絡並沒有發生擁塞,恢復之前的擁塞窗口。函數tcp_try_undo_recovery執行擁塞窗口恢復(tcp_undo_cwnd_reduction)。

static bool tcp_try_undo_recovery(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);
 
    if (tcp_may_undo(tp)) { 
        /* Happy end! We did not retransmit anything or our original transmission succeeded.
         */
        DBGUNDO(sk, inet_csk(sk)->icsk_ca_state == TCP_CA_Loss ? "loss" : "retrans");
        tcp_undo_cwnd_reduction(sk, false);
        ...
    } else if (tp->rack.reo_wnd_persist) {
        tp->rack.reo_wnd_persist--;
    }

另外,對於Reno算法,如果當前窗口中還有重傳報文存在於網絡中,保留retrans_stamp的值,避免這些重傳報文觸發dupack,再次引起錯誤的快速重傳,此時需要保持擁塞狀態不撤銷,當再次接收到新的ACK報文(tcp_try_undo_recovery再次運行,但不會再執行以上的撤銷擁塞窗口部分),確認SND.UNA大於high_seq後,進入TCP_CA_Open狀態。

否則,如果SND.UNA大於high_seq,套接口直接恢復到TCP_CA_Open狀態。

    if (tp->snd_una == tp->high_seq && tcp_is_reno(tp)) {
        /* Hold old state until something *above* high_seq
         * is ACKed. For Reno it is MUST to prevent false
         * fast retransmits (RFC2582). SACK TCP is safe. */
        if (!tcp_any_retrans_done(sk))
            tp->retrans_stamp = 0;
        return true;
    }
    tcp_set_ca_state(sk, TCP_CA_Open);
    tp->is_sack_reneg = 0;
    return false;

撤銷TCP_CA_Recovery狀態二(undo_partial)

對於TCP_CA_Recovery擁塞狀態,如果ACK報文沒有確認全部的進入擁塞時SND.NXT(high_seq)之前的數據,僅確認了一部分(FLAG_SND_UNA_ADVANCED),執行撤銷函數tcp_try_undo_partial。

static void tcp_fastretrans_alert(struct sock *sk, const u32 prior_snd_una,
                  int num_dupack, int *ack_flag, int *rexmit)
{
   /* E. Process state. */
   switch (icsk->icsk_ca_state) {
   case TCP_CA_Recovery:
       if (!(flag & FLAG_SND_UNA_ADVANCED)) {
           ...
       } else {
           if (tcp_try_undo_partial(sk, prior_snd_una))
               return;
           /* Partial ACK arrived. Force fast retransmit. */
           do_lost = tcp_is_reno(tp) || tcp_force_fast_retransmit(sk);
       }
       if (tcp_try_undo_dsack(sk)) {
           tcp_try_keep_open(sk);
           return;
       }
       tcp_identify_packet_loss(sk, ack_flag);
       break;

如下函數tcp_try_undo_partial所示,與tcp_try_undo_recovery函數不同,這裏沒有使用tcp_may_undo進行撤銷擁塞窗口的判斷,而是使用了其中的一部分,即tcp_packet_delayed判斷報文是否僅是被延遲了,忽略undo_retrans值的判斷。其邏輯是在接收到部分確認ACK的情況下,只要tcp_packet_delayed成立,原始報文就沒有丟失而是被延時了,就應檢查當前的亂序級別設置是否需要更新(tcp_check_sack_reordering),防止快速重傳被誤觸發。

另外,需要注意一點,在上一節函數tcp_try_undo_recovery的處理中,最終成功的進行了恢復,所以並不調整亂序級別。而這裏的部分確認,則表明亂序級別低估了。

雖然不進行undo_retrans值的判斷,但是,這裏判斷變量retrans_out(重傳報文數量)是否有值,如果網絡中還有發出的重傳報文,不進行擁塞窗口的撤銷操作,函數結束處理,等待重傳報文被確認。

static bool tcp_try_undo_partial(struct sock *sk, u32 prior_snd_una)
{
    struct tcp_sock *tp = tcp_sk(sk);
 
    if (tp->undo_marker && tcp_packet_delayed(tp)) {
        /* Plain luck! Hole if filled with delayed
         * packet, rather than with a retransmit. Check reordering.
         */
        tcp_check_sack_reordering(sk, prior_snd_una, 1);
 
        /* We are getting evidence that the reordering degree is higher
         * than we realized. If there are no retransmits out then we
         * can undo. Otherwise we clock out new packets but do not
         * mark more packets lost or retransmit more.
         */
        if (tp->retrans_out) 
            return true;

變量retrans_stamp記錄了第一個重傳報文的時間戳,如果已經沒有了重傳報文,清零此時間戳。函數tcp_try_keep_open嘗試進入TCP_CA_Open狀態,但是,如果套接口還有亂序報文或者丟失報文,將進入TCP_CA_Disorder擁塞狀態。

        if (!tcp_any_retrans_done(sk))
            tp->retrans_stamp = 0;

        DBGUNDO(sk, "partial recovery");
        tcp_undo_cwnd_reduction(sk, true);
        NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPPARTIALUNDO);
        tcp_try_keep_open(sk);
        return true;
    }
    return false;

撤銷TCP_CA_Recovery狀態三(dsack)

在函數tcp_fastretrans_alert中,對於處在TCP_CA_Recovery擁塞狀態的套接口,ACK報文並沒有推進SND.UNA序號,或者,在partial-undo未執行的情況下,嘗試進行DSACK相關的撤銷操作,由函數tcp_try_undo_dsack完成。

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_Recovery:
        ...
        if (tcp_try_undo_dsack(sk)) {
            tcp_try_keep_open(sk);
            return;
        }
        tcp_identify_packet_loss(sk, ack_flag);
        break;
    case TCP_CA_Loss:
        ...
    default:
        ...
        if (icsk->icsk_ca_state <= TCP_CA_Disorder)
            tcp_try_undo_dsack(sk);

以下tcp_try_undo_dsack函數,如果undo_marker有值,並且undo_retrans爲零,表明所有的重傳報文都被D-SACK所確認,即重傳是不必要的,執行擁塞窗口恢復操作。

/* Try to undo cwnd reduction, because D-SACKs acked all retransmitted data */
static bool tcp_try_undo_dsack(struct sock *sk)
{
    struct tcp_sock *tp = tcp_sk(sk);

    if (tp->undo_marker && !tp->undo_retrans) {
        tp->rack.reo_wnd_persist = min(TCP_RACK_RECOVERY_THRESH,
                           tp->rack.reo_wnd_persist + 1);
        DBGUNDO(sk, "D-SACK");
        tcp_undo_cwnd_reduction(sk, false);
        NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPDSACKUNDO);
        return true;
    }
    return false;

在DSACK判斷函數tcp_check_dsack中,如果SACK序號塊被認定爲DSACK,並且undo_retrans大於零(進行過重傳操作),並且,DSACK序號塊的終止序號滿足如下條件:

(prior_SND.UNA >= end_seq_0 > undo_marker)

表明對端接收到了原始報文和擁塞之後發送的重傳報文,將undo_retrans遞減一。

static bool tcp_check_dsack(struct sock *sk, const struct sk_buff *ack_skb,
                struct tcp_sack_block_wire *sp, int num_sacks, u32 prior_snd_una)
{   
    struct tcp_sock *tp = tcp_sk(sk);
    u32 start_seq_0 = get_unaligned_be32(&sp[0].start_seq);
    u32 end_seq_0 = get_unaligned_be32(&sp[0].end_seq);

    ...
    /* D-SACK for already forgotten data... Do dumb counting. */
    if (dup_sack && tp->undo_marker && tp->undo_retrans > 0 &&
        !after(end_seq_0, prior_snd_una) &&
        after(end_seq_0, tp->undo_marker))
        tp->undo_retrans--;
    
    return dup_sack;

另外,在函數tcp_sacktag_one中,也進行如上的判斷。

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)
{           
    struct tcp_sock *tp = tcp_sk(sk);
            
    /* Account D-SACK for retransmitted packet. */
    if (dup_sack && (sacked & TCPCB_RETRANS)) {
        if (tp->undo_marker && tp->undo_retrans > 0 &&
            after(end_seq, tp->undo_marker))
            tp->undo_retrans--;

進入TCP_CA_Loss狀態

內核只有在報文的重傳定時器到期時,在tcp_retransmit_timer函數中,進入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_init_undo初始化擁塞撤銷操作。

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

    tcp_timeout_mark_lost(sk);

    /* Reduce ssthresh if it has not yet been made inside this window. */
    if (icsk->icsk_ca_state <= TCP_CA_Disorder || !after(tp->high_seq, tp->snd_una) ||
        (icsk->icsk_ca_state == TCP_CA_Loss && !icsk->icsk_retransmits)) {
        tp->prior_ssthresh = tcp_current_ssthresh(sk);
        tp->prior_cwnd = tp->snd_cwnd;
        tp->snd_ssthresh = icsk->icsk_ca_ops->ssthresh(sk);
        tcp_ca_event(sk, CA_EVENT_LOSS);
        tcp_init_undo(tp);
    }
	tp->high_seq = tp->snd_nxt;

撤銷TCP_CA_Loss狀態

對於處在TCP_CA_Loss狀態的套接口,由函數tcp_process_loss進行處理,稍後介紹其中的擁塞撤銷操作。

static void tcp_fastretrans_alert(struct sock *sk, const u32 prior_snd_una,
                  int num_dupack, int *ack_flag, int *rexmit)
{
    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狀態的套接口,如果ACK報文推進了SND.UNA序號,嘗試進行TCP_CA_Loss狀態撤銷,由函數tcp_try_undo_loss完成。對於FRTO,如果S/ACK確認了並沒有重傳的報文(原始報文),同樣嘗試進入撤銷流程,因爲此ACK報文表明RTO值設置的不夠長(並非擁塞導致報文丟失),過早進入了TCP_CA_Loss狀態。

如果SND.UNA不在high_seq之前,表明恢復流程已經結束,進入TCP_CA_Loss狀態時的發送報文(SND.NXT(high_seq))都已經被確認,執行tcp_try_undo_recovery。

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;
        ...
    }
    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_try_undo_loss,如果FRTO執行撤銷操作,或者tcp_may_undo(參見以上介紹)檢測到需要執行撤銷,調用tcp_undo_cwnd_reduction函數恢復擁塞窗口。

/* Undo during loss recovery after partial ACK or using F-RTO. */
static bool tcp_try_undo_loss(struct sock *sk, bool frto_undo)
{
    struct tcp_sock *tp = tcp_sk(sk);

    if (frto_undo || tcp_may_undo(tp)) {
        tcp_undo_cwnd_reduction(sk, true);

        DBGUNDO(sk, "partial loss");
        NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPLOSSUNDO);
        if (frto_undo) NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPSPURIOUSRTOS);
        inet_csk(sk)->icsk_retransmits = 0;
        if (frto_undo || tcp_is_sack(tp)) {
            tcp_set_ca_state(sk, TCP_CA_Open);
            tp->is_sack_reneg = 0;
        }
        return true;
    }
    return false;

內核版本 5.0

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