虚假的重传超时将导致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