出现以下情况时,TCP接收方的接收缓冲区将被塞满数据:
发送方的发送速度大于接收方的接收速度。
接收方的应用程序未能及时从接收缓冲区中读取数据。
当接收方的接收缓冲区满了以后,会把响应报文中的通告窗口字段置为0,从而阻止发送方的继续发送,
这就是TCP的流控制。当接收方的应用程序读取了接收缓冲区中的数据以后,接收方会发送一个ACK,通过
通告窗口字段告诉发送方自己又可以接收数据了,发送方收到这个ACK之后,就知道自己可以继续发送数据了。
Q:那么问题来了,当接收方的接收窗口重新打开之后,如果它发送的ACK丢失了,发送方还能得知这一消息吗?
A:答案是不能。正常的ACK报文不需要确认,因而也不会被重传,如果这个ACK丢失了,发送方将无法得知对端
的接收窗口已经打开了,也就不会继续发送数据。这样一来,会造成传输死锁,接收方等待对端发送数据包,而发送
方等待对端的ACK,直到连接超时关闭。
为了避免上述情况的发生,发送方实现了一个零窗口探测定时器,也叫做持续定时器:
当接收方的接收窗口为0时,每隔一段时间,发送方会主动发送探测包,通过迫使对端响应来得知其接收窗口有无打开。
这就是山不过来,我就过去:)
激活
(1) 发送数据包时
在发送数据包时,如果发送失败,会检查是否需要启动零窗口探测定时器。
tcp_rcv_established
|--> tcp_data_snd_check
|--> tcp_push_pending_frames
- static inline void tcp_push_pending_frames(struct sock *sk)
- {
- if (tcp_send_head(sk)) {
- struct tcp_sock *tp = tcp_sk(sk);
- __tcp_push_pending_frames(sk, tcp_current_mss(sk), tp->nonagle);
- }
- }
-
-
-
-
-
- void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss, int nonagle)
- {
-
-
-
-
- if (unlikely(sk->sk_state == TCP_CLOSE))
- return;
-
-
- if (tcp_write_xmit(sk, cur_mss, nonagle, 0, sk_gfp_atomic(sk, GFP_ATOMIC)))
- tcp_check_probe_timer(sk);
- }
当网络中没有发送且未确认的数据包,且本端有待发送的数据包时,启动零窗口探测定时器。
为什么要有这两个限定条件呢?
如果网络中有发送且未确认的数据包,那这些包本身就可以作为探测包,对端的ACK即将到来。
如果没有待发送的数据包,那对端的接收窗口为不为0根本不需要考虑。
- static inline void tcp_check_probe_timer(struct sock *sk)
- {
- struct tcp_sock *tp = tcp_sk(sk);
- const struct inet_connection_sock *icsk = inet_csk(sk);
-
-
-
-
- if (! tp->packets_out && ! icsk->icsk_pending)
- inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
- icsk->icsk_rto, TCP_RTO_MAX);
- }
(2) 接收到ACK时
tcp_ack()用于处理接收到的带有ACK标志的段,会检查是否要删除或重置零窗口探测定时器。
- static int tcp_ack (struct sock *sk, const struct sk_buff *skb, int flag)
- {
- ...
- icsk->icsk_probes_out = 0;
- tp->rcv_tstamp = tcp_time_stamp;
-
- if (! prior_packets)
- goto no_queue;
- ...
- no_queue:
-
- if (flag & FLAG_DSACKING_ACK)
- tcp_fastretrans_alert(sk,acked, prior_unsacked, is_dupack, flag);
-
-
-
-
-
-
-
-
- if (tcp_send_head(sk))
-
- tcp_ack_probe(sk);
- }
接收到一个ACK的时候,如果之前网络中没有发送且未确认的数据段,本端又有待发送的数据段,
说明可能遇到对端接收窗口为0的情况。
这个时候会根据此ACK是否打开了接收窗口来进行零窗口探测定时器的处理:
1. 如果此ACK打开接收窗口。此时对端的接收窗口不为0了,可以继续发送数据包。
那么清除超时时间的退避指数,删除零窗口探测定时器。
2. 如果此ACK是接收方对零窗口探测报文的响应,且它的接收窗口依然为0。那么根据指数退避算法,
重新设置零窗口探测定时器的下次超时时间,超时时间的设置和超时重传定时器的一样。
- #define ICSK_TIME_PROBE0 3
-
- static void tcp_ack_probe(struct sock *sk)
- {
- const struct tcp_sock *tp = tcp_sk(sk);
- struct inet_connection_sock *icsk = inet_csk(sk);
-
-
-
-
- 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);
-
-
-
-
-
- } else {
- inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
- min(icsk->icsk_rto << icsk->icsk_backoff, TCP_RTO_MAX), TCP_RTO_MAX);
- }
- }
-
-
-
- static inline u32 tcp_wnd_end(const struct tcp_sock *tp)
- {
- return tp->snd_una + tp->snd_wnd;
- }
超时处理函数
icsk->icsk_retransmit_timer可同时作为:超时重传定时器、ER延迟定时器、PTO定时器,
还有零窗口探测定时器,它们的超时处理函数都为tcp_write_timer_handler(),在函数内则
根据超时事件icsk->icsk_pending来做区分。
具体来说,当网络中没有发送且未确认的数据段时,icsk->icsk_retransmit_timer才会用作零窗口探测定时器。
而其它三个定时器的使用场景则相反,只在网络中有发送且未确认的数据段时使用。
和超时重传定时器一样,零窗口探测定时器也使用icsk->icsk_rto和退避指数来计算超时时间。
- void tcp_write_timer_handler(struct sock *sk)
- {
- struct inet_connection_sock *icsk = inet_csk(sk);
- int event;
-
-
- 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;
- 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);
- }
可见零窗口探测定时器的真正处理函数为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;
-
-
-
-
-
- if (tp->packets_out || ! tcp_send_head(sk)) {
- icsk->icsk_probes_out = 0;
- return;
- }
-
-
-
-
-
-
-
-
- max_probes = sysctl_tcp_retries2;
-
- if (sock_flag(sk, SOCK_DEAD)) {
- const int alive = ((icsk->icsk_rto << icsk->icsk_backoff) < TCP_RTO_MAX);
- max_probes = tcp_orphan_retries(sk, alive);
-
-
- if (tcp_out_of_resource(sk, alive || icsk->icsk_probes_out <= max_probes))
- return;
- }
-
-
- if (icsk->icsk_probes_out > max_probes) {
- tcp_write_err(sk);
-
- } else {
-
- tcp_send_probe0(sk);
- }
- }
发送0 window探测报文和发送Keepalive探测报文用的是用一个函数tcp_write_wakeup():
1. 有新的数据段可供发送,且对端接收窗口还没被塞满。发送新的数据段,来作为探测包。
2. 没有新的数据段可供发送,或者对端的接收窗口满了。发送序号为snd_una - 1、长度为0的ACK包作为探测包。
和保活探测定时器不同,零窗口探测定时器总是使用第二种方法,因为此时对端的接收窗口为0。
所以会发送一个序号为snd_una - 1、长度为0的ACK包,对端收到此包后会发送一个ACK响应。
如此一来本端就能够知道对端的接收窗口是否打开了。
-
-
-
-
- void tcp_send_probe0(struct sock *sk)
- {
- struct inet_connection_sock *icsk = inet_csk(sk);
- struct tcp_sock *tp = tcp_sk(sk);
- int err;
-
-
- err = tcp_write_wakeup(sk);
-
-
-
-
-
- if (tp->packets_out || ! tcp_send_head(sk)) {
-
- icsk->icsk_probes_out = 0;
- icsk->icsk_backoff = 0;
- return;
- }
-
-
- if (err < = 0) {
- if (icsk->icsk_backoff < sysctl_tcp_retries2)
- icsk->icsk_backoff++;
-
- icsk->icsk_probes_out++;
- inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0, min(icsk->icsk_rto << icsk->icsk_backoff,
- TCP_RTO_MAX), TCP_RTO_MAX);
-
- } else {
-
-
-
-
-
- if (! icsk->icsk_probes_out)
- icsk->icsk_probes_out = 1;
-
-
- inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,
- min(icsk->icsk_rto << icsk->icsk->icsk_backoff, TCP_RESOURCE_PROBE_INTERVAL),
- TCP_RTO_MAX);
- }
- }