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.重传队列
重传队列如图:
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中增大