TCP拥塞窗口调整撤销剖析
由于当前网络不支持ECN,因此在追踪丢失包时需要推测。重新排序(reordering)对于发送方来说
通常是一个问题,因为它不能分清缺失的ACK是由于丢失还是被延迟了,所以TCP可能会做出错误的
判断,不必要的调整了拥塞窗口。这时就需要一种对错误的拥塞调整做出修正的机制——拥塞窗口
调整撤销。
检测能否撤销
在进行拥塞窗口调整撤销之前,必须先用tcp_may_undo()检测能否撤销。
static inline int tcp_may_undo(struct tcp_sock *tp) { return tp->undo_marker && (!tp->undo_retrans || tcp_packet_delayed(tp)); } /* Nothing was retransmitted or returned timestamp is less than timestamp of the * first retransmission. */ static inline int tcp_packet_delayed(struct tcp_sock *tp) { return !tp->retrans_stamp || (tp->rx_opt.saw_tstamp && tp->rx_opt.rcv_tsecr && before(tp->rx_opt.rcv_tsecr, tp->retrans_stamp)); }
怎么来探测是否不必要重传了数据包呢?
(1)D-SACK
在最近一次恢复期间重传的段都被D-SACK确认。这就说明了调整是不必要的。
最近一次恢复期间重传的数据包个数记为undo_retrans,如果收到一个D-SACK,则
undo_retrans--,直到undo_retrans为0,说明全部的重传都是没必要的,则需要撤销
窗口调整。
(2)Timestamp
使用该选项时,通过比较收到ACK的时间戳和重发数据包的时间戳,可以判断窗口调整
是否没必要。
tcp_may_undo()中的!tp->undo_retrans和tcp_packet_delayed(tp)分别对应以上两种方法。
tcp_packet_delayed()中其实也包含两种方法:Timestamp和F-RTO。!tp->retrans_stamp表
示已经使用F-RTO进行处理。
只有检查出至少有一种成立时,才能进行拥塞窗口调整撤销。
撤销拥塞调整
/* 用来撤销“缩小拥塞窗口”,undo表示需要撤销慢启动阈值*/ static void tcp_undo_cwr(struct sock *sk, const int undo) { struct tcp_sock *tp = tcp_sk(sk); if (tp->prior_ssthresh) { const struct inet_connection_sock *icsk = inet_csk(sk); if (icsk->icsk_ca_ops->undo_cwnd) tp->snd_cwnd = icsk->icsk_ca_ops->undo_cwnd(sk); else tp->snd_cwnd = max(tp->snd_cwnd, tp->snd_ssthresh<<1); if (undo && tp->prior_ssthresh > tp->snd_ssthresh) { tp->snd_ssthresh = tp->prior_ssthresh; TCP_ECN_withdraw_cwr(tp); } } else { /*没保存旧的阈值*/ tp->snd_cwnd = max(tp->snd_cwnd, tp->snd_ssthresh); } tcp_moderate_cwnd(tp); tp->snd_cwnd_stamp = tcp_time_stamp; } /* CWND moderation, preventing bursts due to too big ACKs in *dubious situations */ static inline void tcp_moderate_cwnd(struct tcp_sock *tp) { tp->snd_cwnd = min(tp->snd_cwnd, tcp_packets_in_flight(tp) + tcp_max_burst(tp)); tp->snd_cwnd_stamp = tcp_time_stamp; } static __inline__ __u32 tcp_max_burst(const struct tcp_sock *tp) { return tp->reordering; }
如果当前的拥塞控制算法实现了undo_cwnd接口,则调用它来重设拥塞窗口的大小。
否则取当前拥塞窗口和2倍阈值之间的较大者为拥塞窗口的大小。
使用D-SACK撤销
D-SACK可以通知发送方,新到的段是已经接收过的。如果所有在最近的一次恢复期间重传的数据段
都被D-SACK确认了,发送方就知道恢复期被不必要的触发了。
struct sock { ... u32 retrans_stamp; /* Timestamp of the last retransmit.*/ u32 undo_marker; /* tracking retrans started here.*/ int undo_retrans; /* number of undoable retransmissions.*/ u32 total_retrans; /* Total retransmits for entire connection */ ... }
发送方在探测到一个D-SACK块时,可使undo_retrans减一。如果D-SACK块最终确认了在最近窗口
中的每个不必要的重传,重传计数器因为D-SACK降为0,发送方增大拥塞窗口,恢复最新一次对
ssthresh的修改。
/* Try to undo cwnd reduction, because D-SACKs acked all retransmitted data */ static void tcp_try_undo_dsack(struct sock *sk) { struct tcp_sock *tp = tcp_sk(sk); if (tp->undo_marker && !tp->undo_retrans) { DBGUNDO(sk, "D-SACK"); tcp_undo_cwr(sk, 1); /*进行撤销操作*/ tp->undo_marker = 0; NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPDSACKUNDO); } }
使用时间戳
TCP发送方可以使用附在每个TCP首部的时间戳选项来探测不必要的重传。当使用该选项时,TCP接收
方回显触发确认,返回发送方数据段的时间戳,允许发送方确定ACK是被原始的还是重传的触发。
Eifel算法使用类似方法来探测假重传。
当使用时间戳探测到一个不必要的重传时,如果发送方处于Loss状态,即在一个不必要被触发的RTO
之后正在重传,移除记分牌中所有段的Loss标志,从而使发送方继续发送新的数据而不再重传。此外,
调用tcp_undo_cwr来撤销拥塞窗口和阈值的调整。
从Loss状态撤销
static int tcp_try_undo_loss(struct sock *sk) { struct tcp_sock *tp = tcp_sk(sk); if (tcp_may_undo(tp)) { /*检测能否撤销调整*/ struct sk_buff *skb; tcp_for_write_queue(skb, sk) { /*遍历发送队列,直到snd.nxt*/ if (skb == tcp_send_head(sk)) break; TCP_SKB_CB(skb)->sacked &= ~TCPCB_LOST; /*清除Loss标志*/ } tcp_clear_all_retrans_hints(tp); DBGUNDO(sk, "partial loss"); tp->lost_out = 0; tcp_undo_cwr(sk, 1); NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPLOSSUNDO); inet_csk(sk)->icsk_retransmits = 0; tp->undo_marker = 0; if (tcp_is_sack(tp)) /*为什么Reno不行,RFC2582*/ tcp_set_ca_state(sk, TCP_CA_Open); /*返回Open态*/ return 1; /*调整成功*/ } return 0; /*调整失败*/ }
从Recovery/Loss状态撤销
static int tcp_try_undo_recovery(struct sock *sk) { struct tcp_sock *tp = tcp_sk(sk); if (tcp_may_undo(tp)) { /*可以进行拥塞撤销*/ int mib_idx; DBG(sk, inet_csk(sk)->icsk_ca_state == TCP_CA_Loss ? "loss" : "retrans"); tcp_undo_cwr(sk, true); /* 具体撤销内容*/ if (inet_csk(sk)->icsk_ca_state == TCP_CA_Loss) mib_idx = LINUX_MIB_TCPLOSSUNDO; else mib_idx = LINUX_MIB_TCPFULLUNDO; NET_INC_STATS_BH(sock_net(sk), mib_idx); tp->undo_marker = 0; /*复位撤销标志*/ } /* Hold old state until above high_seq is ACKed. For Reno it is * MUST to prevent false fast retransmits (RFC2582). * SACK TCP is safe. * 防止虚假的快速重传? */ if (tp->snd_una == tp->high_seq && tcp_is_reno(tp)) { tcp_moderate_cwnd(tp); return 1; } tcp_set_ca_state(sk, TCP_CA_Open); return 0; }
从Recovery状态撤销
/* We can clear retrans_stamp when there are no retransmissions in the window. * It would seem that it is trivially available for us in tp->retrans_out, however, * that kind of assumptions doesn't consider what will happen if errors occur when * sending retransmission for the second time...It could be that such segment has * only TCPCB_EVER_RETRANS set at the present time. It seems that checking the head * skb is enough except for some reneging corner cases that are not worth the effort. * * Main reason for all this complexity is the fact that connection dying time now * dpends on the validity of the retrans_stamp, in particular, that successive * retransmissions of a segment must not advance retrans_stamp under any conditions. */ static int tcp_any_retrans_done(const struct sock *sk) { const struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *skb; if (tp->retrans_out) return 1; skb = tcp_write_queue_head(sk); /* 发送队列中第一个数据包*/ if (unlikely(skb && TCP_SKB_CB(skb)->sacked & TCPCB_EVER_RETRANS) return 1; return 0; }
在Recovery状态,收到部分确认,则调用此函数撤销拥塞调整。
static int tcp_try_undo_partial(struct sock *sk, int acked) { struct tcp_sock *tp= tcp_sk(sk); /* Partial ACK arrived. Force hoe's retransmit. */ /* 如果是使用reno,收到partial ACK则必须马上重传。 * 如果此时非reorder,则也要重传。 */ int failed = tcp_is_reno(tp) || (tcp_fackets_out(tp) > tp->reordering); /* 需要进行拥塞调整撤销时*/ if (tcp_may_undo(tp)) { /* Plain luck! Hole if filled with delayed packet, * rather than with a retransmit. */ if (!tcp_any_retrans_done(sk)) tp->retrans_stamp = 0; tcp_update_reordering(sk, tcp_fackets_out(tp) + acked, 1); DBGUNDO(sk, "Hoe"); tcp_undo_cwr(sk, false); NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPPARTIALUNDO); /* So... Do not make Hoe's retransmit yet. If the first packet was delayed, * the rest ones are most probably delayed as well. */ failed = 0; /*表示不用重传了,可以发送新的数据了。*/ } return failed; }