日期:2014-05-16  浏览次数:20761 次

linux内核tcp的定时器管理(一)
在内核中tcp协议栈有6种类型的定时器:

1 重传定时器。

2 delayed ack定时器

3 零窗口探测定时器

上面三种定时器都是作为tcp状态机的一部分来实现的。

4 keep-alive 定时器

主要是管理established状态的连接。

5 time_wait定时器

主要是用来客户端关闭时的time_wait状态用到。

6 syn-ack定时器(主要是用在listening socket)

管理新的连接请求时所用到。


而在内核中,tcp协议栈管理定时器主要有下面4个函数:

inet_csk_reset_xmit_timer

这个函数是用来重启定时器

inet_csk_clear_xmit_timer

这个函数用来删除定时器。

上面两个函数都是针对状态机里面的定时器。

tcp_set_keepalive

这个函数是用来管理keepalive 定时器的接口。

tcp_synack_timer

这个函数是用来管理syn_ack定时器的接口。


ok,我们现在先来看定时器的初始化。

首先是在tcp_v4_init_sock中对定时器的初始化,它会调用tcp_init_xmit_timers,我们就先来看这个函数:

void tcp_init_xmit_timers(struct sock *sk)
{
	inet_csk_init_xmit_timers(sk, &tcp_write_timer, &tcp_delack_timer,
				  &tcp_keepalive_timer);
}



可以看到这个函数很简单,就是调用inet_csk_init_xmit_timers,然后把3个定时器的回掉函数传递进去,下面我们来看inet_csk_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;
}




我们可以看到icsk->icsk_retransmit_timer定时器,也就是重传定时器的回调函数是tcp_write_timer,而icsk->icsk_delack_timer定时器也就是delayed-ack 定时器的回调函数是tcp_delack_timer,最后sk->sk_timer也就是keepalive定时器的回掉函数是tcp_keepalive_timer.

这里还有一个要注意的,tcp_write_timer还会处理0窗口定时器。

这里有关内核定时器的一些基础的东西我就不介绍了,想了解的可以去看下ldd第三版。

接下来我们就来一个个的分析这6个定时器,首先是重传定时器。

我们知道4层最终调用tcp_xmit_write来讲数据发送到3层,并且tcp是字节流的,因此每次他总是发送一段数据到3层,而每次当它发送完毕(返回正确),则它就会启动重传定时器,我们来看代码:


static int tcp_write_xmit(struct sock *sk, unsigned int mss_now, int nonagle,
			  int push_one, gfp_t gfp)
{
	struct tcp_sock *tp = tcp_sk(sk);
	struct sk_buff *skb;
	unsigned int tso_segs, sent_pkts;
	int cwnd_quota;
	int result;

.............................................

	while ((skb = tcp_send_head(sk))) {
..................................................

///可以看到只有当传输成功,我们才会走到下面的函数。
		if (unlikely(tcp_transmit_skb(sk, skb, 1, gfp)))
			break;

		/* Advance the send_head.  This one is sent out.
		 * This call will increment packets_out.
		 */
///最终在这个函数中启动重传定时器。
		tcp_event_new_data_sent(sk, skb);

		tcp_minshall_update(tp, mss_now, skb);
		sent_pkts++;

		if (push_one)
			break;
	}
...........................
}


现在我们来看tcp_event_new_data_sent,如何启动定时器的.

static void tcp_event_new_data_sent(struct sock *sk, struct sk_buff *skb)
{
	struct tcp_sock *tp = tcp_sk(sk);
	unsigned int prior_packets = tp->packets_out;

	tcp_advance_send_head(sk, skb);
	tp->snd_nxt = TCP_SKB_CB(skb)->end_seq;

	/* Don't override Nagle indefinately with F-RTO */
	if (tp->frto_counter == 2)
		tp->frto_counter = 3;
///关键在这里.
	tp->packets_out += tcp_skb_pcount(skb);
	if (!prior_packets)
		inet_csk_reset_xmit_timer(sk, ICSK_TIME_RETRANS,
					  inet_csk(sk)->icsk_rto, TCP_RTO_MAX);
}


可以看到只有当prior_packets为0时才会重启定时器,而prior_packets则是发送未确认的段的个数,也就是说如果发送了很多段,如果前面的段没有确认,那么后面发送的时候不会重启这个定时器.

我们要知道,定时器的间隔是通过rtt来得到的,具体的算法,可以看下tcp/ip详解。

当启动了重传定时器,我们就会等待ack的到来,如果超时还没到来,那么就调用重传定时器的回调函数,否则最终会调用tcp_rearm_rto来删除或者重启定时器,这个函数是在tcp_ack()->tcp_clean_rtx_queue()中被调用的。tcp_ack是专门用来处理ack。


这个函数很简单,就是通过判断packets_out,这个值表示当前还未确认的段的