TCP的定时器

TCP为每条连接建立七个定时器,依次为:连接建立定时器、重传定时器、延时ACK定时器、持续定时器、保活定时器、FIN_WAIT_2定时器和TIME_WAIT定时器。实际上,为了提高效率,内核中只使用了四个定时器来完成七个定时器的功能。

TCP定时器的实现涉及以下文件:

net/ipv4/tcp_timer.c TCP的定时器

net/ipv4/inet_connection_sock.c 基于连接的传输控制块实现

net/ipv4/tcp_output.c TCP的输出

net/ipv4/tcp_input.c TCP的输入

初始化

传输控制块定时器的初始化函数tcp_init_xmit_timers()在创建套接口、传输控制块时被调用

void inet_csk_init_xmit_timers(struct sock *sk,
			       void (*retransmit_handler)(unsigned long),
			       void (*delack_handler)(unsigned long),
			       void (*keepalive_handler)(unsigned long))
{
	struct inet_connection_sock *icsk = inet_csk(sk);

	setup_timer(&icsk->icsk_retransmit_timer, retransmit_handler,
			(unsigned long)sk);
	setup_timer(&icsk->icsk_delack_timer, delack_handler,
			(unsigned long)sk);
	setup_timer(&sk->sk_timer, keepalive_handler, (unsigned long)sk);
	icsk->icsk_pending = icsk->icsk_ack.pending = 0;
}
void tcp_init_xmit_timers(struct sock *sk)
{
	inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer,
				  &tcp_keepalive_timer);
}

连接建立定时器

连接建立定时器用于被动建立连接时,服务端在接收到客户端的SYN,发送SYN+ACK段后,等待客户端的ACK段,试图建立一个新的连接时启动,超时几次后,连接建立将中止。

连接建立定时器处理函数会删除那些尝试连接的次数达到上限还未完成连接建立的传输控制块,该上限由inet_connection_sock结构的icsk_syn_retries给出。

连接建立定时器处理函数tcp_keepalive_timer(),该函数实现了TCP中的三个定时器:连接建立定时器、保活定时器和FIN_WAIT_2定时器。这是由于这三个定时器分别处于LISTEN、ESTABLISHED和FIN_WAIT2三种状态。

static void tcp_keepalive_timer (unsigned long data)
{
	struct sock *sk = (struct sock *) data;
	struct inet_connection_sock *icsk = inet_csk(sk);
	struct tcp_sock *tp = tcp_sk(sk);
	__u32 elapsed;

	/* Only process if socket is not in use. */
	bh_lock_sock(sk);
	if (sock_owned_by_user(sk)) {
		/* Try again later. */
		inet_csk_reset_keepalive_timer (sk, HZ/20);
		goto out;
	}

	if (sk->sk_state == TCP_LISTEN) {
		tcp_synack_timer(sk);
		goto out;
	}

	if (sk->sk_state == TCP_FIN_WAIT2 && sock_flag(sk, SOCK_DEAD)) {
		if (tp->linger2 >= 0) {
			const int tmo = tcp_fin_time(sk) - TCP_TIMEWAIT_LEN;

			if (tmo > 0) {
				tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
				goto out;
			}
		}
		tcp_send_active_reset(sk, GFP_ATOMIC);
		goto death;
	}

	if (!sock_flag(sk, SOCK_KEEPOPEN) || sk->sk_state == TCP_CLOSE)
		goto out;

	elapsed = keepalive_time_when(tp);

	/* It is alive without keepalive 8) */
	if (tp->packets_out || tcp_send_head(sk))
		goto resched;

	elapsed = tcp_time_stamp - tp->rcv_tstamp;

	if (elapsed >= keepalive_time_when(tp)) {
		if (icsk->icsk_probes_out >= keepalive_probes(tp)) {
			tcp_send_active_reset(sk, GFP_ATOMIC);
			tcp_write_err(sk);
			goto out;
		}
		if (tcp_write_wakeup(sk) <= 0) {
			icsk->icsk_probes_out++;
			elapsed = keepalive_intvl_when(tp);
		} else {
			/* If keepalive was lost due to local congestion,
			 * try harder.
			 */
			elapsed = TCP_RESOURCE_PROBE_INTERVAL;
		}
	} else {
		/* It is tp->rcv_tstamp + keepalive_time_when(tp) */
		elapsed = keepalive_time_when(tp) - elapsed;
	}

	TCP_CHECK_TIMER(sk);
	sk_mem_reclaim(sk);

resched:
	inet_csk_reset_keepalive_timer (sk, elapsed);
	goto out;

death:
	tcp_done(sk);

out:
	bh_unlock_sock(sk);
	sock_put(sk);
}

重传定时器

重传定时器在TCP发送数据时设定,如果定时器已超时而对端确认还未到达,则TCP将重传数据。重传定时器的超时时间值是动态计算的,取决于TCP为该连接测量的往返时间以及该段已被重传的次数

static void tcp_write_timer(unsigned long data)
{
	struct sock *sk = (struct sock *)data;
	struct inet_connection_sock *icsk = inet_csk(sk);
	int event;

	bh_lock_sock(sk);
	if (sock_owned_by_user(sk)) {
		/* Try again later */
		sk_reset_timer(sk, &icsk->icsk_retransmit_timer, jiffies + (HZ / 20));
		goto out_unlock;
	}

	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;
	icsk->icsk_pending = 0;

	switch (event) {
	case ICSK_TIME_RETRANS:
		tcp_retransmit_timer(sk);
		break;
	case ICSK_TIME_PROBE0:
		tcp_probe_timer(sk);
		break;
	}
	TCP_CHECK_TIMER(sk);

out:
	sk_mem_reclaim(sk);
out_unlock:
	bh_unlock_sock(sk);
	sock_put(sk);
}

延时确认定时器

延时ACK定时器在TCP收到必须被确认但无需马上发出确认的段时设定,TCP在200ms后发送确认响应,如果在200ms内,有数据要在该连接上发送,延时ACK响应就可以随数据一起发送回对端,称为捎带确认。

static void tcp_delack_timer(unsigned long data)
{
	struct sock *sk = (struct sock *)data;
	struct tcp_sock *tp = tcp_sk(sk);
	struct inet_connection_sock *icsk = inet_csk(sk);

	bh_lock_sock(sk);
	if (sock_owned_by_user(sk)) {
		/* Try again later. */
		icsk->icsk_ack.blocked = 1;
		NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_DELAYEDACKLOCKED);
		sk_reset_timer(sk, &icsk->icsk_delack_timer, jiffies + TCP_DELACK_MIN);
		goto out_unlock;
	}

	sk_mem_reclaim_partial(sk);

	if (sk->sk_state == TCP_CLOSE || !(icsk->icsk_ack.pending & ICSK_ACK_TIMER))
		goto out;

	if (time_after(icsk->icsk_ack.timeout, jiffies)) {
		sk_reset_timer(sk, &icsk->icsk_delack_timer, icsk->icsk_ack.timeout);
		goto out;
	}
	icsk->icsk_ack.pending &= ~ICSK_ACK_TIMER;

	if (!skb_queue_empty(&tp->ucopy.prequeue)) {
		struct sk_buff *skb;

		NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPSCHEDULERFAILED);

		while ((skb = __skb_dequeue(&tp->ucopy.prequeue)) != NULL)
			sk_backlog_rcv(sk, skb);

		tp->ucopy.memory = 0;
	}

	if (inet_csk_ack_scheduled(sk)) {
		if (!icsk->icsk_ack.pingpong) {
			/* Delayed ACK missed: inflate ATO. */
			icsk->icsk_ack.ato = min(icsk->icsk_ack.ato << 1, icsk->icsk_rto);
		} else {
			/* Delayed ACK missed: leave pingpong mode and
			 * deflate ATO.
			 */
			icsk->icsk_ack.pingpong = 0;
			icsk->icsk_ack.ato      = TCP_ATO_MIN;
		}
		tcp_send_ack(sk);
		NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_DELAYEDACKS);
	}
	TCP_CHECK_TIMER(sk);

out:
	if (tcp_memory_pressure)
		sk_mem_reclaim(sk);
out_unlock:
	bh_unlock_sock(sk);
	sock_put(sk);
}

持续定时器

持续定时器在对端通告接收窗口为0,阻止TCP继续发送数据时而设定。由于连接对端发送的窗口通告不可靠(只有数据才会确认,ACK不会被确认),允许TCP继续发送数据的后续窗口更新可能丢失,因此,如果TCP有数据要发送,而对端通告接收窗口为0,则持续定时器启动,超时后向对端发送1字节的数据,以判断对端接收窗口是否已经打开。

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;
	}

	/* *WARNING* RFC 1122 forbids this
	 *
	 * It doesn't AFAIK, because we kill the retransmit timer -AK
	 *
	 * FIXME: We ought not to do it, Solaris 2.5 actually has fixing
	 * this behaviour in Solaris down as a bug fix. [AC]
	 *
	 * Let me to explain. icsk_probes_out is zeroed by incoming ACKs
	 * even if they advertise zero window. Hence, connection is killed only
	 * if we received no ACKs for normal connection timeout. It is not killed
	 * only because window stays zero for some time, window may be zero
	 * until armageddon and even later. We are in full accordance
	 * with RFCs, only probe timer combines both retransmission timeout
	 * and probe timeout in one bottle.				--ANK
	 */
	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_resources(sk, alive || icsk->icsk_probes_out <= max_probes))
			return;
	}

	if (icsk->icsk_probes_out > max_probes) {
		tcp_write_err(sk);
	} else {
		/* Only send another probe if we didn't close things up. */
		tcp_send_probe0(sk);
	}
}

保活定时器

保活定时器在应用进程选取了套接口SO_KEEPALIVE选项时生效。如果连接的连续空闲时间超过2小时,则保活定时器超时,向对端发送连接探测段,强迫对端相应。相应处理函数tcp_keepalive_timer()。

FIN_WAIT_2定时器

当某个连接从FIN_WAIT_1状态变迁到FIN_WAIT_2状态,且不能再接收任何新数据时,则意味着应用进程调用了close()而非shutdown(),没有利用TCP的半关闭功能,FIN_WAIT_2定时器启动,超时时间为10min,在定时器第一次超时后,重新设置超时时间为75s,第二次超时后关闭连接。加入这个定时器的目的是为了避免对端一直不发FIN,某个连接会永远滞留在FIN_WAIT_2状态。处理函数为tcp_keepalive_timer()。

时间: 2024-08-29 13:20:37

TCP的定时器的相关文章

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

主要内容:零窗口探测定时器的实现. 内核版本:3.15.2 我的博客:http://blog.csdn.net/zhangskd 出现以下情况时,TCP接收方的接收缓冲区将被塞满数据: 发送方的发送速度大于接收方的接收速度. 接收方的应用程序未能及时从接收缓冲区中读取数据. 当接收方的接收缓冲区满了以后,会把响应报文中的通告窗口字段置为0,从而阻止发送方的继续发送, 这就是TCP的流控制.当接收方的应用程序读取了接收缓冲区中的数据以后,接收方会发送一个ACK,通过 通告窗口字段告诉发送方自己又可

TCP保活定时器

TCP有Keepalive功能,它和HTTP的Keepalive功能目的不一样.TCP服务器希望知道客户端是否崩溃.重新启动或者中间路由不通.保活定时器就提供这种功能. 在进一步介绍TCP的保活定时器前,先了解一个概念:长连接和短连接.(TCP是长连接) 长连接:建立一个连接,多个请求复用这个连接,最后再关闭连接. 短连接:建立一个连接,传输一个请求,然后关闭连接. 当服务器发送探测报文时,客户端可能处于4种不同的情况:仍然正常运行.已经崩溃.已经崩溃并重启了. 由于中间链路问题不可达.在不同的

TCP坚持定时器

当接收方TCP缓冲区没有剩余空间后,在ACK中会通知发送方window=0,此时发送方就暂停发送数据.当接收方TCP缓冲区又有空间后,会再次发送一个ACK,告知其剩余缓冲区大小,可以接受新的数据包了,这个ACK叫做窗口更新.TCP接收方则等待新的数据包过来.但是如果这个窗口更新的ACK丢失了,那么则会两端互相等待:接收方等待新的数据包,因为他已经通知对端新的window大小了,而发送方则还在等待窗口更新,因为它还认为对端窗口为0. 坚持定时器(Persist Timer)就是为了解决这个问题而设

【转】TCP/IP详解学习笔记(二)

TCP/IP详解学习笔记(5)-IP选路,动态选路,和一些细节 1.静态IP选路 1.1.一个简单的路由表 选路是IP层最重要的一个功能之一.前面的部分已经简单的讲过路由器是通过何种规则来根据IP数据包的IP地址来选择路由.这里就不重复了.首先来看看一个简单的系统路由表. Destination     Gateway         Genmask         Flags Metric Ref    Use Iface192.168.11.0    *               255.

TCP/IP详解学习笔记

TCP/IP详解学习笔记(1)-基本概念 TCP/IP详解学习笔记(2)-数据链路层 TCP/IP详解学习笔记(3)-IP协议,ARP协议,RARP协议 TCP/IP详解学习笔记(4)-ICMP协议,ping和Traceroute TCP/IP详解学习笔记(5)-IP选路,动态选路,和一些细节 TCP/IP详解学习笔记(6)-UDP协议 TCP/IP详解学习笔记(7)-广播和多播,IGMP协议 TCP/IP详解学习笔记(8)-DNS域名系统 TCP/IP详解学习笔记(9)-TCP协议概述 TCP

TCP/IP详解之一:连接建立、断开

<TCP/IP详解·卷一>看了三遍才算整明白个大概,一直想做个总结. 最初对TCP的印象很简单:丢包重传.流数据.丢包重传很好理解,“流数据”是什么鬼? 知乎上看到个极好的解释:把TCP看作用管子往对端灌水,水是数据,它们之间没有边界,且先发先到:UDP是往对端滚小球,它们之间有明确边界,且可能每个小球速度不同,先滚的不一定先到,得自己处理乱序. 编码上也可看出,TCP的send回调带有dwNumberOfBytesTransferred参数,描述本次网络IO发送了多少字节数据,而不是给它多少

LINUX TCP套接字详细配置

提高服务器的负载能力,是一个永恒的话题.在一台服务器CPU和内存资源额定有限的情况下,最大的压榨服务器的性能,是最终的目的.要提高 Linux系统下的负载能力,可以先启用Apache的Worker模式,来提高单位时间内的并发量.但是即使这么做了,当网站发展起来之后,连接数过多 的问题就会日益明显.在节省成本的情况下,可以考虑修改Linux的内核TCP/IP参数,来最大的压榨服务器的性能.当然,如果通过修改内核参数也无法 解决的负载问题,也只能考虑升级服务器了,这是硬件所限,没有办法的事. Lin

《TCP/IP详解:卷一》-TCP部分讲解

TCP/IP协议 作者:Danbo 2015-7-2 本文为参考TCP/IP详解卷一,某些知识点加上了作者自己的理解,如有错误,欢迎指正,可以微博联系我! TCP包格式和IP包格式如下: TCP的正常建立与关闭 建立连接 TCP协议提供可靠的面向连接服务,采用三次握手建立连接.第一次握手:建立连接时,客户端发送SYN包(syn=j)到服务器,并进入SYN_SEND状态,等待服务器确认:第二次握手:服务器收到SYN包,向客户端返回ACK(ack=j+1),同时自己也发送一个SYN包(syn=k),

LWIP裸机环境下实现TCP与UDP通讯

前面移植了LWIP,并且简单的实用了DHCP的功能,今天来使用一下实际的数据通讯的功能 首先是实现TCP客户端,我先上代码 #ifndef __TCP_CLIENT_H_ #define __TCP_CLIENT_H_ #include "network.h" //连接状态 enum tcp_client_states { ES_NONE = 0, ES_RECEIVED, //接收到了数据 ES_CLOSING //连接关闭 }; //TCP服务器状态 struct tcp_clie