由于当前网络不支持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;
}