1、坚持定时器在接收方通告接收窗口为0,阻止发送端继续发送数据时设定。
由于连接接收端的发送窗口通告不可靠(只有数据才会确认),如果一个确认丢失了,双方就有可能因为等待对方而使连接终止:
接收放等待接收数据(因为它已经向发送方通过了一个非0窗口),而发送方在等待允许它继续发送数据的窗口更新。
为了防止上面的情况,发送方在接收到0窗口通告后,启动一个坚持定时器来周期的发送1字节的数据,以便发现接收方窗口是否已经增大。
这些从发送方发出的报文段称为窗口探测;
Q1:什么时候启动persist 定时器
1、收到ack的时候-----------------
static void tcp_ack_probe(struct sock *sk)
{
const struct tcp_sock *tp = tcp_sk(sk);
struct inet_connection_sock *icsk = inet_csk(sk);
/* Was it a usable window open?
* 对端是否有足够的接收缓存,即我们能否发送一个包。
*/
if (!after(TCP_SKB_CB(tcp_send_head(sk))->end_seq, tcp_wnd_end(tp))) {
icsk->icsk_backoff = 0;
inet_csk_clear_xmit_timer(sk, ICSK_TIME_PROBE0);
/* Socket must be waked up by subsequent tcp_data_snd_check().
* This function is not for random using!
*/
} else { /* 否则根据退避指数重置零窗口探测定时器 */
unsigned long when = tcp_probe0_when(sk, TCP_RTO_MAX);
inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
when, TCP_RTO_MAX);
}
}
/* This routine deals with incoming acks, but not outgoing ones. */
/*
接收到一个ACK的时候,如果之前网络中没有发送且未确认的数据段,
本端又有待发送的数据段,说明可能遇到对端接收窗口为0的情况。
这个时候会根据此ACK是否打开了接收窗口来进行零窗口探测定时器的处理:
1. 如果此ACK打开接收窗口。此时对端的接收窗口不为0了,可以继续发送数据包。??? 那么清除超时时间的退避指数,删除零窗口探测定时器。
2. 如果此ACK是接收方对零窗口探测报文的响应,且它的接收窗口依然为0。那么根据指数退避算法,??? 重新设置零窗口探测定时器的下次超时时间,超时时间的设置和超时重传定时器的一样。
*/
//tcp_ack()用于处理接收到的带有ACK标志的段,会检查是否要删除或重置零窗口探测定时器。
static int tcp_ack(struct sock *sk, const struct sk_buff *skb, int flag)
{
s/* We very likely will need to access write queue head. *//* We passed data and got it acked, remove any soft error
* log. Something worked...
*/
///* 清零探测次数,所以如果对端有响应ACK,实际上是没有次数限制的 */
sk->sk_err_soft = 0;
icsk->icsk_probes_out = 0;
tp->rcv_tstamp = tcp_time_stamp;
if (!prior_packets) /* 如果之前网络中没有发送且未确认的数据段 */
goto no_queue;
no_queue:
/* If data was DSACKed, see if we can undo a cwnd reduction. */
if (flag & FLAG_DSACKING_ACK)
tcp_fastretrans_alert(sk, acked, is_dupack, &flag, &rexmit);
/* If this ack opens up a zero window, clear backoff. It was
* being used to time the probes, and is probably far higher than
* it needs to be for normal retransmission.
*/ /* 如果还有待发送的数据段,而之前网络中却没有发送且未确认的数据段,
* 很可能是因为对端的接收窗口为0导致的,这时候便进行零窗口探测定时器的处理。
*/ /* 如果ACK打开了接收窗口,则删除零窗口探测定时器。否则根据退避指数,给予重置 */
if (tcp_send_head(sk))
tcp_ack_probe(sk);
if (tp->tlp_high_seq)
tcp_process_tlp_ack(sk, ack, flag);
return 1;
invalid_ack:
SOCK_DEBUG(sk, "Ack %u after %u:%u
", ack, tp->snd_una, tp->snd_nxt);
return -1;
old_ack:
/* If data was SACKed, tag it and see if we should send more data.
* If data was DSACKed, see if we can undo a cwnd reduction.
*/
if (TCP_SKB_CB(skb)->sacked) {
flag |= tcp_sacktag_write_queue(sk, skb, prior_snd_una,
&sack_state);
tcp_fastretrans_alert(sk, acked, is_dupack, &flag, &rexmit);
tcp_xmit_recovery(sk, rexmit);
}
SOCK_DEBUG(sk, "Ack %u before %u:%u
", ack, tp->snd_una, tp->snd_nxt);
return 0;
}
2、TCP使用__tcp_push_pending_frames发送数据时:
/* Push out any pending frames which were held back due to
* TCP_CORK or attempt at coalescing tiny packets.
* The socket must be locked by the caller.
把sk发送队列中所有的skb全部发送出去
只发送队列上的第一个SKB采用tcp_push_one 最终都要调用tcp_write_xmit
*/
void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss,
int nonagle)
{
/* If we are closed, the bytes will have to remain here.
* In time closedown will finish, we empty the write queue and
* all will be happy.
*/
if (unlikely(sk->sk_state == TCP_CLOSE))
return;
if (tcp_write_xmit(sk, cur_mss, nonagle, 0,
sk_gfp_mask(sk, GFP_ATOMIC)))
tcp_check_probe_timer(sk);
/*
当网络中没有发送且未确认的数据包,且本端有待发送的数据包时,启动零窗口探测定时器。
为什么要有这两个限定条件呢?
如果网络中有发送且未确认的数据包,那这些包本身就可以作为探测包,对端的ACK即将到来。
如果没有待发送的数据包,那对端的接收窗口为不为0根本不需要考虑。
*/
}
对porbe的分析如下:
static inline void tcp_check_probe_timer(struct sock *sk) { if (!tcp_sk(sk)->packets_out && !inet_csk(sk)->icsk_pending) inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0, tcp_probe0_base(sk), TCP_RTO_MAX); }
/* Called with BH disabled */ void tcp_write_timer_handler(struct sock *sk) { struct inet_connection_sock *icsk = inet_csk(sk); int event; /* * TCP状态为CLOSE或未定义定时器事件,则 * 无需作处理。 */ if (sk->sk_state == TCP_CLOSE || !icsk->icsk_pending) goto out; if (time_after(icsk->icsk_timeout, jiffies)) { sk_reset_timer(sk, &icsk->icsk_retransmit_timer, icsk->icsk_timeout); goto out; } event = icsk->icsk_pending; /* * 由于重传定时器和持续定时器功能是共用了 * 一个定时器实现的,因此需根据定时器事件 * 来区分激活的是哪种定时器;如果event为 * ICSK_TIME_RETRANS,则调用tcp_retransmit_timer()进行重传 * 处理;如果为ICSK_TIME_PROBE0,则调用tcp_probe_timer() * 进行持续定时器的处理. */ switch (event) { case ICSK_TIME_EARLY_RETRANS: tcp_resume_early_retransmit(sk); break; case ICSK_TIME_LOSS_PROBE: tcp_send_loss_probe(sk); break; case ICSK_TIME_RETRANS: icsk->icsk_pending = 0; tcp_retransmit_timer(sk); break; case ICSK_TIME_PROBE0: icsk->icsk_pending = 0; tcp_probe_timer(sk); break; } out: sk_mem_reclaim(sk); } /*
/* * "持续"定时器在对端通告接收窗口为0,阻止TCP继续发送 * 数据时设定。由于连接对端发送的窗口通告不可靠(只有 * 数据才会确认,ACK不会确认),允许TCP继续发送数据的后 * 续窗口更新有可能丢失,因此,如果TCP有数据发送,而 * 对端通告接收窗口为0,则持续定时器启动,超时后向 * 对端发送1字节的数据,以判断对端接收窗口是否已打开。 * 与重传定时器类似,持续定时器的超时值也是动态计算的, * 取决于连接的往返时间,在5~60s之间取值。 * tcp_probe_timer()为持续定时器超时的处理函数。探测定时器就是当接收到对端的window为0的时候,需要探测对端窗口是否变大, */ //真正的probe报文发送在tcp_send_probe0中的tcp_write_wakeup 探测定时器在tcp_ack函数中激活, 或者在__tcp_push_pending_frames中的tcp_check_probe_timer激活 //tcp_write_timer包括数据报重传tcp_retransmit_timer和窗口探测定时器tcp_probe_timer static void tcp_probe_timer(struct sock *sk) { struct inet_connection_sock *icsk = inet_csk(sk); struct tcp_sock *tp = tcp_sk(sk); int max_probes; u32 start_ts; /* (1)如果存在发送出去未被确认的段, 要么被确认返回窗口,要么重传,无需额外构造探测包 (2)或者发送队列有待发送的段,无数据需要发, 不关心窗口情况 则无需另外组织探测数据 */ if (tp->packets_out || !tcp_send_head(sk)) { icsk->icsk_probes_out = 0; return; } /* RFC 1122 4.2.2.17 requires the sender to stay open indefinitely as * long as the receiver continues to respond probes. We support this by * default and reset icsk_probes_out with incoming ACKs. But if the * socket is orphaned or the user specifies TCP_USER_TIMEOUT, we * kill the socket when the retry count and the time exceeds the * corresponding system limit. We also implement similar policy when * we use RTO to probe window in tcp_retransmit_timer(). */ start_ts = tcp_skb_timestamp(tcp_send_head(sk)); if (!start_ts) skb_mstamp_get(&tcp_send_head(sk)->skb_mstamp); else if (icsk->icsk_user_timeout &&/* 有时间戳则判断是否超过了用户设置时间 */ (s32)(tcp_time_stamp - start_ts) > icsk->icsk_user_timeout) goto abort; /* 最大探测次数设置为连接状态的重试次数 */ max_probes = sock_net(sk)->ipv4.sysctl_tcp_retries2; /* * TCP协议规定RTT的最大值为120s(TCP_RTO_MAX),因此 * 可以通过将指数退避算法得出的超时时间与 * RTT最大值相比,来判断是否需要给对方发送 * RST。 *////这里的处理和上面的tcp_write_timeout很类似。 if (sock_flag(sk, SOCK_DEAD)) { const bool alive = inet_csk_rto_backoff(icsk, TCP_RTO_MAX) < TCP_RTO_MAX; /* * 如果连接已断开,套接字即将关闭,则获取在 * 关闭本端TCP连接前重试次数的上限。 */ /* 获取在本端关闭tcp前重试次数上限 */ max_probes = tcp_orphan_retries(sk, alive); if (!alive && icsk->icsk_backoff >= max_probes) goto abort; /* * 释放资源,如果该套接字在释放过程中被关闭, * 就无需再发送持续探测段了。 */ if (tcp_out_of_resources(sk, true)) return; } /* 探测次数超过了最大探测次数,错误处理,关闭连接 */ if (icsk->icsk_probes_out > max_probes) { abort: tcp_write_err(sk); } else { /* Only send another probe if we didn't close things up. */ tcp_send_probe0(sk); } }
/* Initiate keepalive or window probe from timer. */ /* * tcp_write_wakeup()用来输出持续探测段。如果传输 * 控制块处于关闭状态,则直接返回失败,否 * 则传输持续探测段,过程如下: * 1)如果发送队列不为空,则利用那些待发送 * 段来发送探测段,当然这些待发送的段至 * 少有一部分在对方的接收窗口内。 * 2)如果发送队列为空,则构造需要已确认, * 长度为零的段发送给对端。也就是否则最终会发送序号为snd_una-1,长度为0的ack包 * 其返回值如下: * 0: 表示发送持续探测段成功 * 小于0: 表示发送持续探测段失败 * 大于0: 表示由于本地拥塞而导致发送持续探测段失败。 */ int tcp_write_wakeup(struct sock *sk, int mib) { struct tcp_sock *tp = tcp_sk(sk); struct sk_buff *skb; if (sk->sk_state == TCP_CLOSE) return -1; skb = tcp_send_head(sk); if (skb && before(TCP_SKB_CB(skb)->seq, tcp_wnd_end(tp))) { int err; /* * 如果发送队列中有段需要发送,并且最先 * 待发送的段至少有一部分在对端接收窗口 * 内,那么可以直接利用该待发送的段来发 * 送持续探测段。 */ unsigned int mss = tcp_current_mss(sk); /* * 获取当前的MSS以及待分段的段长。分段得到 * 的新段必须在对方接收窗口内,待分段的段 * 长初始化为SND.UNA-SND_WND-SKB.seq. */ unsigned int seg_size = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq; /* * 如果该段的序号已经大于pushed_seq,则需要 * 更新pushed_seq。 */ if (before(tp->pushed_seq, TCP_SKB_CB(skb)->end_seq)) tp->pushed_seq = TCP_SKB_CB(skb)->end_seq; /* We are probing the opening of a window * but the window size is != 0 * must have been a result SWS avoidance ( sender ) */ /* * 如果待分段段长大于剩余等待发送数据,或者段长度 * 大于当前MSS,则对该段进行分段,分段段长取待分段 * 段长与当前MSS两者中的最小值,以保证只发送出一个 * 段到对方。 */ if (seg_size < TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq || skb->len > mss) { seg_size = min(seg_size, mss); TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH; if (tcp_fragment(sk, skb, seg_size, mss, GFP_ATOMIC)) return -1; } else if (!tcp_skb_pcount(skb)) tcp_set_skb_tso_segs(skb, mss); /* * 将探测段发送出去,如果发送成功, * 则更新发送队首等标志。 */ TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH; err = tcp_transmit_skb(sk, skb, 1, GFP_ATOMIC); if (!err) tcp_event_new_data_sent(sk, skb); return err; } else { /* * 如果发送队列为空,则构造并发送一个需要已确认、 * 长度为零的段给对端。如果处于紧急模式,则多发送 * 一个序号为SND.UNA的段给对端。 */ if (between(tp->snd_up, tp->snd_una + 1, tp->snd_una + 0xFFFF)) tcp_xmit_probe_skb(sk, 1, mib); return tcp_xmit_probe_skb(sk, 0, mib); } }
/* A window probe timeout has occurred. If window is not closed send * a partial packet else a zero probe. */ /* * 当持续定时器超时之后,会调用tcp_send_probe0() * 进行探测。 */ void tcp_send_probe0(struct sock *sk) { struct inet_connection_sock *icsk = inet_csk(sk); struct tcp_sock *tp = tcp_sk(sk); struct net *net = sock_net(sk); unsigned long probe_max; int err; /* * 输出持续探测段。 */ /* 发送一个序号为snd_una - 1,长度为0的ACK包作为零窗口探测报文 */ err = tcp_write_wakeup(sk, LINUX_MIB_TCPWINPROBE); /* * 如果有已发送但未确认的段,或者发送队列为空, * 这两种情况都无需再发送持续探测段了,因此需要 * 将icsk_probes_out和icsk_backoff清零,然后返回。 */ if (tp->packets_out || !tcp_send_head(sk)) { /* Cancel probe timer, if it is not required. */ icsk->icsk_probes_out = 0; icsk->icsk_backoff = 0; return; } if (err <= 0) { /* * 如果重传成功或并非由于本地拥塞而发送失败, * 则更新icsk_backoff和icsk_probes_out,然后复位持续定时器。 */ if (icsk->icsk_backoff < net->ipv4.sysctl_tcp_retries2) icsk->icsk_backoff++; icsk->icsk_probes_out++; probe_max = TCP_RTO_MAX; } else { /* If packet was not sent due to local congestion, * do not backoff and do not remember icsk_probes_out. * Let local senders to fight for local resources. * * Use accumulated backoff yet. */ /* * 如果由于本地拥塞而导致发送失败,则不需要累计 * icsk_probes_out,同时复位持续定时器,缩短超时时间, * 尽可能争取资源。 */ if (!icsk->icsk_probes_out) icsk->icsk_probes_out = 1; probe_max = TCP_RESOURCE_PROBE_INTERVAL; } inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0, tcp_probe0_when(sk, probe_max), TCP_RTO_MAX); }