linux TCP超时重传

TCP超时重传是保证TCP可靠性传输的机制之一,当超时后仍没有收到应答报文,就重传数据包并设置超时时钟(超时时间一般增大到原超时时间2倍);直到收到应答报文或超过最大重试次数。


linux TCP超时重传是通过设置重传超时时钟icsk_retransmit_timer来实现的。
零窗探测超时时钟与重传超时时钟共用icsk_retransmit_timer,根据icsk_pending是ICSK_TIME_RETRANS、ICSK_TIME_PROBE0来判断是重传超时还是零窗探测超时。
只有当发送方被通告零窗,连接双方没有数据来往而使接收方无法通过ACK报文通告新窗口时,才使用零窗探测机制;所以重传队列中有重传包时,不会出现零窗探测,出现零窗时不能再发送新数据也就没有重传;所以零窗探测超时时钟与重传超时时钟可以共用icsk_retransmit_timer


I.超时处理函数

i.tcp_retransmit_timer

281 /*
282  *      The TCP retransmit timer.
283  */
284 
285 void tcp_retransmit_timer(struct sock *sk)
286 {
287         struct tcp_sock *tp = tcp_sk(sk);
288         struct inet_connection_sock *icsk = inet_csk(sk);
289 
290         if (!tp->packets_out)
291                 goto out;
292 
293         WARN_ON(tcp_write_queue_empty(sk));
294 
295         if (!tp->snd_wnd && !sock_flag(sk, SOCK_DEAD) &&
296             !((1 << sk->sk_state) & (TCPF_SYN_SENT | TCPF_SYN_RECV))) {
297                 /* Receiver dastardly shrinks window. Our retransmits
298                  * become zero probes, but we should not timeout this
299                  * connection. If the socket is an orphan, time it out,
300                  * we cannot allow such beasts to hang infinitely.
301                  */
302 #ifdef TCP_DEBUG
303                 struct inet_sock *inet = inet_sk(sk);
304                 if (sk->sk_family == AF_INET) {
305                         LIMIT_NETDEBUG(KERN_DEBUG "TCP: Peer %pI4:%u/%u unexpectedly shrunk window %u:%u (repaired)\n",
306                                &inet->daddr, ntohs(inet->dport),
307                                inet->num, tp->snd_una, tp->snd_nxt);
308                 }
309 #if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE)
310                 else if (sk->sk_family == AF_INET6) {
311                         struct ipv6_pinfo *np = inet6_sk(sk);
312                         LIMIT_NETDEBUG(KERN_DEBUG "TCP: Peer %pI6:%u/%u unexpectedly shrunk window %u:%u (repaired)\n",
313                                &np->daddr, ntohs(inet->dport),
314                                inet->num, tp->snd_una, tp->snd_nxt);
315                 }
316 #endif
317 #endif
318                 if (tcp_time_stamp - tp->rcv_tstamp > TCP_RTO_MAX) {
319                         tcp_write_err(sk);
320                         goto out;
321                 }
322                 tcp_enter_loss(sk, 0);
323                 tcp_retransmit_skb(sk, tcp_write_queue_head(sk));
324                 __sk_dst_reset(sk);
325                 goto out_reset_timer;
326         }
327 
328         if (tcp_write_timeout(sk))
329                 goto out;
330 
331         if (icsk->icsk_retransmits == 0) {
332                 int mib_idx;
333 
334                 if (icsk->icsk_ca_state == TCP_CA_Disorder) {
335                         if (tcp_is_sack(tp))
336                                 mib_idx = LINUX_MIB_TCPSACKFAILURES;
337                         else
338                                 mib_idx = LINUX_MIB_TCPRENOFAILURES;
339                 } else if (icsk->icsk_ca_state == TCP_CA_Recovery) {
340                         if (tcp_is_sack(tp))
341                                 mib_idx = LINUX_MIB_TCPSACKRECOVERYFAIL;
342                         else
343                                 mib_idx = LINUX_MIB_TCPRENORECOVERYFAIL;
344                 } else if (icsk->icsk_ca_state == TCP_CA_Loss) {
345                         mib_idx = LINUX_MIB_TCPLOSSFAILURES;
346                 } else {
347                         mib_idx = LINUX_MIB_TCPTIMEOUTS;
348                 }
349                 NET_INC_STATS_BH(sock_net(sk), mib_idx);
350         }
351 
352         if (tcp_use_frto(sk)) {
353                 tcp_enter_frto(sk);
354         } else {
355                 tcp_enter_loss(sk, 0);
356         }
357 
358         if (tcp_retransmit_skb(sk, tcp_write_queue_head(sk)) > 0) {
359                 /* Retransmission failed because of local congestion,
360                  * do not backoff.
361                  */
362                 if (!icsk->icsk_retransmits)
363                         icsk->icsk_retransmits = 1;
364                 inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
365                                           min(icsk->icsk_rto, TCP_RESOURCE_PROBE_INTERVAL),
366                                           TCP_RTO_MAX);
367                 goto out;
368         }
369 
370         /* Increase the timeout each time we retransmit.  Note that
371          * we do not increase the rtt estimate.  rto is initialized
372          * from rtt, but increases here.  Jacobson (SIGCOMM 88) suggests
373          * that doubling rto each time is the least we can get away with.
374          * In KA9Q, Karn uses this for the first few times, and then
375          * goes to quadratic.  netBSD doubles, but only goes up to *64,
376          * and clamps at 1 to 64 sec afterwards.  Note that 120 sec is
377          * defined in the protocol as the maximum possible RTT.  I guess
378          * we'll have to use something other than TCP to talk to the
379          * University of Mars.
380          *
381          * PAWS allows us longer timeouts and large windows, so once
382          * implemented ftp to mars will work nicely. We will have to fix
383          * the 120 second clamps though!
384          */
385         icsk->icsk_backoff++;
386         icsk->icsk_retransmits++;
387 
388 out_reset_timer:
389         icsk->icsk_rto = min(icsk->icsk_rto << 1, TCP_RTO_MAX);
390         inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS, icsk->icsk_rto, TCP_RTO_MAX);
391         if (retransmits_timed_out(sk, sysctl_tcp_retries1 + 1))
392                 __sk_dst_reset(sk);
393 
394 out:;
395 }

1.如果是零窗且不是握手包(零窗时仍能发送SYN/SYN-ACK包,由于不包含数据)
  a.检查保活时间上限,如果超过上限,则表示TCP连接已经断开,通知上层应用
  b.进入Loss拥塞状态
  c.重传队列首包重传
  d.重置路由缓存
  e.设置超时时钟,如果重传发送成功重传时间增大2倍(以避免拥塞),如果重传发送失败重传时间不变
2.如果是零窗且且是握手包,或非零窗
  a.根据sysctl配置,检查重传次数是否已经超过上限,如果超过上限说明连接异常断开,通知上层应用
  b.如果是第一次重传,则统计MIB信息
  c.进入Loss拥塞状态或Disorder拥塞状态
  d.重传队列首包重传
  e.设置超时时钟,如果重传发送成功重传时间增大2倍(以避免拥塞),如果重传发送失败重传时间不变

 

ii.tcp_write_timeout
检测TCP连接异常断开机制有,没有数据发送时的保活机制(默认保活超时为sysctl_tcp_keepalive_time=7200s=2小时),有数据发送时的重传超限等
tcp_write_timeout主要用于检测重传次数超限,如果超限后但仍未成功,则表示该连接已经异常断开。
握手时SYN与SYN-ACK的重传上限由sysctl_tcp_syn_retries设置;数据发送的重传上限由sysctl_tcp_retries2设置
sysctl_tcp_syn_retries、sysctl_tcp_retries2严格来讲不是重传次数上限,而是用来衡量超限时间距包第一次重传时间或包发送时间的时间间隔。
重传超限检测函数retransmits_timed_out

1286 /* This function calculates a "timeout" which is equivalent to the timeout of a
1287  * TCP connection after "boundary" unsucessful, exponentially backed-off
1288  * retransmissions with an initial RTO of TCP_RTO_MIN.
1289  */
1290 static inline bool retransmits_timed_out(struct sock *sk,
1291                                          unsigned int boundary)
1292 {
1293         unsigned int timeout, linear_backoff_thresh;
1294         unsigned int start_ts;
1295 
1296         if (!inet_csk(sk)->icsk_retransmits)
1297                 return false;
1298 
1299         if (unlikely(!tcp_sk(sk)->retrans_stamp))
1300                 start_ts = TCP_SKB_CB(tcp_write_queue_head(sk))->when;
1301         else
1302                 start_ts = tcp_sk(sk)->retrans_stamp;
1303 
1304         linear_backoff_thresh = ilog2(TCP_RTO_MAX/TCP_RTO_MIN);
1305 
1306         if (boundary <= linear_backoff_thresh)
1307                 timeout = ((2 << boundary) - 1) * TCP_RTO_MIN;
1308         else
1309                 timeout = ((2 << linear_backoff_thresh) - 1) * TCP_RTO_MIN +
1310                           (boundary - linear_backoff_thresh) * TCP_RTO_MAX;
1311 
1312         return (tcp_time_stamp - start_ts) >= timeout;
1313 }

start_ts:第一次重传时间或发送时间
TCP_RTO_MIN=(HZ/5)=0.2s
TCP_RTO_MAX=(120*HZ)=120s
linear_backoff_thresh = ilog2(120*5)=ilog2(0x258)=9
timeout:未超过linear_backoff_thresh=9的部分按TCP_RTO_MIN 2的指数倍增长,超过的部分按TCP_RTO_MAX线性增长
tcp_time_stamp:当前时钟时间
例如数据发送阶段,sysctl_tcp_retries2=9,则timeout=1023*TCP_RTO_MIN=204.6s;sysctl_tcp_retries2=11时,timeout=1023*TCP_RTO_MIN+2*TCP_RTO_MAX=448.6s
默认sysctl_tcp_retries2=15,timeout=1023*TCP_RTO_MIN+6*TCP_RTO_MAX=920.6s,约15分钟

 


II.超时时间
TCP超时时间初始值是根据RTT时间计算得到,随着重传次数增加重传时间间隔也增加,如
发送包1->超时重传包1->超时重传包1->超时重传包1->收到ack包1->超时重传包2->超时重传包2
的超时时间变化如下:
a      ->2a         ->4a         ->8a         ->a         ->2a         ->4a

 

i.超时时间初始值
ack新数据时,都会重新计算RTT,此时也会根据新的RTT计算超时时间
tcp_clean_rtx_queue->tcp_ack_update_rtt->tcp_ack_saw_tstamp/tcp_ack_saw_tstamp->tcp_valid_rtt_meas->tcp_set_rto
linux TCP的采用平滑RTT计算方式,函数为tcp_rtt_estimator
srtt:平滑的rtt,新rtt平滑值 = 7/8老rtt平滑值 + 1/8新值
mdev:平滑的rtt差值,用来衡量rtt的抖动情况,新mdev平滑值 = 3/4老mdev平滑值 + 1/4新值
mdev_max:上一个RTT内的最大mdev,代表上个RTT内时延的波动情况,有效期为一个RTT
rttvar:mdev_max的平滑值,可升可降,代表着连接的抖动情况

超时时间初始值=__tcp_set_rto()=srtt/8 + rttvar(除8是因为将RTT扩大8倍存储在srtt中);

 

ii.重传超时加倍
重传失败后,超时时间间隔加倍后,再次重传;

 


III.重传超时时钟注册,启用,停止与修改
当TCP连接建立时,注册重传超时时钟处理函数
当重传队列为空,有新数据发送时,启用超时时钟;
当收到ACK报文,重传队列中的数据全都确认后,停止超时时钟;
当收到ACK报文,但是重传队列仍有数据时,修改超时时钟为初始值(根据平滑RTT计算),作为下一个待重传包的超时。
注:重传时钟超时只重传重传队列的首包;此时重传队列数据包的ACK报文都在拥塞状态机Loss状态下处理。

 

i.重传超时时钟处理函数注册
在TCP主动连接或被动连接时初始化超时时钟,即发送SYN或收到SYN时初始化(linux实现是,在收到SYN报文创建request_sock,收到ACK时创建子sock,在创建子sock时初始化超时时钟)
发送SYN:
socket->inet_create->tcp_v4_init_sock->tcp_init_xmit_timers->inet_csk_init_xmit_timers
收到ACK:
tcp_v4_rcv->tcp_v4_do_rcv->tcp_v4_hnd_req->tcp_check_req->tcp_v4_syn_recv_sock->tcp_create_openreq_child->tcp_init_xmit_timers->inet_csk_init_xmit_timers

 

ii.超时时钟启用
发送数据时启用超时时钟:如果没有未被确认的报文,即重传队列为空,则启用超时时钟
tcp_write_xmit->tcp_event_new_data_sent->inet_csk_reset_xmit_timer->sk_reset_timer->mod_timer
注:mod_timer激活时钟,或修改已经激活时钟的过期时间;此处为激活超时时钟

 

iii.重传超时时钟停止
收到ACK报文,确认重传队列中所有报文时,停止重传超时时钟
tcp_ack->tcp_clean_rtx_queue->tcp_rearm_rto->inet_csk_clear_xmit_timer->icsk_pending=0

 

iv.重传超时时钟修改
收到ACK报文,但是重传队列中仍有重传包,则重置重传超时时钟:
tcp_ack->tcp_clean_rtx_queue->tcp_rearm_rto->inet_csk_reset_xmit_timer->sk_reset_timer->mod_timer
注:mod_timer激活时钟,或修改已经激活时钟的过期时间;此处为修改已经激活时钟的过期时间

 


IV.重传队列
重传队列如图:

linux TCP超时重传_第1张图片
sk_write_queue->next:用于标识重传队列首skb
sk_send_head:用于标识重传队列尾下一个skb,发送缓存数据的起始skb,在tcp_add_write_queue_tail赋初始值,在tcp_event_new_data_sent向前移动。在send系统调用中,当因拥塞或流量控制而无法直接发送出去时,会将数据放入发送缓存中,在收到ACK报文后tcp_data_snd_check会对缓存数据再次尝试发送;如果缓存数据仍不能发送成功,且重传队列中无数据时,会启用零窗探测时钟以保证缓存数据一定可以发送出去(如果发送缓存中有数据,零窗探测会发送缓存数据,否则会发送零窗探测报文,则保证一定能够收到ACK报文)。
packets_out:表示重传队列中数据长度,在tcp_event_new_data_sent中增大

你可能感兴趣的:(linux TCP超时重传)