注:本文分析基于3.10.107内核版本
其实TIME_WAIT定时器是挺奇怪的,从名字上来看,是在socket进入TIME_WAIT之后工作的,但实际上并不是,或者说不全是,因为在FIN_WAIT2上也会使用TIME_WAIT定时器,正如之前讲述FIN_WAIT2定时器所描述。
下面我们就来谈一谈TIME_WAIT定时器。
注册
其实TIME_WAIT定时器由两个定时器构成,在全局变量tcp_death_row中定义
struct inet_timewait_death_row tcp_death_row = {
.sysctl_max_tw_buckets = NR_FILE * 2,
.period = TCP_TIMEWAIT_LEN / INET_TWDR_TWKILL_SLOTS,
.death_lock = __SPIN_LOCK_UNLOCKED(tcp_death_row.death_lock),
.hashinfo = &tcp_hashinfo,
.tw_timer = TIMER_INITIALIZER(inet_twdr_hangman, 0,
(unsigned long)&tcp_death_row),
.twkill_work = __WORK_INITIALIZER(tcp_death_row.twkill_work,
inet_twdr_twkill_work),
/* Short-time timewait calendar */
.twcal_hand = -1,
.twcal_timer = TIMER_INITIALIZER(inet_twdr_twcal_tick, 0,
(unsigned long)&tcp_death_row),
};
分别是慢速定时器tw_timer和再生定时器twcal_timer。其他的我们先按下不表,容后道来,我们先看下有哪些情况会使用到TIME_WAIT定时器。
触发
关于TIME_WAIT定时器的触发,一共有5个场景。其实可以通过查询tcp_time_wait()函数的调用就能看到是哪5个场景。
1、在FIN_WAIT2状态下调用close()。但是我不太明白,socket是怎么进入FIN_WAIT2状态的,正常不就是调用close()吗,为什么还能在FIN_WAIT2状态再调用close()?求大神指教。
void tcp_close(struct sock *sk, long timeout)
{
...
if (sk->sk_state == TCP_FIN_WAIT2) {
struct tcp_sock *tp = tcp_sk(sk);
if (tp->linger2 < 0) {//使用linger2选项
tcp_set_state(sk, TCP_CLOSE);
tcp_send_active_reset(sk, GFP_ATOMIC);//直接发送RST报文
NET_INC_STATS_BH(sock_net(sk),
LINUX_MIB_TCPABORTONLINGER);
} else {
const int tmo = tcp_fin_time(sk);
if (tmo > TCP_TIMEWAIT_LEN) {
inet_csk_reset_keepalive_timer(sk,
tmo - TCP_TIMEWAIT_LEN);
} else {
tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
goto out;
}
}
}
...
}
2、这个场景是比较正常的,在FIN_WAIT2状态下收到对端的FIN报文,发送完最后一个ACK报文之后就进入TIME_WAIT状态。
static void tcp_fin(struct sock *sk)
{
...
case TCP_FIN_WAIT2:
/* Received a FIN -- send ACK and enter TIME_WAIT. */
tcp_send_ack(sk);//发送ACK报文,即第四挥手的最后一个报文
tcp_time_wait(sk, TCP_TIME_WAIT, 0);
break;
...
}
3、这个场景是在FIN_WAIT2状态下工作的。在接收到对端对FIN报文的回复后,且tcp_fin_timeout的值<=60s的时候,会启动TIME_WAIT定时器。由于tcp_fin_timeout的值默认就为60s,所以一般情况下,在FIN_WAIT2下工作的是TIME_WAIT定时器。是不是觉得有点怪怪的,但事实就是这样,而且更怪的还在后面。
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
...
case TCP_FIN_WAIT1:
...
tcp_set_state(sk, TCP_FIN_WAIT2);//设置socket状态为FIN_WAIT2
...
if (!sock_flag(sk, SOCK_DEAD))
...
else {
...
//TCP_TIMEWAIT_LEN的值为60HZ,即60s
if (tmo > TCP_TIMEWAIT_LEN) {
//这里便是FIN_WAIT2定时器的激活
inet_csk_reset_keepalive_timer(sk, tmo - TCP_TIMEWAIT_LEN);
} else if (th->fin || sock_owned_by_user(sk)) {
...
} else {
tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);//这里是进入TIME_WAIT定时器了
goto discard;
}
}
}
break;
...
}
}
...
}
4、这个场景就是在客户端和服务端同时关闭的时候触发的,双发都进入CLOSING状态,此时收到对端的FIN-ACK报文,就进入TIME_WAIT状态。
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
const struct tcphdr *th, unsigned int len)
{
...
case TCP_CLOSING:
if (tp->snd_una == tp->write_seq) {
tcp_time_wait(sk, TCP_TIME_WAIT, 0);
goto discard;
}
break;
...
}
5、这个场景就是刚才说的更怪的了,它和FIN_WAIT2定时器一前一后,一起工作。它是在FIN_WAIT2定时器的超时函数里触发,同时要求tcp_fin_timeout的值大于60s。而且,超时时间是(tmo - 60),和FIN_WAIT2定时器的超时时间一样。也就是同一个超时时间先后在两个定时器里使用。不知道为何这么使用,有何用意,请大伙赐教。
static void tcp_keepalive_timer (unsigned long data)
{
...
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;
}
...
}
定时器操作
理完这些触发场景,我们来看看tcp_time_wait()到底做了啥。
/*
* Move a socket to time-wait or dead fin-wait-2 state.
*/
void tcp_time_wait(struct sock *sk, int state, int timeo)
{
struct inet_timewait_sock *tw = NULL;
const struct inet_connection_sock *icsk = inet_csk(sk);
const struct tcp_sock *tp = tcp_sk(sk);
bool recycle_ok = false;
//判断是否开启快速回收标识,由/proc/sys/net/ipv4/tcp_tw_recycle参数决定,默认不开启,值为0
if (tcp_death_row.sysctl_tw_recycle && tp->rx_opt.ts_recent_stamp)
recycle_ok = tcp_remember_stamp(sk);
//这是会重新分配一个tw sock,替代原先socket处理旧连接中的包,阻止其危害新连接
//是否能重新分配取决于此时这个哈希桶长度是否超标,上限由sysctl_max_tw_buckets决定
//该参数即为/proc/sys/net/ipv4/tcp_max_tw_buckets,默认值为65536
if (tcp_death_row.tw_count < tcp_death_row.sysctl_max_tw_buckets)
tw = inet_twsk_alloc(sk, state);
if (tw != NULL) {
struct tcp_timewait_sock *tcptw = tcp_twsk((struct sock *)tw);
//此时超时时间为3.5倍icsk_rto,icsk_rto在初始化时是1HZ,即1s
//不过该值会随着网络状况动态调整
const int rto = (icsk->icsk_rto << 2) - (icsk->icsk_rto >> 1);
struct inet_sock *inet = inet_sk(sk);
//将旧连接的一些状态值传递给新分配的sock结构
tw->tw_transparent = inet->transparent;
tw->tw_rcv_wscale = tp->rx_opt.rcv_wscale;
tcptw->tw_rcv_nxt = tp->rcv_nxt;
tcptw->tw_snd_nxt = tp->snd_nxt;
....
//将tw sock放入time_wait hash表和bind hash表中,
//将sk从ESTABLISHED hash表中移除
__inet_twsk_hashdance(tw, sk, &tcp_hashinfo);
/* Get the TIME_WAIT timeout firing. */
//保证timeo最少为3.5RTO
if (timeo < rto)
timeo = rto;
if (recycle_ok) {//系统默认为0
tw->tw_timeout = rto;
} else {
tw->tw_timeout = TCP_TIMEWAIT_LEN;
//可见由TIME_WAIT状态进来的场景,timeo都会被设置为TCP_TIMEWAIT_LEN(60)
if (state == TCP_TIME_WAIT)
timeo = TCP_TIMEWAIT_LEN;
}
//这是个重点函数,会根据超时时间注册慢速定时器或者再生定时器
inet_twsk_schedule(tw, &tcp_death_row, timeo,
TCP_TIMEWAIT_LEN);
inet_twsk_put(tw);
} else {
/* Sorry, if we're out of memory, just CLOSE this
* socket up. We've got bigger problems than
* non-graceful socket closings.
*/
NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPTIMEWAITOVERFLOW);
}
tcp_update_metrics(sk);
tcp_done(sk);
}
可以看到,socket进入TIME_WAIT状态后,协议栈生成了一个tw sock代替socket存放在hash表中。tw sock占用空间比socket小,能从一定程度上节约内存空间。而定时器的触发设置都在inet_twsk_schedule()函数。
void inet_twsk_schedule(struct inet_timewait_sock *tw,
struct inet_timewait_death_row *twdr,
const int timeo, const int timewait_len)
{
struct hlist_head *list;
int slot;
//根据超时时间将tw sock划分到不同slot管理,其中INET_TWDR_RECYCLE_TICK和系统HZ有关
slot = (timeo + (1 << INET_TWDR_RECYCLE_TICK) - 1) >> INET_TWDR_RECYCLE_TICK;
spin_lock(&twdr->death_lock);
if (inet_twsk_del_dead_node(tw))//如果已经有这个连接,删除
twdr->tw_count--;
else
atomic_inc(&tw->tw_refcnt);
if (slot >= INET_TWDR_RECYCLE_SLOTS) {//进入慢速定时器
/* Schedule to slow timer */
if (timeo >= timewait_len) {//超时时间最大为timewait_len,即60s
slot = INET_TWDR_TWKILL_SLOTS - 1;
} else {
slot = DIV_ROUND_UP(timeo, twdr->period);//slot索引号向上取整
if (slot >= INET_TWDR_TWKILL_SLOTS)
slot = INET_TWDR_TWKILL_SLOTS - 1;
}
tw->tw_ttd = jiffies + timeo;
//使用循环队列的方式,将tw sock按照超时时间分配到各个slot上
slot = (twdr->slot + slot) & (INET_TWDR_TWKILL_SLOTS - 1);
list = &twdr->cells[slot];
} else {
tw->tw_ttd = jiffies + (slot << INET_TWDR_RECYCLE_TICK);
if (twdr->twcal_hand < 0) {//遍历标识位,之前未开启定时器,或者定时器已失效
twdr->twcal_hand = 0;
twdr->twcal_jiffie = jiffies;//记录定时器启动时间
twdr->twcal_timer.expires = twdr->twcal_jiffie +
(slot << INET_TWDR_RECYCLE_TICK);//设置超时时间
add_timer(&twdr->twcal_timer);//启动再生定时器
} else {
//将定时器的超时时间定为最早超时的tw sock的超时时间,避免多余的超时动作
if (time_after(twdr->twcal_timer.expires,
jiffies + (slot << INET_TWDR_RECYCLE_TICK)))
mod_timer(&twdr->twcal_timer,
jiffies + (slot << INET_TWDR_RECYCLE_TICK));
//同样使用循环队列的方式,将tw sock按照超时时间分配到各个slot上
slot = (twdr->twcal_hand + slot) & (INET_TWDR_RECYCLE_SLOTS - 1);
}
list = &twdr->twcal_row[slot];
}
hlist_add_head(&tw->tw_death_node, list);
if (twdr->tw_count++ == 0)//启动慢速定时器,定时周期为twdr->period(7.5s)
mod_timer(&twdr->tw_timer, jiffies + twdr->period);
spin_unlock(&twdr->death_lock);
}
由上可知,慢速定时器和再生定时器的管理都是一样的,利用循环队列的方式,将tw sock按照超时时间挂在不同超时时间的slot上。不同的地方主要有以下几点:
1、管理的tw sock的超时时间
由上可知,slot和INET_TWDR_RECYCLE_TICK有关,它的定义和系统时钟中断HZ相关,定义如下:
#define INET_TWDR_RECYCLE_SLOTS_LOG 5
#define INET_TWDR_RECYCLE_SLOTS (1 << INET_TWDR_RECYCLE_SLOTS_LOG)
/*
* If time > 4sec, it is "slow" path, no recycling is required,
* so that we select tick to get range about 4 seconds.
*/
#if HZ <= 16 || HZ > 4096
# error Unsupported: HZ <= 16 or HZ > 4096
#elif HZ <= 32
# define INET_TWDR_RECYCLE_TICK (5 + 2 - INET_TWDR_RECYCLE_SLOTS_LOG)
#elif HZ <= 64
# define INET_TWDR_RECYCLE_TICK (6 + 2 - INET_TWDR_RECYCLE_SLOTS_LOG)
#elif HZ <= 128
# define INET_TWDR_RECYCLE_TICK (7 + 2 - INET_TWDR_RECYCLE_SLOTS_LOG)
#elif HZ <= 256
# define INET_TWDR_RECYCLE_TICK (8 + 2 - INET_TWDR_RECYCLE_SLOTS_LOG)
#elif HZ <= 512
# define INET_TWDR_RECYCLE_TICK (9 + 2 - INET_TWDR_RECYCLE_SLOTS_LOG)
#elif HZ <= 1024
# define INET_TWDR_RECYCLE_TICK (10 + 2 - INET_TWDR_RECYCLE_SLOTS_LOG)
#elif HZ <= 2048
# define INET_TWDR_RECYCLE_TICK (11 + 2 - INET_TWDR_RECYCLE_SLOTS_LOG)
#else
# define INET_TWDR_RECYCLE_TICK (12 + 2 - INET_TWDR_RECYCLE_SLOTS_LOG)
#endif
在CentOS 7上HZ=1000,在SUSE11SP3上HZ=250。因此对应INET_TWDR_RECYCLE_TICK 分别为7和5。这里我们以7为例说明。
进入慢速定时器或再生定时器的分界点为
#define INET_TWDR_RECYCLE_SLOTS_LOG 5
#define INET_TWDR_RECYCLE_SLOTS (1 << INET_TWDR_RECYCLE_SLOTS_LOG)
slot >= INET_TWDR_RECYCLE_SLOTS
即slot >= 32,逆推回去,可得tmo = [31*2^7, 32*2^7),也就是[3968, 4096)jiffies。对于HZ=1000的CentOS上,即约为4s。因此tw sock超时时间大于4s,进入慢速定时器,反之进入再生定时器。
2、管理tw sock队列的slot长度
从两个定时器slot索引的增长便可看出两个队列的长度,
#define INET_TWDR_TWKILL_SLOTS 8
#define INET_TWDR_RECYCLE_SLOTS_LOG 5
#define INET_TWDR_RECYCLE_SLOTS (1 << INET_TWDR_RECYCLE_SLOTS_LOG)
//慢速定时器
slot = (twdr->slot + slot) & (INET_TWDR_TWKILL_SLOTS - 1);
//再生定时器
slot = (twdr->twcal_hand + slot) & (INET_TWDR_RECYCLE_SLOTS - 1);
因此可知,慢速定时器的slot长度为8,再生定时器的slot长度为32。
3、定时器的周期
慢速定时器的周期为twdr->period,twdr即为tcp_death_row全局变量,文章开始有列出,可知
#define INET_TWDR_TWKILL_SLOTS 8
.period = TCP_TIMEWAIT_LEN / INET_TWDR_TWKILL_SLOTS
因此慢速定时器的周期为60/8=7.5s。而再生定时器的超时时间是不定的,会随着tw sock的超时时间而改变。
if (time_after(twdr->twcal_timer.expires, jiffies + (slot << INET_TWDR_RECYCLE_TICK)))
mod_timer(&twdr->twcal_timer, jiffies + (slot << INET_TWDR_RECYCLE_TICK));
可见,再生定时器的超时时间被设置为最近一个tw sock的超时时间。但是这个仅针对最近一次的超时时间,除此之外,再生定时器和慢速定时器一样,都以slot间隔时间作为定时器超时时间,所以再生定时器超时时间为2^INET_TWDR_RECYCLE_TICK=2^7=128ms。
4.每个slot相隔的时间差
对于慢速定时器,
slot = DIV_ROUND_UP(timeo, twdr->period);
可知每个slot的间隔即为twdr->period=7.5s,而对于再生定时器,
slot = (timeo + (1 << INET_TWDR_RECYCLE_TICK) - 1) >> INET_TWDR_RECYCLE_TICK;
即为2^INET_TWDR_RECYCLE_TICK,即2^7=128jiffies,即128ms。
由slot管理带来的影响是,实际超时时间和设置的时间有差异,某个区间内的超时时间差异会被归一到同个slot,即同个超时时间。比如 ,如果超时时间设置为10s,进入慢速定时器,slot=10/7.5–>取整为2,而如果超时时间为8s也是同样的效果。另外此时slot作为数组下标,因此实际在第三个slot,实际超时时间为3*7.5=22.5s。同样,再生定时器也是如此,只不过其slot的间隔较小,因此差异相比于慢速定时器要小很多。
超时处理函数
从全局变量tcp_death_row中定义可知,慢速定时器的超时处理函数为inet_twdr_hangman(),再生定时器的超时处理函数为inet_twdr_twcal_tick()。先看慢速定时器的处理。
void inet_twdr_hangman(unsigned long data)
{
struct inet_timewait_death_row *twdr;
unsigned int need_timer;
twdr = (struct inet_timewait_death_row *)data;
spin_lock(&twdr->death_lock);
if (twdr->tw_count == 0)
goto out;
need_timer = 0;
if (inet_twdr_do_twkill_work(twdr, twdr->slot)) {
//如果这个slot超时的tw sock过多(大于100),放到工作队列操作
twdr->thread_slots |= (1 << twdr->slot);
schedule_work(&twdr->twkill_work);//启动工作队列
need_timer = 1;
} else {
/* We purged the entire slot, anything left? */
if (twdr->tw_count)//如果还有tw sock,需要定时器继续超时
need_timer = 1;
twdr->slot = ((twdr->slot + 1) & (INET_TWDR_TWKILL_SLOTS - 1));//下个slot
}
if (need_timer)//重置定时器,周期仍为twdr->period(7.5s)
mod_timer(&twdr->tw_timer, jiffies + twdr->period);
out:
spin_unlock(&twdr->death_lock);
}
#define INET_TWDR_TWKILL_QUOTA 100
/* Returns non-zero if quota exceeded. */
static int inet_twdr_do_twkill_work(struct inet_timewait_death_row *twdr,
const int slot)
{
struct inet_timewait_sock *tw;
unsigned int killed;
int ret;
killed = 0;
ret = 0;
rescan:
inet_twsk_for_each_inmate(tw, &twdr->cells[slot]) {//遍历每个tw sock
__inet_twsk_del_dead_node(tw);
spin_unlock(&twdr->death_lock);
//将tw sock从establish 和bind hash表中删除
__inet_twsk_kill(tw, twdr->hashinfo);
#ifdef CONFIG_NET_NS
NET_INC_STATS_BH(twsk_net(tw), LINUX_MIB_TIMEWAITED);
#endif
inet_twsk_put(tw);
killed++;
spin_lock(&twdr->death_lock);
//释放的tw sock数量大于100,返回,后续的释放交给工作队列执行
if (killed > INET_TWDR_TWKILL_QUOTA) {
ret = 1;
break;
}
goto rescan;
}
twdr->tw_count -= killed;
#ifndef CONFIG_NET_NS
NET_ADD_STATS_BH(&init_net, LINUX_MIB_TIMEWAITED, killed);
#endif
return ret;
}
总体就是每隔7.5s遍历一次slot队列,因为slot的时间间隔也是7.5s,所以定时器超时后,这个slot的tw sock肯定是都已经超时,删除就好。
同样,对于再生定时器操作也差不多,只不多定时器周期不一样。
void inet_twdr_twcal_tick(unsigned long data)
{
struct inet_timewait_death_row *twdr;
int n, slot;
unsigned long j;
unsigned long now = jiffies;
int killed = 0;
int adv = 0;
twdr = (struct inet_timewait_death_row *)data;
spin_lock(&twdr->death_lock);
if (twdr->twcal_hand < 0)
goto out;
slot = twdr->twcal_hand;//获取上次遍历到的位置
j = twdr->twcal_jiffie;
//不知道为什么这里还要遍历一次,很明显定时器超时后当前slot的tw sock肯定超时,和慢速定时器一样
//如果当前正好是最后一个slot,那不是还要白白比较前面31个slot吗,想不通
for (n = 0; n < INET_TWDR_RECYCLE_SLOTS; n++) {
if (time_before_eq(j, now)) {//已超时,话说定时器进来不就已经超时了吗。。。。
struct hlist_node *safe;
struct inet_timewait_sock *tw;
inet_twsk_for_each_inmate_safe(tw, safe,
&twdr->twcal_row[slot]) {//这里就是遍历slot的tw sock了
__inet_twsk_del_dead_node(tw);
//将tw sock从establish和bind hash表中删除,其实超时后对tw sock的处理和慢速定时器一样
__inet_twsk_kill(tw, twdr->hashinfo);
#ifdef CONFIG_NET_NS
NET_INC_STATS_BH(twsk_net(tw), LINUX_MIB_TIMEWAITKILLED);
#endif
inet_twsk_put(tw);
killed++;
}
} else {
if (!adv) {
adv = 1;
twdr->twcal_jiffie = j;//还没超时,更新时间
twdr->twcal_hand = slot;//记录尚未超时的slot的位置
}
if (!hlist_empty(&twdr->twcal_row[slot])) {//不为空定时器继续工作
mod_timer(&twdr->twcal_timer, j);
goto out;
}
}
//更新j的时间为下个slot超时时间,用于定时器超时时间设置
//所以再生定时器的周期为2^7=128ms
j += 1 << INET_TWDR_RECYCLE_TICK;
slot = (slot + 1) & (INET_TWDR_RECYCLE_SLOTS - 1);
}
twdr->twcal_hand = -1;//如果遍历到最后说明所有tw sock已经释放,标识再生定时器失效
out:
if ((twdr->tw_count -= killed) == 0)//没有tw sock,定时器就没必要工作了,删除定时器
del_timer(&twdr->tw_timer);
#ifndef CONFIG_NET_NS
NET_ADD_STATS_BH(&init_net, LINUX_MIB_TIMEWAITKILLED, killed);
#endif
spin_unlock(&twdr->death_lock);
}