最近在做操作系统升级时,发现升级后的系统处于TIME_WAIT状态的连接数明显增多(内核版本 2.6.18 -> 2.6.32)。
原因
2.6.18 与 2.6.32 的 diff
结果
net/ipv4/inet_timewait_sock.c
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
@@
-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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
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()。
以下是此数据结构的初始化:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
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。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
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块。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
|
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块释放的逻辑可参考下图:

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。