TCP的定时器系列 — 零窗口探测定时器

出现以下情况时,TCP接收方的接收缓冲区将被塞满数据:

发送方的发送速度大于接收方的接收速度。

接收方的应用程序未能及时从接收缓冲区中读取数据。

 

当接收方的接收缓冲区满了以后,会把响应报文中的通告窗口字段置为0,从而阻止发送方的继续发送,

这就是TCP的流控制。当接收方的应用程序读取了接收缓冲区中的数据以后,接收方会发送一个ACK,通过

通告窗口字段告诉发送方自己又可以接收数据了,发送方收到这个ACK之后,就知道自己可以继续发送数据了。

 

Q:那么问题来了,当接收方的接收窗口重新打开之后,如果它发送的ACK丢失了,发送方还能得知这一消息吗?

A:答案是不能。正常的ACK报文不需要确认,因而也不会被重传,如果这个ACK丢失了,发送方将无法得知对端

的接收窗口已经打开了,也就不会继续发送数据。这样一来,会造成传输死锁,接收方等待对端发送数据包,而发送

方等待对端的ACK,直到连接超时关闭。

 

为了避免上述情况的发生,发送方实现了一个零窗口探测定时器,也叫做持续定时器:

当接收方的接收窗口为0时,每隔一段时间,发送方会主动发送探测包,通过迫使对端响应来得知其接收窗口有无打开。

这就是山不过来,我就过去:)

 

激活

 

(1) 发送数据包时

在发送数据包时,如果发送失败,会检查是否需要启动零窗口探测定时器。

tcp_rcv_established

    |--> tcp_data_snd_check

               |--> tcp_push_pending_frames

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. static inline void tcp_push_pending_frames(struct sock *sk)  
  2. {  
  3.     if (tcp_send_head(sk)) { /* 发送队列不为空 */  
  4.         struct tcp_sock *tp = tcp_sk(sk);  
  5.         __tcp_push_pending_frames(sk, tcp_current_mss(sk), tp->nonagle);  
  6.     }  
  7. }  
  8.   
  9. /* Push out any pending frames which were held back due to TCP_CORK 
  10.  * or attempt at coalescing tiny packets. 
  11.  * The socket must be locked by the caller. 
  12.  */  
  13. void __tcp_push_pending_frames(struct sock *sk, unsigned int cur_mss, int nonagle)  
  14. {  
  15.     /* If we are closed, the bytes will have to remain here. 
  16.      * In time closedown will finish, we empty the write queue and 
  17.      * all will be happy. 
  18.      */  
  19.     if (unlikely(sk->sk_state == TCP_CLOSE))  
  20.         return;  
  21.   
  22.     /* 如果发送失败 */  
  23.     if (tcp_write_xmit(sk, cur_mss, nonagle, 0, sk_gfp_atomic(sk, GFP_ATOMIC)))  
  24.         tcp_check_probe_timer(sk); /* 检查是否需要启用0窗口探测定时器*/  
  25. }  

 

当网络中没有发送且未确认的数据包,且本端有待发送的数据包时,启动零窗口探测定时器。

为什么要有这两个限定条件呢?

如果网络中有发送且未确认的数据包,那这些包本身就可以作为探测包,对端的ACK即将到来。

如果没有待发送的数据包,那对端的接收窗口为不为0根本不需要考虑。

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. static inline void tcp_check_probe_timer(struct sock *sk)  
  2. {  
  3.     struct tcp_sock *tp = tcp_sk(sk);  
  4.     const struct inet_connection_sock *icsk = inet_csk(sk);  
  5.   
  6.     /* 如果网络中没有发送且未确认的数据段,并且零窗口探测定时器尚未启动, 
  7.      *  则启用0窗口探测定时器。 
  8.      */  
  9.     if (! tp->packets_out && ! icsk->icsk_pending)  
  10.         inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,  
  11.                  icsk->icsk_rto, TCP_RTO_MAX);  
  12. }  

 

(2) 接收到ACK时

tcp_ack()用于处理接收到的带有ACK标志的段,会检查是否要删除或重置零窗口探测定时器。

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. static int tcp_ack (struct sock *sk, const struct sk_buff *skb, int flag)  
  2. {  
  3.     ...  
  4.     icsk->icsk_probes_out = 0/* 清零探测次数,所以如果对端有响应ACK,实际上是没有次数限制的 */  
  5.     tp->rcv_tstamp = tcp_time_stamp; /* 记录最近接收到ACK的时间点,用于保活定时器 */  
  6.     /* 如果之前网络中没有发送且未确认的数据段 */  
  7.     if (! prior_packets)   
  8.         goto no_queue;  
  9.     ...  
  10. no_queue:  
  11.     /* If data was DSACKed, see if we can undo a cwnd reduction. */  
  12.     if (flag & FLAG_DSACKING_ACK)  
  13.         tcp_fastretrans_alert(sk,acked, prior_unsacked, is_dupack, flag);  
  14.   
  15.     /* If this ack opens up a zero window, clear backoff. 
  16.      * It was being used to time the probes, and is probably far higher than 
  17.      * it needs to be for normal retransmission. 
  18.      */  
  19.     /* 如果还有待发送的数据段,而之前网络中却没有发送且未确认的数据段, 
  20.      * 很可能是因为对端的接收窗口为0导致的,这时候便进行零窗口探测定时器的处理。 
  21.      */  
  22.     if (tcp_send_head(sk))   
  23.         /* 如果ACK打开了接收窗口,则删除零窗口探测定时器。否则根据退避指数,给予重置 */  
  24.         tcp_ack_probe(sk);  
  25. }  

 

接收到一个ACK的时候,如果之前网络中没有发送且未确认的数据段,本端又有待发送的数据段,

说明可能遇到对端接收窗口为0的情况。

这个时候会根据此ACK是否打开了接收窗口来进行零窗口探测定时器的处理:

1. 如果此ACK打开接收窗口。此时对端的接收窗口不为0了,可以继续发送数据包。

    那么清除超时时间的退避指数,删除零窗口探测定时器。

2. 如果此ACK是接收方对零窗口探测报文的响应,且它的接收窗口依然为0。那么根据指数退避算法

    重新设置零窗口探测定时器的下次超时时间,超时时间的设置和超时重传定时器的一样。

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. #define ICSK_TIME_PROBE0 3 /* Zero window probe timer */  
  2.   
  3. static void tcp_ack_probe(struct sock *sk)  
  4. {  
  5.     const struct tcp_sock *tp = tcp_sk(sk);  
  6.     struct inet_connection_sock *icsk = inet_csk(sk);  
  7.   
  8.     /* Was it a usable window open ? 
  9.      * 对端是否有足够的接收缓存,即我们能否发送一个包。 
  10.      */  
  11.     if (! after(TCP_SKB_CB(tcp_send_head(sk))->end_seq, tcp_wnd_end(tp))) {  
  12.         icsk->icsk_backoff = 0/* 清除退避指数 */  
  13.         inet_csk_clear_xmit_timer(sk, ICSK_TIME_PROBE0); /* 清除零窗口探测定时器*/  
  14.   
  15.         /* Socket must be waked up by subsequent tcp_data_snd_check(). 
  16.          * This function is not for random using! 
  17.          */  
  18.   
  19.     } else { /* 否则根据退避指数重置零窗口探测定时器 */  
  20.         inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,  
  21.                   min(icsk->icsk_rto << icsk->icsk_backoff, TCP_RTO_MAX), TCP_RTO_MAX);  
  22.     }  
  23. }  
  24.   
  25. /* 返回发送窗口的最后一个字节序号 */  
  26. /* Returns end sequence number of the receiver's advertised window */  
  27. static inline u32 tcp_wnd_end(const struct tcp_sock *tp)  
  28. {  
  29.     return tp->snd_una + tp->snd_wnd;  
  30. }  

 

超时处理函数

 

icsk->icsk_retransmit_timer可同时作为:超时重传定时器、ER延迟定时器、PTO定时器,

还有零窗口探测定时器,它们的超时处理函数都为tcp_write_timer_handler(),在函数内则

根据超时事件icsk->icsk_pending来做区分。

 

具体来说,当网络中没有发送且未确认的数据段时,icsk->icsk_retransmit_timer才会用作零窗口探测定时器。

而其它三个定时器的使用场景则相反,只在网络中有发送且未确认的数据段时使用。  

和超时重传定时器一样,零窗口探测定时器也使用icsk->icsk_rto和退避指数来计算超时时间。

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. void tcp_write_timer_handler(struct sock *sk)  
  2. {  
  3.     struct inet_connection_sock *icsk = inet_csk(sk);  
  4.     int event;  
  5.   
  6.     /* 如果连接处于CLOSED状态,或者没有定时器在计时 */  
  7.     if (sk->sk_state == TCP_CLOSE || !icsk->icsk_pending)  
  8.         goto out;  
  9.   
  10.     /* 如果定时器还没有超时,那么继续计时 */  
  11.     if (time_after(icsk->icsk_timeout, jiffies)) {  
  12.         sk_reset_timer(sk, &icsk->icsk_retransmit_timer, icsk->icsk_timeout);  
  13.         goto out;  
  14.     }  
  15.   
  16.     event = icsk->icsk_pending; /* 用于表明是哪种定时器 */  
  17.     switch(event) {  
  18.         case ICSK_TIME_EARLY_RETRANS: /* ER延迟定时器触发的 */  
  19.             tcp_resume_early_retransmit(sk); /* 进行early retransmit */  
  20.             break;  
  21.   
  22.         case ICSK_TIME_LOSS_PROBE: /* PTO定时器触发的 */  
  23.             tcp_send_loss_probe(sk); /* 发送TLP探测包 */  
  24.             break;  
  25.   
  26.         case ICSK_TIME_RETRANS: /* 超时重传定时器触发的 */  
  27.             icsk->icsk_pending = 0;  
  28.             tcp_retransmit_timer(sk);  
  29.             break;  
  30.   
  31.         case ICSK_TIME_PROBE0: /* 零窗口探测定时器触发的 */  
  32.             icsk->icsk_pending = 0;  
  33.             tcp_probe_timer(sk);  
  34.             break;  
  35.     }  
  36.   
  37. out:  
  38.     sk_mem_reclaim(sk);  
  39. }  

可见零窗口探测定时器的真正处理函数为tcp_probe_timer()。

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. static void tcp_probe_timer(struct sock *sk)  
  2. {  
  3.     struct inet_connection_sock *icsk = inet_csk(sk);  
  4.     struct tcp_sock *tp = tcp_sk(sk);  
  5.     int max_probes;  
  6.   
  7.     /* 如果网络中有发送且未确认的数据包,或者没有待发送的数据包。 
  8.      * 这个时候不需要使用零窗口探测定时器。前一种情况时已经有现成的探测包了, 
  9.      * 后一种情况中根本就不需要发送数据了。 
  10.      */  
  11.     if (tp->packets_out || ! tcp_send_head(sk)) {  
  12.         icsk->icsk_probes_out = 0/* 清零探测包的发送次数 */  
  13.         return;  
  14.     }  
  15.   
  16.     /* icsk_probes_out is zeroed by incoming ACKs even if they advertise zero window. 
  17.      * Hence, connection is killed only if we received no ACKs for normal connection timeout. 
  18.      * It is not killed only because window stays zero for some time, window may be zero until 
  19.      * armageddon and even later. We are full accordance with RFCs, only probe timer combines 
  20.      * both retransmission timeout and probe timeout in one bottle. 
  21.      */  
  22.   
  23.     max_probes = sysctl_tcp_retries2; /* 当没有收到ACK时,运行发送探测包的最大次数,之后连接超时 */  
  24.   
  25.     if (sock_flag(sk, SOCK_DEAD)) { /* 如果套接口即将关闭 */  
  26.         const int alive = ((icsk->icsk_rto << icsk->icsk_backoff) < TCP_RTO_MAX);  
  27.         max_probes = tcp_orphan_retries(sk, alive); /* 决定重传的次数 */  
  28.   
  29.         /* 如果当前的孤儿socket数量超过tcp_max_orphans,或者内存不够时,关闭此连接 */  
  30.         if (tcp_out_of_resource(sk, alive || icsk->icsk_probes_out <= max_probes))  
  31.             return;  
  32.     }  
  33.   
  34.     /* 如果发送出的探测报文的数目达到最大值,却依然没有收到对方的ACK时,关闭此连接 */  
  35.     if (icsk->icsk_probes_out > max_probes) { /* 实际上每次收到ACK后,icsk->icsk_probes_out都会被清零 */  
  36.         tcp_write_err(sk);  
  37.   
  38.     } else {  
  39.         /* Only send another probe if we didn't close things up. */  
  40.         tcp_send_probe0(sk); /* 发送零窗口探测报文 */  
  41.     }  
  42. }  

 

发送0 window探测报文和发送Keepalive探测报文用的是用一个函数tcp_write_wakeup():

1. 有新的数据段可供发送,且对端接收窗口还没被塞满。发送新的数据段,来作为探测包。

2. 没有新的数据段可供发送,或者对端的接收窗口满了。发送序号为snd_una - 1、长度为0的ACK包作为探测包。

 

和保活探测定时器不同,零窗口探测定时器总是使用第二种方法,因为此时对端的接收窗口为0。

所以会发送一个序号为snd_una - 1、长度为0的ACK包,对端收到此包后会发送一个ACK响应。

如此一来本端就能够知道对端的接收窗口是否打开了。

[java]  view plain  copy
  在CODE上查看代码片 派生到我的代码片
  1. /* A window probe timeout has occurred. 
  2.  * If window is not closed, send a partial packet else a zero probe. 
  3.  */  
  4.   
  5. void tcp_send_probe0(struct sock *sk)  
  6. {  
  7.     struct inet_connection_sock *icsk = inet_csk(sk);  
  8.     struct tcp_sock *tp = tcp_sk(sk);  
  9.     int err;  
  10.   
  11.     /* 发送一个序号为snd_una - 1,长度为0的ACK包作为零窗口探测报文 */  
  12.     err = tcp_write_wakeup(sk);  
  13.   
  14.     /* 如果网络中有发送且未确认的数据包,或者没有待发送的数据包。 
  15.      * 这个时候不需要使用零窗口探测定时器。前一种情况时已经有现成的探测包了, 
  16.      * 后一种情况中根本就不需要发送数据了。check again 8) 
  17.      */  
  18.     if (tp->packets_out || ! tcp_send_head(sk)) {  
  19.         /* Cancel probe timer, if it is not required. */  
  20.         icsk->icsk_probes_out = 0;  
  21.         icsk->icsk_backoff = 0;  
  22.         return;  
  23.     }  
  24.   
  25.     /* err:0成功,-1失败 */  
  26.     if (err < = 0) {  
  27.         if (icsk->icsk_backoff < sysctl_tcp_retries2)  
  28.             icsk->icsk_backoff++; /* 退避指数 */  
  29.   
  30.         icsk->icsk_probes_out++; /* 探测包的发送次数 */  
  31.         inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0, min(icsk->icsk_rto << icsk->icsk_backoff,   
  32.             TCP_RTO_MAX), TCP_RTO_MAX); /* 重置零窗口探测定时器 */  
  33.   
  34.     } else { /* 如果由于本地拥塞导致无法发送探测包 */  
  35.         /* If packet was not sent due to local congestion, 
  36.          * do not backoff and do not remember icsk_probes_out. 
  37.          * Let local senders to fight for local resources. 
  38.          * Use accumulated backoff yet. 
  39.          */  
  40.          if (! icsk->icsk_probes_out)  
  41.              icsk->icsk_probes_out = 1;  
  42.   
  43.          /* 使零窗口探测定时器更快的超时 */  
  44.          inet_csk_reset_xmit_timer(sk, ICSK_TIME_PROBE0,   
  45.             min(icsk->icsk_rto << icsk->icsk->icsk_backoff, TCP_RESOURCE_PROBE_INTERVAL),  
  46.             TCP_RTO_MAX);  
  47.     }  
  48. }  
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值