IT技术博客大学习 共学习 共进步
全部 移动开发 后端 数据库 AI 算法 安全 DevOps 前端 设计 开发者

Linux内核协议栈对于timewait状态的处理

UC技术博客 2013-08-21 13:26:08 累计浏览 4,846 次
本机暂存

   最近在做操作系统升级时,发现升级后的系统处于TIME_WAIT状态的连接数明显增多(内核版本 2.6.18 -> 2.6.32)。

原因

   2.6.18 与 2.6.32 的 diff 结果

   net/ipv4/inet_timewait_sock.c

@@ -178,15 +212,14 @@
      need_timer = 0;
      if (inet_twdr_do_twkill_work(twdr, twdr->slot)) {
        twdr->thread_slots |= (1 << twdr->slot);
    -   mb();
        schedule_work(&twdr->twkill_work);
        need_timer = 1;
      } else {
        /* We purged the entire slot, anything left?  */  
        if (twdr->tw_count)
          need_timer = 1;
        //这句话位置的变动引起TIME_WAIT状态增多
    +   twdr->slot = ((twdr->slot + 1) & (INET_TWDR_TWKILL_SLOTS - 1));
      }   
    - twdr->slot = ((twdr->slot + 1) & (INET_TWDR_TWKILL_SLOTS - 1));
      if (need_timer)
        mod_timer(&twdr->tw_timer, jiffies + twdr->period);
    out:

   导致TIME_WAIT状态增多,正是由于

   twdr->slot = ((twdr->slot + 1) & (INET_TWDR_TWKILL_SLOTS - 1));

   位置的改变。

具体分析

1. 关键数据结构

   inet_timewait_death_row: 用于管理timewait控制块的数据结构,位置: include/net/inet_timewait_sock.h。

struct inet_timewait_death_row {
    /* Short-time timewait calendar */
    int         twcal_hand;
    int         twcal_jiffie;
    struct timer_list   twcal_timer;
    struct hlist_head   twcal_row[INET_TWDR_RECYCLE_SLOTS];

    spinlock_t      death_lock;
    int         tw_count;
    int         period;
    u32         thread_slots;
    struct work_struct  twkill_work;
    struct timer_list   tw_timer;
    int         slot;
    //INET_TWDR_TWKILL_SLOTS 值为 8
    struct hlist_head   cells[INET_TWDR_TWKILL_SLOTS];
    struct inet_hashinfo    *hashinfo;
    int         sysctl_tw_recycle;
    int         sysctl_max_tw_buckets;
};

   此数据结构,可以分为两部分看,一部分处理 tw_recycle 开启时timewait块的快速回收,另一部分为未开启时用于等待时间较长的timewait块的回收。由于系统没有开启 tw_recycle , 因此我们主要关注等待时间较长的timewait块回收。

   用于等待时间较长的主要成员变量:

   int period : tw_timer 定时器的超时时间固定值为 TCP_TIMEWAIT_LEN / INET_TWDR_TWKILL_SLOTS,其中 TCP_TIMEWAIT_LEN 为 60 * HZ (60s),INET_TWDR_TWKILL_SLOTS 为 8。

   u32 thread_slots : 用于标识未完成的timewait块的位图。

   struct work_struct twkill_work : 分批删除(默认值为每次删除100个)cells中timewait块时的工作队列。

   struct timer_list tw_timer : 定时器,每过 period,触发一次 inet_twdr_hangman()

   以下是此数据结构的初始化:

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,
                 &tcp_death_row),
    /* Short-time timewait calendar */

    .twcal_hand = -1,
    .twcal_timer = TIMER_INITIALIZER(inet_twdr_twcal_tick, 0,
                (unsigned long)&tcp_death_row),
};

   inet_timewait_sock: 用于组成 tcp_timewait_sock 结构,其前部是 sock_common 的前部。位置: include/net/inet_timewait_sock.h。

struct inet_timewait_sock {
    /*  
     * Now struct sock also uses sock_common, so please just
     * don't add nothing before this first member (__tw_common) --acme
     */
    struct sock_common  __tw_common;
#define tw_family   __tw_common.skc_family
#define tw_state    __tw_common.skc_state
#define tw_reuse    __tw_common.skc_reuse
#define tw_bound_dev_if   __tw_common.skc_bound_dev_if
#define tw_node     __tw_common.skc_node
#define tw_bind_node    __tw_common.skc_bind_node
#define tw_refcnt   __tw_common.skc_refcnt
#define tw_hash     __tw_common.skc_hash
#define tw_prot     __tw_common.skc_prot
    volatile unsigned char  tw_substate;
    /* 3 bits hole, try to pack */
    unsigned char   tw_rcv_wscale;
    /* Socket demultiplex comparisons on incoming packets. */
    /* these five are in inet_sock */
    __u16     tw_sport;
    __u32     tw_daddr __attribute__((aligned(INET_TIMEWAIT_ADDRCMP_ALIGN_BYTES)));
    __u32     tw_rcv_saddr;
    __u16     tw_dport;
    __u16     tw_num;
    /* And these are ours. */
    __u8      tw_ipv6only:1;
    /* 15 bits hole, try to pack */
    __u16     tw_ipv6_offset;
    int     tw_timeout;
    unsigned long   tw_ttd;
    struct inet_bind_bucket *tw_tb;
    struct hlist_node tw_death_node;
};

   此数据结构暂时只需要知道 tw_substate 即可。 tw_substate : TCP状态迁移到 FIN_WAIT2 或 TIME_WAIT 状态时,协议栈会用 timewait 块取代 tcp_sock 块,因为这两种状态都需要由定时器处理,超时立即释放。其对外状态都表现为 TIME_WAIT , 但其内部状态还是有分别,通过 tw_substate 进行区分。


2. timewait块释放时的逻辑

   inet_twdr_hangman() 此函数是定时器到期时执行的函数,用于释放timewait块。

void inet_twdr_hangman(unsigned long data)
{
    struct inet_timewait_death_row *twdr;
    int unsigned need_timer;

    twdr = (struct inet_timewait_death_row *)data;
    spin_lock(&twdr->death_lock);

    if (twdr->tw_count == 0)
        goto out;

    need_timer = 0;

    //inet_twdr_do_twkill_work 释放timewait块的具体函数,每次释放100个
    //释放完成返回0, 否则返回1
    if (inet_twdr_do_twkill_work(twdr, twdr->slot)) {
        //一次遍历未完全删除timewait块时,剩余的time块放入twkill_work的工作队列中处理。
        //thread_slots标识未完成的timewait块
        twdr->thread_slots |= (1 << twdr->slot);
        mb();
        schedule_work(&twdr->twkill_work);
        //未删除所有timewait块,需要重新调度定时器
        need_timer = 1;
    } else {
      /* We purged the entire slot, anything left?  */
      if (twdr->tw_count)
          need_timer = 1;
    }

    //此句是关键,代码出自2.6.18内核,不管定时器例程一次有没有释放完timewait块,都进行 + 1 操作
    twdr->slot = ((twdr->slot + 1) & (INET_TWDR_TWKILL_SLOTS - 1));
    if (need_timer)
        mod_timer(&twdr->tw_timer, jiffies + twdr->period);
  out:
      spin_unlock(&twdr->death_lock);
}

   用于慢timewait块释放的逻辑可参考下图:

   timewait


3. 结论

   按照 2.6.18 中的逻辑,如果一次没有全部删除一个slot中的timewait控制块, twdr->slot 仍然会执行 + 1 操作。此时如果有一个tcp_sock进入FIN_WAIT2状态,则此时的timewait(tw_substate = fin_wait2)块会被放在上一个slot中,而此时有一个线程正在处理那个队列,因此会导致处于FIN_WAIT2状态的timewait块被提前释放,若此时对端的FIN分节到达,协议栈会回复一个RST分节。

   为了修复此BUG,2.6.32 协议栈中修改了 twdr->slot + 1 的时机,每次必须完全释放一个slot中所有的timewait块后,才会进行 + 1 操作。这也就是说协议栈不保证在 TCP_TWKILL_PERIOD 周期内,移动一个格子,所以当系统繁忙时,会导致timewait块的等待时间大于 TCP_TIMEWAIT_LEN

同分类推荐文章

  1. 等了十年的 Go 链式管道,终于来了:seq 让你像写 Scala 一样写 Go (2026-06-25 18:38:18)
  2. Go 实验特性详解 (2026-06-21 10:05:27)
  3. amd64 微架构级别对 Go 程序性能提升多少? (2026-06-21 09:38:49)

查看更多 后端 文章 →

建议继续学习

  1. gen_tcp发送进程被挂起起因分析及对策 (累计阅读 37,821)
  2. TCP 的那些事儿(上) (累计阅读 22,696)
  3. Linux如何统计进程的CPU利用率 (累计阅读 16,307)
  4. 从输入 URL 到页面加载完成的过程中都发生了什么事情? (累计阅读 15,933)
  5. 我的 RHCA 之路 (累计阅读 14,012)
  6. Linux内存点滴 用户进程内存空间 (累计阅读 13,228)
  7. 自建DNS以防止GFW干扰 (累计阅读 13,125)
  8. 给程序员新手的一些建议 (累计阅读 13,088)
  9. Linux 性能监控、测试、优化工具 (累计阅读 13,011)
  10. 关于linux内存free的一些事情 (累计阅读 12,867)