一、概述
我們之前介紹過Tahoe版本中,無論是快速重傳還是RTO超時重傳,都會觸發乘法減小,將cwnd置為1,然后重新開始慢啟動過程。在reno版本中引入快速回復,當發生快速重傳的時候,就會觸發快速恢復過程,但是reno中的快速恢復過程在收到partial ACK的時候就會退出。在new reno中對快速恢復進行了改進,只有當收到的ack number越過recovery point的時候,才會退出快速恢復過程,而對於partial ACK,new reno則會立即進行一個快速重傳。
RFC2582給出了new reno算法,即在沒有SACK功能的情況下的快速恢復,我簡單描述一下,詳細的請參考RFC2582。
當TCP收到3個dup ACK觸發快速重傳的時候,先設置ssthresh = max (FlightSize / 2, 2*MSS),然后重傳丟失的報文,設置cwnd=ssthresh+3*MSS,這個協議稱為inflate,這個反映了有三個數據包離開中間網絡到達了對端。
對於隨后每個dup ACK,進行inflate操作:cwnd=cwnd+MSS,反映有一個數據包到達對端,如果cwnd和awnd允許,那么此時可以傳輸新的TCP數據
在收到partial ACK的時候,立即重傳ack number對應的數據包,假設這個partial ACK的ack number確認了n*MSS的數據,那么更新cwnd=cwnd-n*MSS,這個協議稱呼為deflate,接着更新cwnd=cwnd+1。
當收到的ACK報文使TCP越過Recovery point的時候,退出快速恢復,更新cwnd=min (ssthresh, FlightSize + MSS),或者直接更新cwnd=ssthresh,但是采用后一種更新方式的時候要避免cwnd的突然增大,而造成burst業務,影響網絡穩定性。
二、Linux中的實現
Linux中的reno擁塞控制算法並不僅僅是BSD原始的reno擁塞控制算法,可以看成是BSD原始reno擁塞控制算法的擴展。在linux中設置reno擁塞控制算法的時候,也會進行快速恢復的過程,但是這個快速恢復的過程並不是reno擁塞控制算法實現的。下面我們介紹一下SACK關閉時候Linux中的快速恢復算法。
Linux中TCP連接初始建立的時候處於Open狀態,當收到dup ACK的時候會立即進入Disorder狀態,當連續收到的dup ACK總數達到dupthresh門限的時候,TCP會進入Recovery狀態,然后觸發快速重傳,進入快速恢復過程。當收到的ack number越過recovery point的時候就會從Recovery狀態切換到正常的Open狀態。
Open狀態下慢啟動和擁塞避免對於cwnd的更新我們前面已經有介紹了,而Disorder狀態下因為dup ACK的ack number並沒有確認新的數據(這里指SACK關閉的場景下的dup ACK),因此cwnd並不會更新,下面我們則要介紹一下Recovery狀態下cwnd的更新。下面這個流程不僅僅是關閉SACK的時候會用到,后面我們會介紹SACK和FACK下的快速恢復,同樣也會使用到這個cwnd更新流程,所以說這個很重要啊。
Linux從Disorder狀態進入Recovery狀態的時候,會初始化prior_ssthresh=max(ssthresh,3/4*cwnd) , ssthresh=max(cwnd/2,2), prior_cwnd=cwnd, prr_delivered=0, prr_out=0。接着會進行下面的cwnd更新流程,在隨后的Recovery狀態下收到dup ACK或者partial ACK的時候都會執行下面的cwnd更新流程。相關新增的變量我已經在里面注釋說明了。linux中並不會按照RFC2582來inflate或者deflate擁塞窗口,而是使用sacked_out這個狀態變量來達到和協議中inflate和deflate相同的效果。
//進入Recovery狀態時候 初始化prr_delivered、prr_out為0
//newly_acked_sacked:表示收到的這個ACK報文的ack number和sack確認了多少個數據包,對於關閉SACK的情況下,dup ACK也算是新確認了一個報文
//newly_acked_sacked的值實際上就是收到這個ACK報文前(packets_out-sacked_out)和收到這個報文后(packets_out-sacked_out)的差值
if (newly_acked_sacked <= 0 )
return;
delta = ssthresh - in_flight;
prr_delivered += newly_acked_sacked;
if (delta < 0) {
//注意下面的除法要向下取整
sndcnt = (ssthresh * prr_delivered + prior_cwnd - 1)/prior_cwnd - prr_out
} else if (ack number新確認了之前重傳的數據且RACK沒有標記重傳報文丟失) {
sndcnt = min(delta, max(prr_delivered - prr_out,newly_acked_sacked) + 1);
} else {
sndcnt = min(delta, newly_acked_sacked);
}
//fast_rexmit:當前dup ACK是否已經達到了dupthresh門限而需要快速重傳,僅在初始進入Recovery狀態觸發快速重傳的時候置位
sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0));
cwnd = in_flight + sndcnt;
//然后根據擁塞窗口大小可能會進行數據重傳或者新數據的發送 並更新prr_out
可以看到linux實現中在進入快速恢復的時候cwnd並不是按照cwnd=ssthresh+3*MSS來初始化的,實際上這個linux中的這個cwnd更新流程是參考RFC6973實現的,稱呼為PRR算法,一定要記住這個PRR更新算法,后面各種Recovery狀態和CWR狀態下cwnd的更新都會用到,后面介紹FACK的時候我們會簡單介紹一下PRR的背景。
當TCP從Recovery狀態切換回到Open狀態的時候cwnd如何更新呢?下一篇文章我們會進行介紹。
三、wireshark示例
同樣在執行示例前如下設置TCP參數,這里設置ssthresh=10,initcwnd=12,這也意味着server端在建立連接后直接進入擁塞避免階段。並不是所有的TCP連接一開始都處於慢啟動階段的。1990年最初的BSD中的reno版本的tcp是沒有SACK功能的,我們先來看一下非SACK下,linux TCP的處理,因此設置tcp_sack=0。
******@Inspiron:~$ sudo ip route add local 127.0.0.2 dev lo congctl reno initcwnd 12 ssthresh lock 10 #參考本系列destination metric文章
******@Inspiron:~$ sudo ethtool -K lo tso off gso off #關閉tso gso以方便觀察cwnd變化
1、SACK關閉場景下的快速重傳和快速恢復
在介紹下面的示例前我們在介紹兩個linux內核中的狀態變量,第一個是high_seq,這個狀態變量表示重傳的recovery point的系列號,關於recovery point的信息請參考前面重傳相關的文章,high_seq會在TCP進入Disorder狀態或Recovery狀態的時候更新為SND.NXT(當然進入loss狀態也會更新)。另外一個是retransmit_high,在關閉sack選項的情況下,這個變量表示當前TCP重傳報文的時候能最大的重傳系列號,也就是TCP認為已經丟失需要進行重傳的最大系列號。
業務場景:server端建立連接后,休眠1000ms,然后應用層連續write30次,每次寫入50bytes,寫入間隔為3ms。client與server端建立連接后,立即發送一個請求報文,另外server端第4、5、11次write寫入的數據(分別對應下圖中的No9、No10和No16報文)在傳輸的過程中丟失。client端對收到的每個報文都會回復一個ACK確認包,client與server端的時延為50ms。
No1-No3:client與server端通過三次握手建立連接,client端的mss為62bytes,扣除掉12bytes的TSopt選項后,server端每次只能發送50bytes數據。為了簡潔,TSopt選項的信息設置沒有在Info列顯示。另外可以看到server端的No2數據包中並沒有SACK_PERM選項。server端初始化cwnd=12, ssthresh=10, packets_out、 sacked_out、 lost_out、 retrans_out、 cwnd_cnt等變量此時也為0,如果對這些狀態變量的含義不清楚,請翻看前面相關的文章。同樣high_seq和retransmit_high初始值也為0。另外tcp擁塞控制狀態機初始處於Open態。
No4-No5:client發送一段請求報文,server端立即回復了No5確認包。
No6-No17:server端連續發出12數據包后,受限於cwnd,不能在額外發出新的數據。發出No17后可以看到in_flight列值正好為600bytes。此時packets_out=12與之對應。
No18-No19:擁塞避免過程,reno更新cwnd_cnt=cwnd_cnt+1=1。
No20-No21、No22-No23:這兩組數據與No18、No19類似,同樣是擁塞避免過程。收到No12后 cwnd_cnt=3。
No24-No25:No22的ack number確認了No8報文,因為No9和No10這兩個數據包丟失,因此client在接收到No11數據包的時候回復No24這個dup ACK。server端在收到這個dup ACK后,擁塞狀態機從Open切換到Disorder狀態,更新high_seq=snd_nxt=751(相對系列號)。之前我們說過在打開SACK功能的時候,sacked_out表示通過SACK確認的數據包的個數,在關閉SACK的時候,sacked_out表示收到的dup ACK的個數。我們在測試這個示例前設置tcp_sack=0,因此收到No24后,server端更新sacked_out=sacked_out+1=1。此時linux計算的in_flight = packets_out - ( sacked_out + lost_out) + retrans_out =12-(1+0)+0=11(可以看到linux內部維護的in_flight此時已經和wireshrk中的in_flight列不一致了),而這個時候cwnd=12,server端仍然可以發出一個新的數據包,即對應No25,更新packets_out=packets_out+1=13。因為這個dup ACK的ack number並沒有確認新的數據包,因此reno並不會更新cwnd。
No26-No27:這組數據與No24-No25類似,這里不再重復敘述。發出No27后,packets_out=14,sacked_out=2。因為擁塞控制狀態仍然為Disorder,因此high_seq仍然為751。
No28-No29:server端收到No28報文后,發現當前dup ACK的個數已經滿足快速重傳門限,即sacked_out=3 >= dupthresh=3,server端判斷TCP傳輸已經發生了丟包,在沒有SACK的前提下,linux收到三個dup ACK只能認為丟失了一個數據包,因此更新lost_out=1,retransmit_high=201(相對系列號),並置位fast_rexmit=1表示當前需要進行快速重傳。此時server端擁塞控制狀態機進入Recovery狀態,初始進入Recovery狀態的時候,會更新high_seq=snd_nxt=851,重置cwnd_cnt=0,prr_delivered=0,prr_out=0,prior_cwnd=cwnd=12,使用reno算法的回調接口(ssthresh)初始化ssthresh=max(cwnd/2,2)=6。接着更新newly_acked_sacked=1,prr_delivered = prr_delivered +1 = 1,此時in_flight = packets_out - ( sacked_out + lost_out) + retrans_out = 14-(3+1)+0=10,delta=ssthresh-in_flight=-4<0,接着sndcnt = (ssthresh * prr_delivered + prior_cwnd - 1)/prior_cwnd - prr_out = (6*1+11)/12-0=1,sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0))=max(1,1)=1,因此最終更新cwnd = in_flight + sndcnt = 10+1=11。
最后進行快速重傳,發出No29數據包,更新retrans_out=1,prr_out=prr_out+1=1,in_flight = packets_out - ( sacked_out + lost_out) + retrans_out = 14-(3+1)+1=11,與此時的擁塞窗口cwnd相同。受限於擁塞控制linux此時不能在重傳其他的數據或者發送新數據了。
No30:server端收到No30這個dup ACK的時候,更新sacked_out=sacked_out+1=4,newly_acked_sacked=1,prr_delivered = prr_delivered +1 = 2,fast_rexmit=0,此時in_flight = packets_out - ( sacked_out + lost_out) + retrans_out = 14-(4+1)+1=10,delta=ssthresh-in_flight=-4<0,接着sndcnt = (ssthresh * prr_delivered + prior_cwnd - 1)/prior_cwnd - prr_out =(6*2+11)/12-1=0,sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0))=max(0,0)=0,因此最終更新cwnd = in_flight + sndcnt = 10+0=10。可以看到收到No30數據包后擁塞窗口cwnd減小了1。所以雖然No30這個dup ACK代表對端有亂序包到達,linux內部維護的in_flight也減小了1,但是此時in_flight已經與cwnd相同,因此不能在額外重傳或者發送新的數據。
No31-No32:server端收到No31這個dup ACK的時候,更新sacked_out=sacked_out+1=5,newly_acked_sacked=1,prr_delivered = prr_delivered +1 = 3,fast_rexmit=0,此時in_flight = packets_out - ( sacked_out + lost_out) + retrans_out = 14-(5+1)+1=9,delta=ssthresh-in_flight=-3<0,接着sndcnt = (ssthresh * prr_delivered + prior_cwnd - 1)/prior_cwnd - prr_out =(6*3+11)/12-1=1,sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0))=max(1,0)=1,因此最終更新cwnd = in_flight + sndcnt = 9+1=10。可以看到cwnd實在in_flight的基礎上加了1,因此此時允許額外在重傳一個數據包或者發出一個新的數據包。接着TCP進入重傳流程嘗試重傳No10報文,但是No10報文的Seq=201,而retransmit_high也為201,不滿足Seq<retransmit_high條件,也就是linux認為No10報文當前還沒有丟失,因此放棄重傳。接着嘗試發送新的未發送的數據包並發送成功,即對應No32數據包,更新packets_out=packets_out+1=15、prr_out=prr_out+1=2。
No33-No35:與No30-No32處理過程類似。No35后sacked_out=7、packets_out=16、prr_delivered=5、prr_out=3、cwnd=9、in_flight = packets_out - ( sacked_out + lost_out) + retrans_out = 16-(7+1)+1=9。
No36-No38:與No30-No32處理過程類似。No38后sacked_out=9、packets_out=17、prr_delivered=7、prr_out=4、cwnd=8、in_flight = packets_out - ( sacked_out + lost_out) + retrans_out = 17-(9+1)+1=8。
No39-No40:可以看到這里linux收到了兩個dup ACK后並沒有像上面一樣發出新的數據包,那是什么原因呢?我們繼續分析一下,server端收到No40后sacked_out=11、 packets_out=17、 prr_delivered=9、 prr_out=4、 newly_acked_sacked=1、 in_flight = packets_out - ( sacked_out + lost_out) + retrans_out = 17-(11+1)+1=6。此時delta=ssthresh-in_flight=0,已經不滿足之前那種delta<0的cwnd更新情況了,因為No40的ack number並沒有確認之前重傳的No29數據包,因此更新sndcnt = min(delta, newly_acked_sacked)=0,sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0))=max(0,0)=0,因此更新cwnd = in_flight + sndcnt=6+0=6。可以看到這個時候,cwnd和in_flight相同,因此受限於擁塞控制,此時不能發出新的數據包了。
No41-No43:client收到No29的重傳包的時候,回復了一個ACK,即對應No41,server端在收到No41的時候,發現Ack=201<high_seq=851,因此這個ACK報文被認定為partial ACK。在Recovery狀態下,server端收到partial ACK的時候會立即把之前發出的Seq=201的No10報文標記為丟失,並更新retransmit_high=251。此時sacked_out=11、 packets_out=16、 prr_delivered=10、 prr_out=4、 newly_acked_sacked=1、 retrans_out=0、 lost_out=1(雖然No9這個lost的報文被ack number確認了,但是linux又新認定No10報文丟失,因此lost_out仍然為1)、 in_flight = packets_out - ( sacked_out + lost_out) + retrans_out =16-(11+1)+0=4。此時delta=ssthresh-in_flight=6-4=2,此時滿足ack number新確認了之前重傳的數據且沒有RACK重傳丟失標記,因此更新 sndcnt = min(delta, max(prr_delivered - prr_out,newly_acked_sacked) + 1) = min(2,max(10-4,1)+1) =2,sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0)) = max(2,0)=2,cwnd = in_flight + sndcnt=4+2=6。因此最終允許發出兩個數據包。接着TCP進入快速重傳流程,把剛剛標記為丟失的No10報文進行重傳,即對應No42,然后更新retrans_out=retrans_out+1=1,prr_out=prr_out+1=5。 繼續嘗試重傳No11報文的時候發現No11報文Seq=251,已經達到了retransmit_high,因此退出重傳流程。接着server端嘗試發送新的TCP數據,發出一個報文No43報文后,更新packets_out=packets_out+1=17,prr_out=prr_out+1=6 。
No44-No45:server端收到No44這個dup ACK后,更新sacked_out=sacked_out+1=12, prr_delivered=prr_delivered+1=11, in_flight = packets_out - ( sacked_out + lost_out) + retrans_out = 17-(12+1)+1=5,delta=ssthresh-in_flight=6-5=1,因此更新sndcnt = min(delta, newly_acked_sacked)=min(1,1)=1, 接着sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0)) = max(1,0)=1, cwnd = in_flight + sndcnt = 6,因此此時擁塞控制允許TCP額外多發送一個數據包。接着server端tcp進入快速重傳流程,嘗試重傳No11報文的時候,發現No11的Seq=251,已經與retransmit_high相同,因此退出快速重傳流程,接着tcp嘗試發送新數據,最終發出No45數據包。然后更新packets_out=packets_out+1=18,prr_out=prr_out+1=7。
No46-No47:這兩個數據包的處理流程與No44-No45一致,因此最終sacked_out=sacked_out+1=13、 prr_delivered=prr_delivered+1=12、 packets_out=packets_out+1=19、prr_out=prr_out+1=8 、cwnd = 6。
No48:server端在收到No48這個dup ACK后,更新sacked_out=sacked_out+1=14、 prr_delivered=prr_delivered+1=13、 in_flight = packets_out - ( sacked_out + lost_out) + retrans_out = 19-(14+1)+1=5,cwnd = 6,此時擁塞窗口允許發送新的數據包,但是可以看到No48之后server端並沒有發出新的數據包。原因是server端應用層在write寫入數據的時候達到了發送端緩存的上限,因此write操作進入休眠狀態,等待收到新的ack number后,釋放之前占用的緩存,然后繼續寫入數據。所以此時雖然server端的擁塞窗口允許發送新的數據,但是目前server端內核中並沒有緩存的待發送的TCP新數據了,因此並沒有發出新的數據。對於緩存的更新相對比較復雜,不再做精確的數值分析,這里只要宏觀了解是緩存受限就行了,在下一篇文章中我們會進一步通過實例驗證這個原因。另外從下面后續報文的分析也可以看到server端的應用層在write的時候發生了休眠。
No49-No51:client收到server端的No42重傳包后,回復No49這個ACK報文,server端收到No49的時候,high_seq=851,No49的Ack=501<high_seq,因此No49依然是一個partial ACK,server端更新retrans_out = 0,No49的Ack number為501相比之前No48的201多確認了6個數據包,因此更新packets_out = packets_out - 6 =13,但是這六個數據包中起始系列號為201的數據包被標記為lost,因此更新lost_out = lost_out - 1 = 0、接着更新sacked_out = sacked_out - 5 = 9, ,這里在沒有打開SACK功能的前提下,sacked_out統計的是dup ACK的個數,表示丟失的數據包后面亂序到達的數據包的個數,是不包含丟失的這個數據包的,一定要理解這塊sacked_out更新的背后依據。接着server根據partial ACK把Seq為501(對應No16)的數據包標記為lost,更新lost_out=lost_out+1=1,retransmit_high = 551,。newly_acked_sacked表示收到No49這個數據包之前和之后(packets_out - sacked_out)差值,收到No49之前,(packets_out - sacked_out) = 19-14=5,收到No49更新packets_out和sacked_out后,(packets_out - sacked_out) = 13 - 9 =4,因此newly_acked_sacked = 5-4=1,prr_delivered=prr_delivered+newly_acked_sacked=14、 in_flight = packets_out - ( sacked_out + lost_out) + retrans_out = 13 - (9+1) + 0=3,delta = ssthresh - in_flight = 3>0,因為No49的ack number確認了新的數據包且沒有RACK重傳丟失標記,因此sndcnt = min(delta, max(prr_delivered - prr_out,newly_acked_sacked) + 1) = min(3,max(14-8)+1)=3,sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0)) = max(3,0)=3,最終更新cwnd = in_flight + sndcnt = 3+3=6。可以看到此時允許發出三個數據包,接着server端進入快速重傳流程后重傳No16數據包,即對應No51數據包,然后更新prr_out =prr_out +1 =9,retrans_out = retrans_out + 1 =1。接着server端的tcp嘗試發送新的未發送的數據包,在上面我們說了server端應用層write操作受發送緩存限制進入休眠,目前緩存中沒有新的數據包了,因此嘗試發送新的數據包失敗。但是server端接着發現No49新確認了6個數據包,這6個數據包占用的緩存被釋放后,發送緩存中已經有一定量的空閑空間接收應用層write寫入的數據了,因此server會喚醒write操作,write操作被喚醒后,寫入Seq=1151的數據包,最終發出對應No51數據包。發出No51后,更新packets_out = packets_out+1 =14,prr_out =prr_out +1 = 10。此時in_flight = packets_out - ( sacked_out + lost_out) + retrans_out = 14 - (9+1)+1 = 5,而此時cwnd=6,擁塞控制還允許繼續發送一個數據包,但是server端在No51后並沒有發出新的數據包,原因是write操作被喚醒后,寫入50bytes成功后返回到server端應用層,休眠3ms后,應用層才會繼續下一次write操作。從這里進一步驗證了之前write操作發生了休眠。
No52-No53:可以看到No53和No52兩個數據包的時間差大約為3ms,因此明顯No53並不是No52這個ACK報文觸發的。server端在收到No52的時候,更新sacked_out=sacked_out+1=10、 prr_delivered=prr_delivered+1=15、 in_flight = packets_out - ( sacked_out + lost_out) + retrans_out = 14-(10+1)+1=4,delta = ssthresh - in_flight = 2>0,No52的ack number並沒有確認新的數據,因此sndcnt = min(delta, newly_acked_sacked) = min(2,1)=1, sndcnt = max(sndcnt, (fast_rexmit ? 1 : 0)) = max(1,0) = 1, 最終cwnd = in_flight + sndcnt = 5,注意此時ssthresh=6,這里在Recovey狀態下,cwnd已經降低到ssthresh的下面了。你會在網上看到一些說在Recovery狀態下cwnd只會降低到原來的一般,然后就停止減小,可見這個論斷不是所有場景都正確的。接着server端進入快速重傳流程,但是受到retransmit_high限制而不能進行重傳因此退出快速重傳,然后server端嘗試發送新的未發送的數據包,但是此時server端內核tcp發送緩存還是空的,因此嘗試發送新的數據包失敗,最終No52處理完畢。接着server端的應用層在休眠3ms后醒來,write寫入新的數據包,並以No53發出去。然后應用層write操作返回,繼續休眠3ms。此時packets_out = 1 =15,prr_out =1 = 11,in_flight=15-(10+1)+1=5,in_flight正好與cwnd相同。
No54-No55:這組數據包與No52-No53類似,不再仔細分析,發出No55后,packets_out=16, sacked_out=11, lost_out=1, retrans_out=1, prr_delivered=16, prr_out=12, in_flight = cwnd =5。
No56-No57:這組數據包的處理與No52-No53略有差異,可以看到No56與No55之間的時間差大約為20ms,server端的應用層在write寫入No55后,只剩下4次write操作,write操作的間隔為3ms,因此最終在收到No56這個ack報文之前,server端的應用層已經把全部的1500bytes數據寫入完畢了。server端在收到No56這個dup ACK更新完相關的狀態變量后,嘗試快速重傳,但是重傳受到retransmit_high限制而退出重傳流程,接着嘗試發出新的數據包,此時緩存中有四個待發送的數據包,最終發出No57。發出No57后,packets_out=17, sacked_out=12, lost_out=1, retrans_out=1, prr_delivered=17, prr_out=13, in_flight = cwnd =5。
No58-No61:client在收到No50這個重傳包的時候,之前No16丟包形成的hole得以修復,回復No58確認包。No58的Ack=1151,相比No56的Ack=501,新確認了13個數據包,因此更新packets_out = packets_out-13=4,sacked_out = sacked_out - 12 =0(這里減12的原因與上面partial ACK相同,新確認的13個數據包里面有個被標記為lost了),lost_out=0,retrans_out=0,此時snd_una=1201, 而此時high_seq=851,snd_una>high_seq,TCP已經越過Recovery point,因此TCP擁塞控制狀態更新為Open,重置sacked_out=0(實際上sacked_out已經為0了),更新cwnd=ssthresh=6(Recovery狀態切換到Open狀態時候cwnd的更新參考下篇文章)。SACK關閉的情況下,Open狀態下,sacked_out大於等於dupthreah的時候才會進入快速重傳流程,因此這時候server端並不會去嘗試快速重傳。接着server端進入reno的擁塞避免過程,因為No56的ack number新確認了13個數據包,因此更新cwnd_cnt=13,cwnd_cnt/cwnd向下取整后為2,因此更新cwnd_cnt = cwnd_cnt - 2*6=1,更新cwnd = 6+2=8。接着tcp進入嘗試發送新數據的流程,此時顯然緩存中的三個數據包都可以發出去了,分別對應No59-No61。這三個數據包發送完后,packets_out=7,sacked_out=0, lost_out=0, retrans_out=0,cwnd = 8。
No62:server端收到No62后,更新packets_out = 6,接着進入reno的擁塞避免過程,但是此時server端的業務處於application-limited狀態,因此並不會更新cwnd_cnt。
No63-No68:這些數據包的處理與No62類似,最終packets_out=0,sacked_out=0, lost_out=0, retrans_out=0,cwnd = 8,ssthresh=6, cwnd_cnt=1。
最后給出示例的seq-time圖,如下圖所示,其中X軸為時間,Y軸為系列號,I字形的藍色短數線表示server端發出數據包的系列號,下半部的灰色線表示server端收到的Ack number,從下圖可以明顯的看到server端進行了三次快速重傳,下圖中標示1、2、3的點分別對應No29、No42、No50三個數據包。
補充說明:
1、server端應用層write寫入數據受發送端緩存sndbuf限制而休眠等待釋放內存代碼點sk_stream_memory_free、 sk_stream_wait_memory、 sk_stream_write_space
2、關閉SACK的情況,sacked_out的更新點tcp_remove_reno_sacks、tcp_add_reno_sack
3、從Recovery恢復為Open狀態時刻更新cwnd代碼點tcp_try_undo_recovery、tcp_end_cwnd_reduction,上面示例中在tcp_end_cwnd_reduction中更新
4、Recovery狀態下,cwnd更新點tcp_cwnd_reduction。
5、因為原始的BSD上的reno版本的TCP沒有sack的功能,因此在SACK關閉的情況下,linux認為這是reno版本的TCP:tcp_is_reno。但是我們這里為了避免linux下的reno擁塞控制算法混淆,本篇文章標題稱呼為SACK關閉的快速恢復。前文我們介紹過reno的這個詞的幾個不同意義,一定要注意區分。