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,这个值表示当前还未确认的段的