TCP TIME_WAIT定时器

注:本文分析基于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);
}
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值