以前只是注意TCP连接建立时经历的经典的“三次握手”,而对于连接的关闭关注较少,最近看了一下关闭的流程,比建立更为复杂。这个其实也不值得大惊小怪,因为free往往要比malloc复杂,因为free可能要处理释放块的合并。其中比较特殊的有一个time_wait状态,从RFC说明中看,该状态是断链主动发起方必经的最后一个状态,除非是中间网络不通而出现重传之后超时而直接出现错误。对于内核的实现来说,这个状态是一个自回收状态,也就是内核会使用特定的数据结构将系统中所有的该状态套接口接管起来,并等待合理时机进行销毁和回收。
按照理想情况,当拆链发起方收到对方拆链报文FIN并回应ACK之后进入time_wait状态,该状态历时2MSL(Max Segmentation Lifetime)时间,然后从系统中消失。该状态的存在是为了避免自己发送的FIN报文ACK丢失而可能引发的问题:
1、直接关闭套接口。由于对方没有收到对FIN的ACK,所以它会重传FIN,如果拆链主动方套接口已经被释放,此时协议栈会发送一个非常不友好的RESET报文,从而导致对方“不优雅”的结束。
2、套接口复用。假设说主动方没有执行close回收套接口,而是直接在关闭的链路上再次执行connect来重新启动一个连接(这种锲而不舍的精神值得学习,但不提倡),此时重传的FIN同样会导致这个连接直接关闭。
二、time_wait状态入口函数
当协议栈上层觉得套接口应该进入该状态时,它就调用linux-2.6.21\net\ipv4\tcp_minisocks.c:void tcp_time_wait(struct sock *sk, int state, int timeo)
接口来完成状态转换。该函数中相关核心代码摘录如下:
if (tcp_death_row.tw_count < tcp_death_row.sysctl_max_tw_buckets)
tw = inet_twsk_alloc(sk, state);
……
if (tw != NULL) {
……
/* Linkage updates. */
__inet_twsk_hashdance(tw, sk, &tcp_hashinfo);
……
inet_twsk_schedule(tw, &tcp_death_row, timeo,
TCP_TIMEWAIT_LEN);
tcp_update_metrics(sk);
tcp_done(sk);
三、time_wait套接口分配
inet_twsk_alloc完成对一个inet_timewait_sock套接口的分配,它使用了自己的slab来管理这种类型的结构。当inet_timewait_sock分配之后,inet_twsk_alloc还会将即将进入time_wait状态的原始套接口的大部分信息赋值给新的inet_timewait_sock套接口,其中包含了本地地址、对方地址、双方端口号等信息,相当于进程中的“僵尸”套接口,只是比僵尸进程功能大一些,但是功能已经非常单薄,例如,它没有重传定时器、发送队列之类的结构。
{
struct inet_timewait_sock *tw =
kmem_cache_alloc(sk->sk_prot_creator->twsk_prot->twsk_slab,
GFP_ATOMIC);
if (tw != NULL) {
const struct inet_sock *inet = inet_sk(sk);
/* Give us an identity. */
tw->tw_daddr = inet->daddr;
tw->tw_rcv_saddr = inet->rcv_saddr;
tw->tw_bound_dev_if = sk->sk_bound_dev_if;
tw->tw_num = inet->num;
tw->tw_state = TCP_TIME_WAIT;
tw->tw_substate = state;
tw->tw_sport = inet->sport;
tw->tw_dport = inet->dport;
tw->tw_family = sk->sk_family;
……
}
}
四、套接口“舞动”
该操作主要在__inet_twsk_hashdance函数中完成,名字中有一个很文艺的单词“dance”,而这个dance就是跳舞的意思,也就是这个套接口将会从一个“建立态”舞动到“time_wait”态。
1、established链表中删除
/* Step 2: Remove SK from established hash. */
if (__sk_del_node_init(sk))
sock_prot_dec_use(sk->sk_prot);
/* Step 3: Hash TW into TIMEWAIT chain. */
inet_twsk_add_node(tw, &ehead->twchain);
正如注释所说,其中的__sk_del_node_init将会完成将一个套接口从tcp_hashinfo的ehash链表中删除,这样系统中将不能从established链表中找到该
套接口,注意到的是tcp_check_req-->>>tcp_v4_syn_recv_sock--->>__inet_hash--->>__sk_add_node
static __inline__ void __sk_add_node(struct sock *sk, struct hlist_head *list)
{
hlist_add_head(&sk->sk_node, list);
}
中使用的sk_sk_node和__inet_twsk_hashdance--->>>__sk_del_node_init--->>>__sk_del_node
static __inline__ void __sk_del_node(struct sock *sk)
{
__hlist_del(&sk->sk_node);
}
中使用的是相同的sk->sk_node,所以__inet_twsk_hashdance中执行该操作之后,established链表中不再有该套接口。
2、time_wait链表中添加
static inline void inet_twsk_add_node(struct inet_timewait_sock *tw,
struct hlist_head *list)
{
hlist_add_head(&tw->tw_node, list);
}
我们注意到,inet_hashinfo结构中的struct inet_ehash_bucket *ehash;变量不仅有建链链表,还有一个专门的time_wait链表
struct inet_ehash_bucket {
rwlock_t lock;
struct hlist_head chain;
struct hlist_head twchain;
};
当TCP查找一个套接口的时候,它会依次变量这两个队列。tcp_v4_rcv--->>>__inet_lookup--->>__inet_lookup_established
/* Optimize here for direct hit, only listening connections can
* have wildcards anyways.
*/
unsigned int hash = inet_ehashfn(daddr, hnum, saddr, sport);
struct inet_ehash_bucket *head = inet_ehash_bucket(hashinfo, hash);
prefetch(head->chain.first);
read_lock(&head->lock);
sk_for_each(sk, node, &head->chain) {首先搜索已经建链队列。
if (INET_MATCH(sk, hash, acookie, saddr, daddr, ports, dif))
goto hit; /* You sunk my battleship! */
}
/* Must check for a TIME_WAIT'er before going to listener hash. */
sk_for_each(sk, node, &head->twchain) {这里同样会搜索time_wait队列。
if (INET_TW_MATCH(sk, hash, acookie, saddr, daddr, ports, dif))
goto hit;
}
所以,当报文进入系统时,可以从__inet_lookup中找到态time_wait套接口。
五、time_wait套接口何时删除
linux-2.6.21\net\ipv4\tcp_minisocks.c文件中定义的全局变量struct inet_timewait_death_row tcp_death_row 会统一接管系统中的这种time_wait套接口,它将会负责这些套接口的数量限制、删除、释放等操作。
struct inet_timewait_death_row tcp_death_row = {这里的death row在英语中为“(*的)死囚区”,也就是即将消失的连接套接口。
.sysctl_max_tw_buckets = NR_FILE * 2,
.period = TCP_TIMEWAIT_LEN / INET_TWDR_TWKILL_SLOTS,编译时常量值,7.5s。
.death_lock = __SPIN_LOCK_UNLOCKED(tcp_death_row.death_lock),
.hashinfo = &tcp_hashinfo,
.tw_timer = TIMER_INITIALIZER(inet_twdr_hangman, 0, 其中hangman在英语中为“绞刑吏,刽子手”,可见作者还是一个很文艺的程序员,这一点和我很像^-^。
(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),
};
1、纳入管理结构
在tcp_time_wait--->>inet_twsk_schedule函数中将会把新创建的time_wait套接口托付给tcp_death_row结构,也正是在该函数中完成对time_wait态的托付接收。
在tcp_death_row中存在两种回收机制,一种是针对该状态需要保持时间较长的套接口,它们放入twcal_timer定时器中,另一种是等待时间较短的队列,它的定时单位并不是固定的秒数,而是HZ,但是作者也说尽量保持在一个合理值内,短时队列的单位为1<<INET_TWDR_RECYCLE_TICK个tick,而INET_TWDR_RECYCLE_TICK定义为
#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)
假设系统HZ为32, INET_TWDR_RECYCLE_TICK 值为2,因此一个周期时间为1<<2=4个tick,总共(1<<5)*4/HZ=4s,这也就是注释中所说的4s的来历。总起来说,就是如果time_wait时间小于4s,那么放入tcp_death_row变量的twcal_timer定时器中,该定时器分辨率为4/32s;当time_wait时间大于4s时,放入tcp_death_row中tw_timer队列中,该对立分辨率为TCP_TIMEWAIT_LEN / INET_TWDR_TWKILL_SLOTS=7.5s。由此可见,这个状态时间并不是那么精确的。
time_wait套接口通过套接口中的tw_death_node成员连接入待收割队列
hlist_add_head(&tw->tw_death_node, list);
2、入队流程简单分析
slot = (timeo + (1 << INET_TWDR_RECYCLE_TICK) - 1) >> INET_TWDR_RECYCLE_TICK;该操作以1/8秒为单位向上取整
if (slot >= INET_TWDR_RECYCLE_SLOTS) {如果大于4s,在twdr->cell中寻找合适位置。
/* Schedule to slow timer */
if (timeo >= timewait_len) {
slot = INET_TWDR_TWKILL_SLOTS - 1;
} else {
slot = (timeo + twdr->period - 1) / twdr->period;period为7.5s(同样是tick形式),以该分辨率为单位向上取整。
if (slot >= INET_TWDR_TWKILL_SLOTS)
slot = INET_TWDR_TWKILL_SLOTS - 1;
}
tw->tw_ttd = jiffies + timeo;
slot = (twdr->slot + slot) & (INET_TWDR_TWKILL_SLOTS - 1);找到合适槽位,由于最多8个槽位,所以所有time_wait不会超过TCP_TIMEWAIT_LEN=60s。
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 {
if (time_after(twdr->twcal_timer.expires,
jiffies + (slot << INET_TWDR_RECYCLE_TICK)))
mod_timer(&twdr->twcal_timer,
jiffies + (slot << INET_TWDR_RECYCLE_TICK));
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)
mod_timer(&twdr->tw_timer, jiffies + twdr->period);
spin_unlock(&twdr->death_lock);
}
3、定时器老化
当time_wait套接口放到合适的位置之后,它们就等待定时器到来之后被定时器处理函数收割掉了,也就是说,它放置的位置决定了它何时被收割,这个也正是inet_twsk_schedule函数的作用。两个定时器的处理函数分别为inet_twdr_hangman和inet_twdr_twcal_tick,因为它们只是到指定的位置收割节点,所以比较简单,只是长时队列中稍微复杂一些,这里说明一下:
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;
if (inet_twdr_do_twkill_work(twdr, twdr->slot)) {这里的inet_twdr_do_twkill_work函数里有一个判断,如果收割的节点数大于INET_TWDR_TWKILL_QUOTA=100的话就返回1,然后启动工作队列来完成之后可能剩余的节点收割,这样可以避免在定时器中做过多操作,因为定时器是在高优先级的软中断环境中执行的。
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)
need_timer = 1;
}
twdr->slot = ((twdr->slot + 1) & (INET_TWDR_TWKILL_SLOTS - 1));
if (need_timer)
mod_timer(&twdr->tw_timer, jiffies + twdr->period);
4、真正回收操作
两个定时器回收节点都是通过__inet_twsk_kill函数来完成,真正的结构内存回收路径为
__inet_twsk_kill--->>>inet_twsk_put--->>>kmem_cache_free(tw->tw_prot->twsk_prot->twsk_slab, tw)
该操作和tcp_time_wait--->>>inet_twsk_alloc--->>kmem_cache_alloc(sk->sk_prot_creator->twsk_prot->twsk_slab,GFP_ATOMIC)相对应。两者虽然看起来不同,但是值是相同的,在inet_twsk_alloc函数中。
tw->tw_prot = sk->sk_prot_creator;