虛假的重傳超時將導致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