1. 程式人生 > 其它 >聊一聊tcp 擁塞控制 三

聊一聊tcp 擁塞控制 三

擁塞控制狀態處理

/* 
open狀態: open狀態是常態, 這種狀態下tcp 傳送放通過優化後的快速路徑來接收處理ack,當一個ack到達時, 傳送方根據擁塞視窗是小於還是大於 滿啟動閾值,
    按照慢啟動或者擁塞避免來增大擁塞視窗
disorder 狀態: 當傳送方收到 DACK 或者SACK的時候, 將變為disorder 狀態, 再次狀態下擁塞視窗不做調整,但是沒到一個新到的段  就回觸發傳送一個新的段傳送出去
        此時TCP 遵循發包守恆原則,就是一個新包只有在一個老的包離開網路後才傳送
cwr 狀態:傳送發被通知出現擁塞通知, 直接告知!! 比喻通過icmp 源端抑制 等方式通知,當收到擁塞通知時,傳送方並不是立刻減少擁塞視窗, 而是每個一個新到
            的ack減小一個段 知道視窗減小到原來的一半為止,傳送方在減小視窗過程中如果沒有明顯重傳,就回保持cwr 狀態, 但是如果出現明顯重傳,就回被
            recovery 或者loss 中斷而進入 loss recovery 狀態;
recovery狀態:在收到足夠多的連續重複的ack 後,傳送方重傳第一個沒有被確認的段,進入recovery 狀態,預設情況下 連續收到三個重複的ack 就回進入recovery狀態,
            在recovery狀態期間,擁塞視窗的大小每隔一個新到的確認就會減少一個段, 和cwr 一樣 出於擁塞控制期間,這種視窗減少 終止於大小等於ssthresh,也就是
                        進入recovery狀態時視窗的一半。傳送方重傳被標記為丟失的段,或者根據包守恆原則 傳送資料,傳送方保持recovery 狀態直到所有recovery狀態中
             被 傳送的資料被確認,此時recovery狀態就回變為open,超時重傳可能中斷recovery狀態
Loss 狀態
:當一個RTO到期,傳送方進入Loss 狀態 , 所有正在傳送的段都被標記為丟失段,擁塞視窗設定為一個段。傳送方啟動滿啟動演算法增大視窗。Loss 狀態是
                擁塞視窗在被重置為一個段後增大,但是recovery狀態是擁塞視窗只能被減小, Loss 狀態不能被其他狀態中斷,所以只有所有loss 狀態下開始傳輸的資料
                 得到確認後,才能到open狀態, 也就是快速重傳不能在loss 狀態下觸發。
                 當一個RTO 超時, 或者收到ack 的確認已經被以前的sack 確認過, 則意味著我們記錄的sack資訊不能反應接收方實際的狀態,
                 此時就回進入Loss 狀態。
(1)Open:Normal state, no dubious events, fast path.
(2)Disorder:In all respects it is Open, but requres a bit more attention.
          It is entered when we see some SACKs or dupacks. It is split of Open
          mainly to move some processing from fast path to slow one.
(3)CWR:cwnd was reduced due to some Congestion Notification event.
          It can be ECN, ICMP source quench, local device congestion.
(4)Recovery:cwnd was reduced, we are fast-retransmitting.
(5)Loss:cwnd was reduced due to RTO timeout or SACK reneging.
              

Process an event, which can update packets-in-flight not trivially.
 * Main goal of this function is to calculate new estimate for left_out,
 * taking into account both packets sitting in receiver's buffer and
 * packets lost by network.
 *
 * Besides that it does CWND reduction, when packet loss is detected
 * and changes state of machine.
 *
 * It does _not_ decide what to send, it is made in function
 * tcp_xmit_retransmit_queue().
 
 tcp_fastretrans_alert() is entered:

(1)each incoming ACK, if state is not Open
(2)when arrived ACK is unusual, namely:
          SACK
          Duplicate ACK
          ECN ECE

 
*/ /*FACK的全稱是forward acknowledgement,FACK通過記錄SACK塊中系列號最大(forward-most)的SACK塊來推測丟包資訊, 在linux中使用fackets_out這個狀態變數來記錄FACK資訊。我們之前介紹SACK重傳時候說過在SACK下需要3個dup ACK來觸發快速重傳(3個為預設值), 而SACK下的dup ACK的定義與傳統的非SACK下定義又略有不同(詳細請參考前面的文章和示例)。當使能FACK的時候, 實際上我們可以通過一個SACK塊資訊來推測丟包情況進而觸發快速重傳。比如server端依次發出P1(0-9)、P2(10-19)、P3(20-29)、P4(30-39)、P5(40-49), 假設client端正常收到了P1包並回復了ACK確認包,P2、P3、P4則由於網路擁塞等原因丟失,client在收到P5時候回覆一個Ack=10的確認包, 並攜帶P5有SACK塊資訊(40-50),這樣server在收到P1的確認包和P5的dup ACK時候,就可以根據dup ACK中的SACK資訊得知client端收到了P1報文和P5報文, 計算出P1和P5兩個資料包中間間隔了3個數據包,達到了dup ACK門限(預設為3),進而推測出P2報文丟失。
*/ static void tcp_fastretrans_alert(struct sock *sk, const int acked, const int prior_unsacked, bool is_dupack, int flag) { struct inet_connection_sock *icsk = inet_csk(sk); struct tcp_sock *tp = tcp_sk(sk); //is_dupack = !(flag & (FLAG_SND_UNA_ADVANCED | FLAG_NOT_DUP));/* 判斷是不是重複的ACK*/
bool do_lost = is_dupack || ((flag & FLAG_DATA_SACKED) && // 新來的一個sack (tcp_fackets_out(tp) > tp->reordering));//tcp_fackets_out 返回 "空洞-亂序"的大小, 如果超過3 個 (預設) 就認為丟包。 // tp->reordering 表示 重複dupack 度量值 ; 預設為3 int fast_rexmit = 0; /* 如果packet_out 等待被ack的報文為0,那麼不可能有sacked_out(被sack 確認的段) */ if (WARN_ON(!tp->packets_out && tp->sacked_out)) tp->sacked_out = 0; /* fack中fack'd packets 的計數至少需要依賴一個SACK的段.--- 未確認和已選擇確認之間有段*/ if (WARN_ON(!tp->sacked_out && tp->fackets_out)) tp->fackets_out = 0; /* Now state machine starts. * A. ECE, hence prohibit cwnd undoing, the reduction is required. 禁止擁塞視窗撤銷,並開始減小擁塞視窗。*/ if (flag & FLAG_ECE) tp->prior_ssthresh = 0; /* B. In all the states check for reneging SACKs. 檢查是否為虛假的SACK,即ACK是否確認已經被SACK的資料 如果接收到的 ACK 指向已記錄的 SACK,這說明我們記錄的 SACK 並沒有反應接收方的真實狀態。 也就是說接收方現在已經處於嚴重的擁塞狀態或者在處理上有bug,那麼我們接下來就要按照重傳超時的方式去處理。 因為按照正常的邏輯流程,接受的 ACK不應該指向已記錄的 SACK,而應指向 SACK 並未包含的, 這說明接收方由於擁塞已經把 SACK 部分接收的段已經丟棄或者處理上有 BUG, 我們必須需要重傳*/ if (tcp_check_sack_reneging(sk, flag)) return; /* C. Check consistency of the current state. 確定left_out < packets_out 檢視是否從傳送佇列發出的包的數量是否不小於發出主機的包的數量 該函式的功能主要是判斷 left_out 是否大於 packets_out, 當然,這是不可能的,因 為前者是已經發送離開主機的未被確認的段數,而後者是已經離開發送佇列 (不一定離 開主機)但未確認的段數。故而,這裡有一個WARN_ON,以便輸出相應的警告資訊 */ tcp_verify_left_out(tp); /* D. Check state exit conditions. State can be terminated * when high_seq is ACKed. */ if (icsk->icsk_ca_state == TCP_CA_Open) { WARN_ON(tp->retrans_out != 0); tp->retrans_stamp = 0;/* 清除上次重傳階段第一個重傳段的傳送時間*/ } else if (!before(tp->snd_una, tp->high_seq)) {//tp->snd_una >= tp->high_seq switch (icsk->icsk_ca_state) { case TCP_CA_CWR: /* CWR is to be held something *above* high_seq * is ACKed for CWR bit to reach receiver. */ if (tp->snd_una != tp->high_seq) { // 第一個if 是》= 現在只需要判斷 》 就行 tcp_end_cwnd_reduction(sk);//需要snd_una > high_seq才能撤銷---結束視窗減小 tcp_set_ca_state(sk, TCP_CA_Open);//回到 OPen 狀態 } break; case TCP_CA_Recovery: /*TCP_CA_Recovery擁塞狀態接收到ACK報文,其ack_seq序號確認了high_seq之前的所有報文(SND.UNA >= high_seq), high_seq記錄了進入擁塞時的最大發送序號SND.NXT,故表明對端接收到了SND.NXT之前的所有報文,未發生丟包,需要撤銷擁塞狀態*/ if (tcp_is_reno(tp))//判斷對方是否提供了 SACK 服務,提供,返回 0, 否則返回 1 tcp_reset_reno_sack(tp);//設定 sacked_out 為 0 if (tcp_try_undo_recovery(sk))//嘗試從 Recovery 狀態撤銷 成功,就直接返回 return; tcp_end_cwnd_reduction(sk);//結束擁塞視窗縮小 break; } } /* Use RACK to detect loss */ if (sysctl_tcp_recovery & TCP_RACK_LOST_RETRANS && tcp_rack_mark_lost(sk)) flag |= FLAG_LOST_RETRANS; /* E. Process state. */ switch (icsk->icsk_ca_state) { case TCP_CA_Recovery: if (!(flag & FLAG_SND_UNA_ADVANCED)) {//判斷是否沒有段被確認 也就是 ack報文是否將send_un增長了 if (tcp_is_reno(tp) && is_dupack)////判斷是否啟用了 SACK, 未啟用返回 1, 並且收到的是重複的 ACK tcp_add_reno_sack(sk);///* 增加sacked_out (記錄接收到的重複的 ACK 數量 沒有啟用sack 就是 dupack了),檢查是否出現reorder*/ } else { /*於TCP_CA_Recovery擁塞狀態,如果ACK報文沒有確認全部的進入擁塞時SND.NXT(high_seq)之前的資料,僅確認了一部分(FLAG_SND_UNA_ADVANCED), 執行撤銷函式tcp_try_undo_partial*/ if (tcp_try_undo_partial(sk, acked, prior_unsacked, flag)) return; /* Partial ACK arrived. Force fast retransmit. */ do_lost = tcp_is_reno(tp) || tcp_fackets_out(tp) > tp->reordering; } /*對於處在TCP_CA_Recovery擁塞狀態的套介面,ACK報文並沒有推進SND.UNA序號,或者, 在partial-undo未執行的情況下,嘗試進行DSACK相關的撤銷操作,由函式tcp_try_undo_dsack完成。*/ if (tcp_try_undo_dsack(sk)) { tcp_try_keep_open(sk); return; } break; case TCP_CA_Loss: tcp_process_loss(sk, flag, is_dupack); if (icsk->icsk_ca_state != TCP_CA_Open && !(flag & FLAG_LOST_RETRANS)) return; /* Change state if cwnd is undone or retransmits are lost */ default: if (tcp_is_reno(tp)) {//判斷是否開啟了 SACK,沒啟用返回 1 if (flag & FLAG_SND_UNA_ADVANCED)// 也就是 snd_unack 序列號增長了 所以重置 tcp_reset_reno_sack(tp);//重置 sacked sacked_out 資料 被sack‘d 的資料可以clear了 if (is_dupack) tcp_add_reno_sack(sk);//如果是 dupack 則記錄並且sack_out ++ } if (icsk->icsk_ca_state <= TCP_CA_Disorder) tcp_try_undo_dsack(sk); //確定能夠離開 Disorder 狀態,而進入 Recovery 狀態。如果不進入 Recovery 狀態,判斷可否進入 OPen 狀態。 if (!tcp_time_to_recover(sk, flag)) { tcp_try_to_open(sk, flag, prior_unsacked);//如果不進入 Recovery 狀態,判斷可否進入 OPen 狀態。 return; } /* MTU probe failure: don't reduce cwnd */ if (icsk->icsk_ca_state < TCP_CA_CWR && icsk->icsk_mtup.probe_size && tp->snd_una == tp->mtu_probe.probe_seq_start) { tcp_mtup_probe_failed(sk); /* Restores the reduction we did in tcp_mtup_probe() */ tp->snd_cwnd++; tcp_simple_retransmit(sk); return; } /* Otherwise enter Recovery state */ tcp_enter_recovery(sk, (flag & FLAG_ECE)); fast_rexmit = 1; } //如果接收到重複的 ACK 或者重傳隊首的段超 //則要為確定丟失的段更新記分牌 if (do_lost)///* 更新記分牌,標誌丟失和超時的資料包,增加lost_out */ tcp_update_scoreboard(sk, fast_rexmit); tcp_cwnd_reduction(sk, prior_unsacked, fast_rexmit, flag); tcp_xmit_retransmit_queue(sk);//重傳重傳佇列中那些標記為 LOST 的段,同時重置 RTO 定時器。 }

檢查接收放是否違約

/* If ACK arrived pointing to a remembered SACK, it means that our
 * remembered SACKs do not reflect real state of receiver i.e.
 * receiver _host_ is heavily congested (or buggy).
 *
 * To avoid big spurious retransmission bursts due to transient SACK
 * scoreboard oddities that look like reneging, we give the receiver a
 * little time (max(RTT/2, 10ms)) to send us some more ACKs that will
 * restore sanity to the SACK scoreboard. If the apparent reneging
 * persists until this RTO then we'll clear the SACK scoreboard.
 如果接收到的確認 ACK 指向之前記錄的 SACK,這說明之前記錄的 SACK 並沒有
反映接收方的真實狀態。接收路徑上很有可能已經有擁塞發生或者接收主機正在經歷嚴
重的擁塞甚至處理出現了 BUG,因為按照正常的邏輯流程,接收的 ACK 不應該指向已
1988.9. TCP ACK CHAPTER 8. 非核心程式碼分析
記錄的 SACK,而應該指向 SACK 後面未接收的地方。通常情況下,此時接收方已經刪
除了儲存到失序佇列中的段。
為了避免短暫奇怪的看起來像是違約的 SACK 導致更大量的重傳,我們給接收者
一些時間, 即 max(RTT/2; 10ms) 以便於讓他可以給我們更多的 ACK,從而可以使得
SACK 的記分板變得正常一點。如果這個表面上的違約一直持續到重傳時間結束,我們
就把 SACK 的記分板清除掉
 */
static bool tcp_check_sack_reneging(struct sock *sk, int flag)
{
    if (flag & FLAG_SACK_RENEGING) {// 接收方違約了 
        struct tcp_sock *tp = tcp_sk(sk);
        unsigned long delay = max(usecs_to_jiffies(tp->srtt_us >> 4),
                      msecs_to_jiffies(10));//計算超時重傳時間
            //更新超時重傳定時器
        inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
                      delay, TCP_RTO_MAX);
        return true;
    }
    return false;
}

ACKCHECK標誌

SACK/ RENO/ FACK是否啟用

/* These functions determine how the current flow behaves in respect of SACK
 * handling. SACK is negotiated with the peer, and therefore it can vary
 * between different flows.
 *
 * tcp_is_sack - SACK enabled
 * tcp_is_reno - No SACK
 * tcp_is_fack - FACK enabled, implies SACK enabled
 */
static inline int tcp_is_sack(const struct tcp_sock *tp)
{
    return tp->rx_opt.sack_ok;
}

static inline bool tcp_is_reno(const struct tcp_sock *tp)
{
    return !tcp_is_sack(tp);
}

static inline bool tcp_is_fack(const struct tcp_sock *tp)
{
    return tp->rx_opt.sack_ok & TCP_FACK_ENABLED;
}

static inline void tcp_enable_fack(struct tcp_sock *tp)
{
    tp->rx_opt.sack_ok |= TCP_FACK_ENABLED;
}

  Reneging 的意思就是違約,接收方有權把已經報給傳送端 SACK 裡的資料給丟了。當然,我們肯定是不鼓勵這樣做的,因為這個事會把問題複雜化。但是,接收方可能會由於一些極端情況這麼做,比如要把記憶體給別的更重要的東西。所以,傳送方也不能完全依賴 SACK,主要還是要依賴 ACK,並維護 Time-Out。如果後續的 ACK 沒有增長,那麼還是要把 SACK 的東西重傳,另外,接收端這邊永遠不能把 SACK 的包標記為 Ack。   注意:SACK 會消費傳送方的資源,試想,如果一個攻擊者給資料傳送方發一堆 SACK 的選項,這會導致傳送方開始要重傳甚至遍歷已經發出的資料,這會消耗很多傳送端的資源

  SACK Selective Acknowledgment(SACK),這種方式需要在 TCP 頭裡加一個 SACK 的東西,ACK 還是 Fast Retransmit 的 ACK,SACK 則是彙報收到的資料碎版。這樣,在傳送端就可以根據回傳的 SACK 來知道哪些資料到了,哪些沒有到。於是就優化了 Fast Retransmit 的演算法。當然,這個協議需要兩邊都支援。

  D-SACK Duplicate SACK 又稱 D-SACK,其主要使用了 SACK 來告訴傳送方有哪些資料被重複接收了。D-SACK 使用了 SACK 的第一個段來做標誌,如果 SACK 的第一個段的範圍被 ACK 所覆蓋,那麼就是 D-SACK。如果 SACK 的第一個段的範圍被 SACK 的第二個段覆蓋,那麼就是 D-SACK。引入了 D-SACK,有這麼幾個好處: 

  • 可以讓傳送方知道,是發出去的包丟了,還是回來的 ACK 包丟了。
  • 是不是自己的 timeout 太小了,導致重傳。
  • 網路上出現了先發的包後到的情況(又稱 reordering)
  • 網路上是不是把我的資料包給複製了。

理一下幾個概念:

/* left_out = sacked_out + lost_out 
    sacked_out:Packets, which arrived to receiver out of order and hence not ACKed. With SACK this  number is simply amount of SACKed data. 
Even without SACKs it is easy to give pretty reliable  estimate of this number, counting duplicate ACKs.

lost_out:Packets lost by network. TCP has no explicit loss notification feedback from network
        (for now). It means that this number can be only guessed. Actually, it is the heuristics to predict  lossage that distinguishes different algorithms.
        F.e. after RTO, when all the queue is considered as lost, lost_out = packets_out and in_flight = retrans_out.
*/
static inline unsigned int tcp_left_out(const struct tcp_sock *tp)
{
    return tp->sacked_out + tp->lost_out;
}

/* This determines how many packets are "in the network" to the best
 * of our knowledge.  In many cases it is conservative, but where
 * detailed information is available from the receiver (via SACK
 * blocks etc.) we can make more aggressive calculations.
 *
 * Use this for decisions involving congestion control, use just
 * tp->packets_out to determine if the send queue is empty or not.
 *
 * Read this equation as:
 *
 *    "Packets sent once on transmission queue" MINUS
 *    "Packets left network, but not honestly ACKed yet" PLUS
 *    "Packets fast retransmitted"
 
 packets_out is SND.NXT - SND.UNA counted in packets.
 retrans_out is number of retransmitted segments.
left_out is number of segments left network, but not ACKed yet.

 */
static inline unsigned int tcp_packets_in_flight(const struct tcp_sock *tp)
{
    return tp->packets_out - tcp_left_out(tp) + tp->retrans_out;
}

各狀態的退出

Loss

  • icsk->icsk_retransmits = 0; /*超時重傳次數歸0*/
  • tcp_try_undo_recovery(sk);
  • 檢查是否需要undo,不管undo成功與否,都返回Open態。

CWR

If seq number greater than high_seq is acked, it indicates that the CWR indication has reached the peer TCP,call tcp_complete_cwr() to bring down the cwnd to ssthresh value.

Disorder

啟用sack,則tcp_try_undo_dsack(sk),交給它處理。

否則,tp->undo_marker = 0;

Recovery

tcp_try_undo_recovery(sk);

(1)CWR狀態

Q: 什麼時候進入CWR狀態?

A: 如果檢測到ACK包含ECE標誌,表示接收方通知傳送法進行顯示擁塞控制。

@tcp_try_to_open():

if (flag & FLAG_ECE)

tcp_enter_cwr(sk, 1);

tcp_enter_cwr()函式分析

它主要做了:

1. 重新設定慢啟動閾值。

2. 清除undo需要的標誌,不允許undo。

3. 記錄此時的最高序號(high_seq = snd_nxt),用於判斷退出時機。

4. 新增CWR標誌,用於通知接收方它已經做出反應。

5. 設定此時的狀態為TCP_CA_CWR。

Q: 在CWR期間採取什麼措施?

A: 擁塞視窗每隔一個確認段減小一個段,即每收到2個確認將擁塞視窗減1,直到擁塞視窗等於慢啟動閾值為止。

(2)Disorder狀態

Q: 什麼時候進入Disorder狀態?

A: 如果檢測到有被sacked的資料包,或者有重傳的資料包,則進入Disorder狀態。

當然,之前已經確認不能進入Loss或Recovery狀態了。

判斷條件: sacked_out、lost_out、retrans_out、undo_marker不為0。

Q: 在Disorder期間採取什麼措施?

A: 1. 設定CA狀態為TCP_CA_Disorder。

2. 記錄此時的最高序號(high_seq = snd_nxt),用於判斷退出時機。

3. 微調擁塞視窗,防止爆發式傳輸。

In Disorder state TCP is still unsure of genuiness of loss, after receiving acks with sack there may be

a clearing ack which acks many packets non dubiously in one go. Such a clearing ack may cause a

packet burst in the network, to avoid this cwnd size is reduced to allow no more than max_burst (usually 3)

number of packets.

(3)Open狀態

因為Open狀態是正常的狀態,是狀態處理的最終目的,所以不需要進行額外處理。

http代理伺服器(3-4-7層代理)-網路事件庫公共元件、核心kernel驅動 攝像頭驅動 tcpip網路協議棧、netfilter、bridge 好像看過!!!! 但行好事 莫問前程 --身高體重180的胖子