TCP连接中的七个定时器
TCP为每条连接建立了七个定时器。按照它们在一条连接生存期内出现的次序,简要介绍如下:
1) “连接建立”定时器在发送SYN报文段建立一条新连接时启动。如果在75秒内没收到响应,连接建立将终止。
2) “重传”定时器在TCP发送数据时设定。如果定时器已超时而对端的确认还未到达,TCP将重新传数据。重传定时器的值(即TCP等待对端确认的时间)是动态计算的,取决于TCP为该连接测量的往返时间和该报文段已被重传的次数。
3) “延迟ACK”定时器在TCP收到必须被确认但无需马上发出确认的数据时设定。TCP等待200ms后发送确认响应。如果在这200ms内,有数据要在该连接上发送,延迟的ACK响应可随着数据一起发送到对端,成为捎带确认。
4) “持续”定时器在连接对端通告接收窗口为0,阻止TCP继续发送数据时设定。由于连接对端发送的窗口通告不可靠(只有数据才会被确认,ACK不会被确认),允许TCP继续发送数据的后继窗口更新可能丢失。因此,如果TCP有数据要发送,但对端通告的窗口为0,则启动持续定时器,超时后向对端发送1字节的数据,判断对端接受窗口是否已打开。与重传定时器类似,持续定时器的值也是动态计算的,取决于连接建立的往返时间,在5秒到60秒之间取值。
5) “保活”定时器在应用进程选取插口的SO_KEEPALIVE选项时生效。如果连接的连续空闲时间超过2小时,保活定时器超时,向对端发送连接探测报文段,强迫对端响应。如果收到了期待的响应,TCP可确定对端主机正常工作,在该连接再次空闲超过2小时之前,TCP不会在进行保活测试。如果收到的是其他响应,TCP可确定对端主机已重启。如果连续若干次保活测试都未收到响应,TCP就假定对端主机已崩溃,尽管它无法区分是主机故障(例如,系统崩溃为重启),还是连接故障(例如,中间路由器发生故障或者电话线断了)。
6) FIN_WAIT_2定时器。当某个连接从FIN_WAIT_1状态变迁到FIN_WAIT_2状态,并且不能在接收任何新数据时(意味着应用进程调用了close,而非shutdown,没有利用TCP的半关闭功能),FIN_WAIT_2定时器启动,设为10分钟。定时器超时后,重新设为75秒,第二次超时后连接关闭。加入这个定时器的目的是为了避免如果对端一直不发送FIN,某个连接会永远都滞留在FIN_WAIT_2状态。
7) TIME_WAIT定时器,一般也称为2MSL定时器。2MSL指两倍的MSL。当连接转移到TIME_WAIT时,即连接主动关闭时,定时器启动。
1、延迟ACK定时器
TCP收到必须被确认但无需马上发出确认的数据时设定,当定时器超时时,内核调用tcp_fasttimo(内核每隔200ms调用一次tcp_fasttimo),在tcp_fasttimo中检查TCP链表中每个具有对应TCP控制块的Internet PCB,如果TF_DELACK标志置位,清除该标志,并置TF_ACKNOW标志。调用tcp_output,由于TF_ACKNOW标置已置位,ACK被发送。
2、FIN_WAIT_2和2MSL定时器
tcp_slowtimo函数中,当TCP的四个定时计数器(对应6个定时器)任何一个超时时,都会调用请求类型为PRU_SLOWTIMO的tcp_usrreq( )函数。该函数的部分代码如下,可以看出,对PRU_SLOWTIMO的请求是调用tcp_timers( )函数来处理的。
int
tcp_usrreq(so, req, m, nam, control)
struct socket *so;
int req;
struct mbuf *m, *nam, *control;
{
/*初始化处理*/
......
Switch(req)
{
/*switch cases*/
...
case PRU_SLOWTIMO:
tp = tcp_timers(tp, (int)nam);
req |= (int)nam << 8; /* for debug's sake */
break;
...
}
...
}
struct tcpcb *
tcp_timers(tp, timer)
register struct tcpcb *tp;
int timer;
{
register int rexmt;
switch (timer) {
/*
* 2 MSL timeout in shutdown went off. If we're closed but
* still waiting for peer to close and connection has been idle
* too long, or if 2MSL time is up from TIME_WAIT, delete connection
* control block. Otherwise, check again in a bit.
*/
case TCPT_2MSL:
if (tp->t_state != TCPS_TIME_WAIT &&
tp->t_idle <= tcp_maxidle)
tp->t_timer[TCPT_2MSL] = tcp_keepintvl;
else
tp = tcp_close(tp);
break;
TIME_WAIT定时器:
因为TCPS_2MSL计数器的表示两个定时器,在上述代码中的判断逻辑比较复杂。首先看TIME_WAIT定时器,定时器60秒后超时,将调用tcp_close并释放控制块。
FIN_WAIT_2定时器:
如果连接状态不是TIME_WAIT,TCPT_2MSL计数器表示FIN_WAIT_2定时器。只要连接的空闲时间超过10分钟(tcp_maxidle),连接就会被关闭;但如果连接的空闲时间小于或等于10分钟,FIN_WAIT_2定时器将被设定为75秒。
在FIN_WAIT_1状态下的连接接收到一个ACK后,从FIN_WAIT_1状态变迁到FIN_WAIT_2状态,t_idle被置为0,FIN_WAIT_2定时器被设定为1200(10分钟),当FIN_WAIT_2超时后,t_idle为1198,小于tcp_maxidle,则FIN_WAIT_2定时器设定为150,定时器75秒后在此超时,假定连接一直空闲,则t_idle>tcp_maxidle,调用tcp_close关闭连接。
3、持续定时器
case TCPT_PERSIST:
tcpstat.tcps_persisttimeo++;
/*
* Hack: if the peer is dead/unreachable, we do not
* time out if the window is closed. After a full
* backoff, drop the connection if the idle time
* (no responses to probes) reaches the maximum
* backoff that we would use if retransmitting.
*/
if (tp->t_rxtshift == TCP_MAXRXTSHIFT &&
(tp->t_idle >= tcp_maxpersistidle ||
tp->t_idle >= TCP_REXMTVAL(tp) * tcp_totbackoff)) {
tcpstat.tcps_persistdrop++;
tp = tcp_drop(tp, ETIMEDOUT);
break;
}
tcp_setpersist(tp);
tp->t_force = 1;
(void) tcp_output(tp);
tp->t_force = 0;
break;
如果重传次数达到内核容许的最大重传次数,且连接的空闲时间大于tcp_maxpersistidle或者大于TCP_REXMTVAL(tp) * tcp_totbackoff),则调用tcp_drop丢弃连接。
如果上述过程没有发生,则调用tcp_setpersist计算持续定时器的下一个设定值,并存粗在TCPT_PERSIST计算器中。t_force标志置位,强制tcp_output发送1字节数据。
一旦持续定时器取值达到60秒,TCP将每隔60秒发送一次探测报文段。
4、连接建立定时器和保活定时器
case TCPT_KEEP:
tcpstat.tcps_keeptimeo++;
if (tp->t_state < TCPS_ESTABLISHED)
goto dropit;
连接建立定时器
如果连接状态小于ESTABLISHED,TCP_KEEP计数器代表连接建立定时器。定时器超时后,控制转到dropit,调用tcp_drop终止连接,给出差错代码ETIMEDOUT。
TCP发送SYN的同时初始化了两个定时器:连接建立定时器设定为75s和重传定时器,保证对端无响应时可以重传SYN。
对于一个新连接,重传定时器初始化为6,后续值依次为24,48,也就是说在tcp重传3次SYN时如果仍没有ACK确认,那么在75s时连接建立定时器超时,调用tcp_drop终止连接。
保活定时器
if (tp->t_inpcb->inp_socket->so_options & SO_KEEPALIVE &&
tp->t_state <= TCPS_CLOSE_WAIT) {
所有连接上的保活定时器在连续2小时空闲后超时,无论连接是否选取了插口的SO_KEEPALIVE选项。如果插口选项置位,并且连接处于ESTABLISHED状态活CLOSE_WAIT状态,TCP将发送连接探测报文段。但如果应用进程调用了close(状态大于CLOSE_WAIT),即使连接已空闲2小时,TCP也不会发送连接探测报文。
if (tp->t_idle >= tcp_keepidle + tcp_maxidle)
goto dropit;
如果连接的空闲时间大于或等于2小时加10分钟,连接将被丢弃。也就是说,对端无响应时,TCP最多发送9个连接探测报文段,间隔75秒(tcp_keepintvl)。TCP在确认连接已死之前必须发送多个连接探测报文段的一个原因是,对端的响应很可能是不带数据的纯ACK报文段(不能有超时定时器,接收端也不对其进行确认),TCP无法保证此类报文段的可靠传输,因此,连接探测报文段的响应可能丢失。
tcpstat.tcps_keepprobe++;
#ifdef TCP_COMPAT_42
/*
* The keepalive packet must have nonzero length
* to get a 4.2 host to respond.
*/
tcp_respond(tp, tp->t_template, (struct mbuf *)NULL,
tp->rcv_nxt - 1, tp->snd_una - 1, 0);
#else
tcp_respond(tp, tp->t_template, (struct mbuf *)NULL,
tp->rcv_nxt, tp->snd_una - 1, 0);
#endif
tp->t_timer[TCPT_KEEP] = tcp_keepintvl;
如果TCP进行保活测试的次数还在许可的范围之内,tcp_respond将发送连接探测包文段。
} else
tp->t_timer[TCPT_KEEP] = tcp_keepidle;
如果插口选项未置位,或者连接状态大于CLOSE_WAIT,连接的保活定时器将复位,重新被置为2小时。
break;
dropit:
tcpstat.tcps_keepdrops++;
tp = tcp_drop(tp, ETIMEDOUT);
break;
重传定时器
case TCPT_REXMT:
if (++tp->t_rxtshift > TCP_MAXRXTSHIFT) {
tp->t_rxtshift = TCP_MAXRXTSHIFT;
tcpstat.tcps_timeoutdrop++;
tp = tcp_drop(tp, tp->t_softerror ?
tp->t_softerror : ETIMEDOUT);
break;
}
重传移位计数器(t_rxtshift )在每次重传时递增,如果大于12(TCP_MAXRXTSHIFT),连接将被丢弃。
tcpstat.tcps_rexmttimeo++;
rexmt = TCP_REXMTVAL(tp) * tcp_backoff[tp->t_rxtshift];
TCPT_RANGESET(tp->t_rxtcur, rexmt,
tp->t_rttmin, TCPTV_REXMTMAX);
tp->t_timer[TCPT_REXMT] = tp->t_rxtcur;
利用TCP_REXMTVAL宏实现指数退避,计算新的RTO值。新的RTO值存储在t_rxtcur中,供连接的重传定时器——t_timer[TCPT_REXMT]——使用,tcp_input在启动重传定时器时会用到它。
if (tp->t_rxtshift > TCP_MAXRXTSHIFT / 4) {
in_losing(tp->t_inpcb);
如果报文段以重传4次以上,in_losing将释放缓存中的路由,tcp_output再重传该报文时,将选择一条新的,也许好一些的路由。
tp->t_rttvar += (tp->t_srtt >> TCP_RTT_SHIFT);
tp->t_srtt = 0;
已平滑的RTT估计器(t_srtt)被置为0,强迫tcp_xmit_timer将下一个RTT测量值做为已平滑的RTT估计器,这是因为报文段重传4此后,意味着TCP的以平滑的RTT估计器可能已经失效。
}
tp->snd_nxt = tp->snd_una;
下一个发送序号被置为最早的未确认的序号。
/*
* If timing a segment in this window, stop the timer.
*/
Karn算法
tp->t_rtt = 0;
RTT计数器,t_rtt,被置为0。Karn算法认为:由于报文段即将重传,对该报文段的计时就失去了意义。即使收到了ACK,也无法区分它是对第一次报文还是对第二次报文的确认。
慢启动和避免拥塞
{
u_int win = min(tp->snd_wnd, tp->snd_cwnd) / 2 / tp->t_maxseg;
if (win < 2)
win = 2;
tp->snd_cwnd = tp->t_maxseg;
tp->snd_ssthresh = win * tp->t_maxseg;
Win被置为现有窗口大小(接收方通告的窗口大小snd_wnd和发送方拥塞窗口大小snd_cwnd ,两者之间的最小值)的一半,以报文为单位而非字节,最小值为2 。它的值等于网络拥塞时现有窗口大小的一半,也就是满启动门限t_ssthresh。拥塞窗口的大小,被置为只容纳1个报文,强迫执行慢启动。上述做法假定造成网络拥塞的原因之一是本地数据发送太快,因此在拥塞发生时,必须降低发送窗口的大小。
tp->t_dupacks = 0;
连续重复ACK计数器,t_dupacks 被置为0。在TCP快速重传和快速恢复算法中将用到它。
}
(void) tcp_output(tp);
tcp_output重新发送包含最早的未确认序号的报文,即由重传定时器超时引发了报文重传。
break;
}
return (tp);
}