由於網絡路徑的變化或者延時的突然增加等,引發亂序並觸發快速恢復或者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),調整了擁塞窗口,否則沒有必要進行恢復窗口操作。並且需要滿足以下條件中的一個:
- undo_retrans等於0. 報文重傳之後被D-SACK確認,表明這些重傳爲不必要的,原始報文未丟失。
- retrans_stamp等於0(重傳報文時間戳retrans_stamp等於零). 在進入擁塞狀態後還沒有進行過任何重傳,或者重傳報文都已送達。
- 接收到的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