kk Blog —— 通用基础


date [-d @int|str] [+%s|"+%F %T"]
netstat -ltunp
sar -n DEV 1

TIME_WAIT状态下对接收到的数据包如何处理

http://www.educity.cn/linux/1605134.html

正常情况下主动关闭连接的一端在连接正常终止后,会进入TIME_WAIT状态,存在这个状态有以下两个原因(参考《Unix网络编程》):

《UNIX网络编程.卷2:进程间通信(第2版)》[PDF]下载

1、保证TCP连接关闭的可靠性。如果最终发送的ACK丢失,被动关闭的一端会重传最终的FIN包,如果执行主动关闭的一端没有维护这个连接的状态信息,会发送RST包响应,导致连接不正常关闭。

2、允许老的重复分组在网络中消逝。假设在一个连接关闭后,发起建立连接的一端(客户端)立即重用原来的端口、IP地址和服务端建立新的连接。老的连接上的分组可能在新的连接建立后到达服务端,TCP必须防止来自某个连接的老的重复分组在连接终止后再现,从而被误解为同一个连接的化身。要实现这种功能,TCP不能给处于TIME_WAIT状态的连接启动新的连接。TIME_WAIT的持续时间是2MSL,保证在建立新的连接之前老的重复分组在网络中消逝。这个规则有一个例外:如果到达的SYN的序列号大于前一个连接的结束序列号,源自Berkeley的实现将给当前处于TIME_WAIT状态的连接启动新的化身。

最初在看《Unix网络编程》 的时候看到这个状态,但是在项目中发现对这个状态的理解有误,特别是第二个理由。原本认为在TIME_WAIT状态下肯定不会再使用相同的五元组(协议类型,源目的IP、源目的端口号)建立一个新的连接,看书还是不认真啊!为了加深理解,决定结合内核代码,好好来看下内核在TIME_WAIT状态下的处理。其实TIME_WAIT存在的第二个原因的解释更多的是从被动关闭一方的角度来说明的。如果是执行主动关闭的是客户端,客户端户进入TIME_WAIT状态,假设客户端重用端口号来和服务器建立连接,内核会不会允许客户端来建立连接?内核如何来处理这种情况?书本中不会对这些点讲的那么详细,要从内核源码中来找答案。

我们先来看服务器段进入TIME_WAIT后内核的处理,即服务器主动关闭连接。TCP层的接收函数是tcp_v4_rcv(),和TIME_WAIT状态相关的主要代码如下所示:

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
38
39
40
41
42
43
int tcp_v4_rcv(struct sk_buff *skb)
{
	......

	sk = __inet_lookup_skb(&tcp_hashinfo, skb, th->source, th->dest);
	if (!sk)
		goto no_tcp_socket;
process:
	if (sk->sk_state == TCP_TIME_WAIT)
		goto do_time_wait;   
		......

discard_it:
	/* Discard frame. */
	kfree_skb(skb);
	return 0;
	......
do_time_wait:
	......

switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {
	case TCP_TW_SYN: {
		struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev),
							&tcp_hashinfo,
							iph->daddr, th->dest,
							inet_iif(skb));
		if (sk2) {
			inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);
			inet_twsk_put(inet_twsk(sk));
			sk = sk2;
			goto process;
		}
		/* Fall through to ACK */
	}
	case TCP_TW_ACK:
		tcp_v4_timewait_ack(sk, skb);
		break;
	case TCP_TW_RST:
		goto no_tcp_socket;
	case TCP_TW_SUCCESS:;
	}
	goto discard_it;
}

接收到SKb包后,会调用__inet_lookup_skb()查找对应的sock结构。如果套接字状态是TIME_WAIT状态,会跳转到do_time_wait标签处处理。从代码中可以看到,主要由tcp_timewait_state_process()函数来处理SKB包,处理后根据返回值来做相应的处理。

在看tcp_timewait_state_process()函数中的处理之前,需要先看一看不同的返回值会对应什么样的处理。

如果返回值是TCP_TW_SYN,则说明接收到的是一个“合法”的SYN包(也就是说这个SYN包可以接受),这时会首先查找内核中是否有对应的监听套接字,如果存在相应的监听套接字,则会释放TIME_WAIT状态的传输控制结构,跳转到process处开始处理,开始建立一个新的连接。如果没有找到监听套接字会执行到TCP_TW_ACK分支。

如果返回值是TCP_TW_ACK,则会调用tcp_v4_timewait_ack()发送ACK,然后跳转到discard_it标签处,丢掉数据包。

如果返回值是TCP_TW_RST,则会调用tcp_v4_send_reset()给对端发送RST包,然后丢掉数据包。

如果返回值是TCP_TW_SUCCESS,则会直接丢掉数据包。

接下来我们通过tcp_timewait_state_process()函数来看TIME_WAIT状态下的数据包处理。

为了方便讨论,假设数据包中没有时间戳选项,在这个前提下,tcp_timewait_state_process()中的局部变量paws_reject的值为0。

如果需要保持在FIN_WAIT_2状态的时间小于等于TCP_TIMEWAIT_LEN,则会从FIN_WAIT_2状态直接迁移到TIME_WAIT状态,也就是使用描述TIME_WAIT状态的sock结构代替当前的传输控制块。虽然这时的sock结构处于TIME_WAIT结构,但是还要区分内部状态,这个内部状态存储在inet_timewait_sock结构的tw_substate成员中。

如果内部状态为FIN_WAIT_2,tcp_timewait_state_process()中处理的关键代码片段如下所示:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
if (tw->tw_substate == TCP_FIN_WAIT2) {
	/* Just repeat all the checks of tcp_rcv_state_process() */

	/* Out of window, send ACK */
	if (paws_reject ||
		!tcp_in_window(TCP_SKB_CB(skb)->seq, TCP_SKB_CB(skb)->end_seq,
			  tcptw->tw_rcv_nxt,
			  tcptw->tw_rcv_nxt + tcptw->tw_rcv_wnd))
		return TCP_TW_ACK;

	if (th->rst)
		goto kill;

	if (th->syn && !before(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt))
		goto kill_with_rst;

	/* Dup ACK? */
	if (!th->ack ||
		!after(TCP_SKB_CB(skb)->end_seq, tcptw->tw_rcv_nxt) ||
		TCP_SKB_CB(skb)->end_seq == TCP_SKB_CB(skb)->seq) {
		inet_twsk_put(tw);
		return TCP_TW_SUCCESS;
	}

	/* New data or FIN. If new data arrive after half-duplex close,
	 * reset.
	 */
	if (!th->fin ||
		TCP_SKB_CB(skb)->end_seq != tcptw->tw_rcv_nxt + 1) {
kill_with_rst:
		inet_twsk_deschedule(tw, &tcp_death_row);
		inet_twsk_put(tw);
		return TCP_TW_RST;
	}

	/* FIN arrived, enter true time-wait state. */
	tw->tw_substate      = TCP_TIME_WAIT;
	tcptw->tw_rcv_nxt = TCP_SKB_CB(skb)->end_seq;
	if (tmp_opt.saw_tstamp) {
		tcptw->tw_ts_recent_stamp = get_seconds();
		tcptw->tw_ts_recent      = tmp_opt.rcv_tsval;
	}

	/* I am shamed, but failed to make it more elegant.
	 * Yes, it is direct reference to IP, which is impossible
	 * to generalize to IPv6. Taking into account that IPv6
	 * do not understand recycling in any case, it not
	 * a big problem in practice. --ANK 
	 */
	if (tw->tw_family == AF_INET &&
		tcp_death_row.sysctl_tw_recycle && tcptw->tw_ts_recent_stamp &&
		tcp_v4_tw_remember_stamp(tw))
		inet_twsk_schedule(tw, &tcp_death_row, tw->tw_timeout,
				  TCP_TIMEWAIT_LEN);
	else
		inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
				  TCP_TIMEWAIT_LEN);

	return TCP_TW_ACK;
}

如果TCP段序号不完全在接收窗口内,则返回TCP_TW_ACK,表示需要给对端发送ACK。

如果在FIN_WAIT_2状态下接收到的是RST包,则跳转到kill标签处处理,立即释放timewait控制块,并返回TCP_TW_SUCCESS。

如果是SYN包,但是SYN包的序列号在要接收的序列号之前,则表示这是一个过期的SYN包,则跳转到kill_with_rst标签处处理,此时不仅会释放TIME_WAIT传输控制块,还会返回TCP_TW_RST,要给对端发送RST包。

如果接收到DACK,则释放timewait控制块,并返回TCP_TW_SUCCESS。在这种情况下有一个判断条件是看包的结束序列号和起始序列号相同时,会作为DACK处理,所以之后的处理是在数据包中的数据不为空的情况下处理。前面的处理中已经处理了SYN包、RST包的情况,接下来就剩以下三种情况:

1、不带FIN标志的数据包

2、带FIN标志,但是还包含数据

3、FIN包,不包含数据

如果是前两种情况,则会调用inet_twsk_deschedule()释放time_wait控制块。inet_twsk_deschedule()中会调用到inet_twsk_put()减少time_wait控制块的引用,在外层函数中再次调用inet_twsk_put()函数时,就会真正释放time_wait控制块。

如果接收的是对端的FIN包,即第3种情况,则将time_wait控制块的子状态设置为TCP_TIME_WAIT,此时才是进入真正的TIME_WAIT状态。然后根据TIME_WAIT的持续时间的长短来确定是加入到twcal_row队列还是启动一个定时器,最后会返回TCP_TW_ACK,给对端发送TCP连接关闭时最后的ACK包。

到这里,我们看到了对FIN_WAIT_2状态(传输控制块状态为TIME_WAIT状态下,但是子状态为FIN_WAIT_2)的完整处理。

接下来的处理才是对真正的TIME_WAIT状态的处理,即子状态也是TIME_WAIT。

如果在TIME_WAIT状态下,接收到ACK包(不带数据)或RST包,并且包的序列号刚好是下一个要接收的序列号,由以下代码片段处理:

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
if (!paws_reject &&
	(TCP_SKB_CB(skb)->seq == tcptw->tw_rcv_nxt &&
	(TCP_SKB_CB(skb)->seq == TCP_SKB_CB(skb)->end_seq || th->rst))) {
	/* In window segment, it may be only reset or bare ack. */
	if (th->rst) {
		/* This is TIME_WAIT assassination, in two flavors.
		* Oh well... nobody has a sufficient solution to this
		* protocol bug yet.
		*/
		if (sysctl_tcp_rfc1337 == 0) {
kill:
			inet_twsk_deschedule(tw, &tcp_death_row);
			inet_twsk_put(tw);
			return TCP_TW_SUCCESS;
		}
	}
	inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
			  TCP_TIMEWAIT_LEN);

	if (tmp_opt.saw_tstamp) {
		tcptw->tw_ts_recent      = tmp_opt.rcv_tsval;
		tcptw->tw_ts_recent_stamp = get_seconds();
	}

	inet_twsk_put(tw);
	return TCP_TW_SUCCESS;
}

如果是RST包的话,并且系统配置sysctl_tcp_rfc1337(默认情况下为0,参见/proc/sys/net/ipv4/tcp_rfc1337)的值为0,这时会立即释放time_wait传输控制块,丢掉接收的RST包。

如果是ACK包,则会启动TIME_WAIT定时器后丢掉接收到的ACK包。

接下来是对SYN包的处理。前面提到了,如果在TIME_WAIT状态下接收到序列号比上一个连接的结束序列号大的SYN包,可以接受,并建立新的连接,下面这段代码就是来处理这样的情况:

1
2
3
4
5
6
7
8
9
10
if (th->syn && !th->rst && !th->ack && !paws_reject &&
	(after(TCP_SKB_CB(skb)->seq, tcptw->tw_rcv_nxt) ||
	(tmp_opt.saw_tstamp &&
	  (s32)(tcptw->tw_ts_recent - tmp_opt.rcv_tsval) < 0))) {
	u32 isn = tcptw->tw_snd_nxt + 65535 + 2;
	if (isn == 0)
		isn++;
	TCP_SKB_CB(skb)->when = isn;
	return TCP_TW_SYN;
}

当返回TCP_TW_SYN时,在tcp_v4_rcv()中会立即释放time_wait控制块,并且开始进行正常的连接建立过程。

如果数据包不是上述几种类型的包,可能的情况有:

1、不是有效的SYN包。不考虑时间戳的话,就是序列号在上一次连接的结束序列号之前

2、ACK包,起始序列号不是下一个要接收的序列号

3、RST包,起始序列号不是下一个要接收的序列号

4、带数据的SKB包

这几种情况由以下代码处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
if (!th->rst) {
	/* In this case we must reset the TIMEWAIT timer.
	 *
	 * If it is ACKless SYN it may be both old duplicate
	 * and new good SYN with random sequence number <rcv_nxt.
	 * Do not reschedule in the last case.
	 */
	if (paws_reject || th->ack)
		inet_twsk_schedule(tw, &tcp_death_row, TCP_TIMEWAIT_LEN,
				  TCP_TIMEWAIT_LEN);

	/* Send ACK. Note, we do not put the bucket,
	 * it will be released by caller.
	 */
	return TCP_TW_ACK;
}
inet_twsk_put(tw);
return TCP_TW_SUCCESS;

如果是RST包,即第3种情况,则直接返回TCP_TW_SUCCESS,丢掉RST包。

如果带有ACK标志的话,则会启动TIME_WAIT定时器,然后给对端发送ACK。我们知道SYN包正常情况下不会设置ACK标志,所以如果是SYN包不会启动TIME_WAIT定时器,只会给对端发送ACK,告诉对端已经收到SYN包,避免重传,但连接应该不会继续建立。

还有一个细节需要提醒下,就是我们看到在返回TCP_TW_ACK时,没有调用inet_twsk_put()释放对time_wait控制块的引用。这时因为在tcp_v4_rcv()中调用tcp_v4_timewait_ack()发送ACK时会用到time_wait控制块,所以需要保持对time_wait控制块的引用。在tcp_v4_timewait_ack()中发送完ACK后,会调用inet_twsk_put()释放对time_wait控制块的引用。

OK,现在我们对TIME_WAIT状态下接收到数据包的情况有了一个了解,知道内核会如何来处理这些包。但是看到的这些更多的是以服务器端的角度来看的,如果客户端主动关闭连接的话,进入TIME_WAIT状态的是客户端。如果客户端在TIME_WAIT状态下重用端口号来和服务器建立连接,内核会如何处理呢?

我编写了一个测试程序:创建一个套接字,设置SO_REUSEADDR选项,建立连接后立即关闭,关闭后立即又重复同样的过程,发现在第二次调用connect()的时候返回EADDRNOTAVAIL错误。这个测试程序很容易理解,写起来也很容易,就不贴出来了。

要找到这个错误是怎么返回的,需要从TCP层的连接函数tcp_4_connect()开始。在tcp_v4_connect()中没有显示返回EADDRNOTAVAIL错误的地方,可能的地方就是在调用inet_hash_connect()返回的。为了确定是不是在inet_hash_connect()中返回的,使用systemtap编写了一个脚本,发现确实是在这个函数中返回的-99错误(EADDRNOTAVAIL的值为99)。其实这个通过代码也可以看出来,在这个函数之前会先查找目的主机的路由缓存项,调用的是ip_route_connect()函数,跟着这个函数的调用轨迹,没有发现返回EADDRNOTAVAIL错误的地方。

inet_hash_connect()函数只是对__inet_hash_connect()函数进行了简单的封装。在__inet_hash_connect()中如果已绑定了端口号,并且是和其他传输控制块共享绑定的端口号,则会调用check_established参数指向的函数来检查这个绑定的端口号是否可用,代码如下所示:

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
int __inet_hash_connect(struct inet_timewait_death_row *death_row,
		struct sock *sk, u32 port_offset,
		int (*check_established)(struct inet_timewait_death_row *,
			struct sock *, __u16, struct inet_timewait_sock **),
		void (*hash)(struct sock *sk))
{
	struct inet_hashinfo *hinfo = death_row->hashinfo;
	const unsigned short snum = inet_sk(sk)->num;
	struct inet_bind_hashbucket *head;
	struct inet_bind_bucket *tb;
	int ret;
	struct net *net = sock_net(sk);

	if (!snum) {
		......
	}

	head = &hinfo->bhash[inet_bhashfn(net, snum, hinfo->bhash_size)];
	tb  = inet_csk(sk)->icsk_bind_hash;
	spin_lock_bh(&head->lock);
	if (sk_head(&tb->owners) == sk && !sk->sk_bind_node.next) {
		hash(sk);
		spin_unlock_bh(&head->lock);
		return 0;
	} else {
		spin_unlock(&head->lock);
		/* No definite answer... Walk to established hash table */
		ret = check_established(death_row, sk, snum, NULL);
out:
		local_bh_enable();
		return ret;
	}
}

(sk_head(&tb->owners) == sk && !sk->sk_bind_node.next)这个判断条件就是用来判断是不是只有当前传输控制块在使用已绑定的端口,条件为false时,会执行else分支,检查是否可用。这么看来,调用bind()成功并不意味着这个端口就真的可以用。

check_established参数对应的函数是__inet_check_established(),在inet_hash_connect()中可以看到。在上面的代码中我们还注意到调用check_established()时第三个参数为NULL,这在后面的分析中会用到。

__inet_check_established()函数中,会分别在TIME_WAIT传输控制块和除TIME_WIAT、LISTEN状态外的传输控制块中查找是已绑定的端口是否已经使用,代码片段如下所示:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
/* called with local bh disabled */
static int __inet_check_established(struct inet_timewait_death_row *death_row,
					struct sock *sk, __u16 lport,
					struct inet_timewait_sock **twp)
{
	struct inet_hashinfo *hinfo = death_row->hashinfo;
	struct inet_sock *inet = inet_sk(sk);
	__be32 daddr = inet->rcv_saddr;
	__be32 saddr = inet->daddr;
	int dif = sk->sk_bound_dev_if;
	INET_ADDR_COOKIE(acookie, saddr, daddr)
	const __portpair ports = INET_COMBINED_PORTS(inet->dport, lport);
	struct net *net = sock_net(sk);
	unsigned int hash = inet_ehashfn(net, daddr, lport, saddr, inet->dport);
	struct inet_ehash_bucket *head = inet_ehash_bucket(hinfo, hash);
	spinlock_t *lock = inet_ehash_lockp(hinfo, hash);
	struct sock *sk2;
	const struct hlist_nulls_node *node;
	struct inet_timewait_sock *tw;

	spin_lock(lock);

	/* Check TIME-WAIT sockets first. */
	sk_nulls_for_each(sk2, node, &head->twchain) {
		tw = inet_twsk(sk2);

	if (INET_TW_MATCH(sk2, net, hash, acookie,
					saddr, daddr, ports, dif)) {
			if (twsk_unique(sk, sk2, twp))
				goto unique;
			else
				goto not_unique;
		}
	}
	tw = NULL;

	/* And established part... */
	sk_nulls_for_each(sk2, node, &head->chain) {
		if (INET_MATCH(sk2, net, hash, acookie,
					saddr, daddr, ports, dif))
			goto not_unique;
	}

unique:
	......
	return 0;

not_unique:
	spin_unlock(lock);
	return -EADDRNOTAVAIL;
}

可以看到返回EADDRNOTVAIL错误的有两种情况:

1、在TIME_WAIT传输控制块中找到匹配的端口,并且twsk_unique()返回true时

2、在除TIME_WAIT和LISTEN状态外的传输块中存在匹配的端口。

第二种情况很好容易理解了,只要状态在FIN_WAIT_1、ESTABLISHED等的传输控制块使用的端口和要查找的匹配,就会返回EADDRNOTVAIL错误。第一种情况还要取决于twsk_uniqueue()的返回值,所以接下来我们看twsk_uniqueue()中什么情况下会返回true。

如果是TCP套接字,twsk_uniqueue()中会调用tcp_twsk_uniqueue()来判断,返回true的条件如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int tcp_twsk_unique(struct sock *sk, struct sock *sktw, void *twp)
{
	const struct tcp_timewait_sock *tcptw = tcp_twsk(sktw);
	struct tcp_sock *tp = tcp_sk(sk);

	if (tcptw->tw_ts_recent_stamp &&
		(twp == NULL || (sysctl_tcp_tw_reuse &&
				get_seconds() - tcptw->tw_ts_recent_stamp > 1))) {
		......
		return 1;
	}

	return 0;
}

我们前面提到过,__inet_hash_connect()函数调用check_established指向的函数时第三个参数为NULL,所以现在我们只需要关心tcptw->tw_ts_recent_stamp是否非零,只要这个值非零,tcp_twsk_unique()就会返回true, 在上层connect()函数中就会返回EADDRNOTVAIL错误。tcptw->tw_ts_recent_stamp存储的是最近接收到段的时间戳值,所以正常情况下这个值不会为零。当然也可以通过调整系统的参数,让这个值可以为零,这不是本文讨论的重点,感兴趣的可以参考tcp_v4_connect()中的代码进行修改。

在导致返回EADDRNOTVAIL的两种情况中,第一种情况可以有办法避免,但是如果的第二次建立连接的时间和第一次关闭连接之间的时间间隔太小的话,此时第一个连接可能处在FIN_WAIT_1、FIN_WAIT_2等状态,此时没有系统参数可以用来避免返回EADDRNOTVAIL。如果你还是想无论如何都要在很短的时间内重用客户端的端口,这样也有办法,要么是用kprobe机制,要么用systemtap脚本,改变__inet_check_established()函数的返回值。

内核处理time_wait状态详解

http://simohayha.iteye.com/blog/566980

这次来详细看内核的time_wait状态的实现,在前面介绍定时器的时候,time_wait就简单的介绍了下。这里我们会先介绍tw状态的实现,然后来介绍内核协议栈如何处理tw状态。

首先我们要知道在linux内核中time_wait的处理是由tcp_time_wait这个函数来做得,比如我们在closing状态收到一个fin,就会调用tcp_time_wait.而内核为time_wait状态的socket专门设计了一个结构就是inet_timewait_sock,并且是挂载在inet_ehash_bucket的tw上(这个结构前面也已经介绍过了)。这里要注意,端口号的那个hash链表中也是会保存time_wait状态的socket的。

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
38
39
40
41
42
43
44
struct inet_timewait_sock {  

	//common也就是包含了一些socket的必要信息。  
	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_nulls_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  
#define tw_net          __tw_common.skc_net  

	 //tw状态的超时时间  
	int         tw_timeout;  
	//这个用来标记我们是正常进入tw还是说由于超时等一系列原因进入(比如超时等一系列原因)  
	volatile unsigned char  tw_substate;  
	/* 3 bits hole, try to pack */  
	//和tcp option中的接收窗口比例类似  
	unsigned char       tw_rcv_wscale;  

	//也就是标示sock的4个域,源目的端口和地址  
	__be16          tw_sport;  
	__be32          tw_daddr __attribute__((aligned(INET_TIMEWAIT_ADDRCMP_ALIGN_BYTES)));  
	__be32          tw_rcv_saddr;  
	__be16          tw_dport;  
	//本地端口。  
	__u16           tw_num;  
	kmemcheck_bitfield_begin(flags);  
	/* And these are ours. */  
	//几个标记位。  
	unsigned int        tw_ipv6only     : 1,  
				tw_transparent  : 1,  
				tw_pad      : 14,   /* 14 bits hole */  
				tw_ipv6_offset  : 16;  
	kmemcheck_bitfield_end(flags);  
	unsigned long       tw_ttd;  
	//链接到端口的hash表中。  
	struct inet_bind_bucket *tw_tb;  
	//链接到全局的tw状态hash表中。  
	struct hlist_node   tw_death_node;  
};  

然后我们要知道linux有两种方式执行tw状态的socket,一种是等待2×MSL时间(内核中是60秒),一种是基于RTO来计算超时时间。

基于RTO的超时时间也叫做recycle模式,这里内核会通过sysctl_tw_recycle(也就是说我们能通过sysctl来打开这个值)以及是否我们还保存有从对端接收到的最近的数据包的时间戳来判断是否进入打开recycle模式的处理。如果进入则会调用tcp_v4_remember_stamp来得到是否打开recycle模式。

下面就是这个片断的代码片断:

1
2
3
4
//必须要设置sysctl_tw_recycle以及保存有最后一次的时间戳.  
if (tcp_death_row.sysctl_tw_recycle && tp->rx_opt.ts_recent_stamp)  
	//然后调用remember_stamp(在ipv4中被初始化为tcp_v4_remember_stamp)来得到是否要打开recycle模式。  
	recycle_ok = icsk->icsk_af_ops->remember_stamp(sk);  

然后我们来看tcp_v4_remember_stamp,在看这个之前,我们需要理解inet_peer结构,这个结构也就是保存了何当前主机通信的主机的一些信息,我前面在分析ip层的时候有详细分析这个结构,因此可以看我前面的blog:

http://simohayha.iteye.com/blog/437695

tcp_v4_remember_stamp主要用来从全局的inet_peer中得到对应的当前sock的对端的信息(通过ip地址).然后设置相关的时间戳(tcp_ts_stamp和tcp_ts).这里要特别注意一个东西,那就是inet_peer是ip层的东西,因此它的key是ip地址,它会忽略端口号。所以说这里inet_peer的这两个时间戳是专门为解决tw状态而设置的。

然后我们来看下tcp option的几个关键的域:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
struct tcp_options_received {  
	/*  PAWS/RTTM data  */  

	//这个值为我们本机更新ts_recent的时间  
	long    ts_recent_stamp;  
	//这个表示最近接收的那个数据包的时间戳  
	u32 ts_recent;  
	//这个表示这个数据包发送时的时间戳    
	u32 rcv_tsval;  
	//这个表示当前数据包所回应的数据包的时间戳。  
	u32 rcv_tsecr;  
	//如果上面两个时间戳都有设置,则saw_tstamp设置为1.  
	u16 saw_tstamp : 1,   //TIMESTAMP seen on SYN packet  
		tstamp_ok : 1,    //d-scack标记  
		dsack : 1,        //Wscale seen on SYN packet  
		wscale_ok : 1,    //SACK seen on SYN packet     
		sack_ok : 4,      //下面两个是窗口扩大倍数,主要是为了解决一些特殊网络下大窗口的问题。  
		snd_wscale : 4,   
		rcv_wscale : 4;   
	/*  SACKs data  */  
	u8  num_sacks;    
	u16 user_mss;     
	u16 mss_clamp;    
};  

而inet_peer中的两个时间戳与option中的ts_recent和ts_recent_stamp类似。

来看tcp_v4_remember_stamp的实现:

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
int tcp_v4_remember_stamp(struct sock *sk)  
{  
	struct inet_sock *inet = inet_sk(sk);  
	struct tcp_sock *tp = tcp_sk(sk);  
	struct rtable *rt = (struct rtable *)__sk_dst_get(sk);  
	struct inet_peer *peer = NULL;  
	int release_it = 0;  

	//得到对应peer(两种得到的方式)。  
	if (!rt || rt->rt_dst != inet->daddr) {  
		peer = inet_getpeer(inet->daddr, 1);  
		release_it = 1;  
	} else {  
		if (!rt->peer)  
			rt_bind_peer(rt, 1);  
		peer = rt->peer;  
	}  

	//如果peer不存在则会返回0,也就是关闭recycle模式。  
	if (peer) {  
	//这里tcp_ts以及tcp_ts_stamp保存的是最新的时间戳,所以这里与当前的sock的时间戳比较小的话就要更新。  
	if ((s32)(peer->tcp_ts - tp->rx_opt.ts_recent) <= 0 ||(peer->tcp_ts_stamp + TCP_PAWS_MSL < get_seconds() &&  
	 peer->tcp_ts_stamp <= tp->rx_opt.ts_recent_stamp)) {  

	//更新时间戳。  
	peer->tcp_ts_stamp = tp->rx_opt.ts_recent_stamp;  
		peer->tcp_ts = tp->rx_opt.ts_recent;  
		}  
		if (release_it)  
			inet_putpeer(peer);  
		return 1;  
	}  

	//关闭recycle模式。  
	return 0;  
}  

ok,我们来看tcp_time_wait的实现,这里删掉了ipv6以及md5的部分:

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
//这里也就是2*MSL=60秒。  
#define TCP_TIMEWAIT_LEN (60*HZ)   

//这里的state标记我们是正常进入tw状态,还是由于死在fin-wait-2状态才进入tw状态的。  
void tcp_time_wait(struct sock *sk, int state, int timeo)  
{  
	//TW的socket  
	struct inet_timewait_sock *tw = NULL;  
	const struct inet_connection_sock *icsk = inet_csk(sk);  
	const struct tcp_sock *tp = tcp_sk(sk);  

	//recycle模式的标记。  
	int recycle_ok = 0;  

	//上面已经分析过了。  
	if (tcp_death_row.sysctl_tw_recycle && tp->rx_opt.ts_recent_stamp)  
	recycle_ok = icsk->icsk_af_ops->remember_stamp(sk);  
	//然后判断tw状态的sock数量是否已经超过限制。  
	if (tcp_death_row.tw_count < tcp_death_row.sysctl_max_tw_buckets)  
	//没有的话alloc一个新的。  
		tw = inet_twsk_alloc(sk, state);  

	//如果tw不为空才会进入处理。  
	if (tw != NULL) {  
		struct tcp_timewait_sock *tcptw = tcp_twsk((struct sock *)tw);  
		//计算对应的超时时间,这里可以看到刚好是3.5*rto.  
		const int rto = (icsk->icsk_rto << 2) - (icsk->icsk_rto >> 1);  
		//更新对应的域。  
		tw->tw_rcv_wscale    = tp->rx_opt.rcv_wscale;  
		tcptw->tw_rcv_nxt    = tp->rcv_nxt;  
		tcptw->tw_snd_nxt    = tp->snd_nxt;  
		tcptw->tw_rcv_wnd    = tcp_receive_window(tp);  
		tcptw->tw_ts_recent  = tp->rx_opt.ts_recent;  
		tcptw->tw_ts_recent_stamp = tp->rx_opt.ts_recent_stamp;  

		//更新链表(下面会分析)。  
		__inet_twsk_hashdance(tw, sk, &tcp_hashinfo);  
		//如果传递进来的超时时间小于我们计算的,则让他等于我们计算的超时时间。  
		/* Get the TIME_WAIT timeout firing. */  
		if (timeo < rto)  
			timeo = rto;  

		//如果打开recycle模式,则超时时间为我们基于rto计算的时间。  
		if (recycle_ok) {  
			tw->tw_timeout = rto;  
		} else {  
			//否则为2*MSL=60秒  
			tw->tw_timeout = TCP_TIMEWAIT_LEN;  
			//如果正常进入则timeo也就是超时时间为2*MSL.  
			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 {  
		LIMIT_NETDEBUG(KERN_INFO "TCP: time wait bucket table overflow\n");  
	}  
	tcp_update_metrics(sk);  
	tcp_done(sk);  
}  

然后我们来看__inet_twsk_hashdance函数,这个函数主要是用于更新对应的全局hash表。有关这几个hash表的结构可以去看我前面的blog。

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
void __inet_twsk_hashdance(struct inet_timewait_sock *tw, struct sock *sk,  
			   struct inet_hashinfo *hashinfo)  
{  
	const struct inet_sock *inet = inet_sk(sk);  
	const struct inet_connection_sock *icsk = inet_csk(sk);  
	//得到ehash。  
	struct inet_ehash_bucket *ehead = inet_ehash_bucket(hashinfo, sk->sk_hash);  
	spinlock_t *lock = inet_ehash_lockp(hashinfo, sk->sk_hash);  
	struct inet_bind_hashbucket *bhead;  

	//下面这几步是将tw sock链接到bhash中。  
	bhead = &hashinfo->bhash[inet_bhashfn(twsk_net(tw), inet->num,hashinfo->bhash_size)];  
	spin_lock(&bhead->lock);  
	//链接到bhash。这里icsk的icsk_bind_hash也就是bash的一个元素。  
	tw->tw_tb = icsk->icsk_bind_hash;  
	WARN_ON(!icsk->icsk_bind_hash);  
	//将tw加入到bash中。  
	inet_twsk_add_bind_node(tw, &tw->tw_tb->owners);  
	spin_unlock(&bhead->lock);  

	spin_lock(lock);  


	atomic_inc(&tw->tw_refcnt);  
	//将tw sock加入到ehash的tw chain中。  
	inet_twsk_add_node_rcu(tw, &ehead->twchain);  

	//然后从全局的establish hash中remove掉这个socket。详见sock的sk_common域。  
	if (__sk_nulls_del_node_init_rcu(sk))  
		sock_prot_inuse_add(sock_net(sk), sk->sk_prot, -1);  

	spin_unlock(lock);  
}  

这里我们要知道还有一个专门的全局的struct inet_timewait_death_row类型的变量tcp_death_row来保存所有的tw状态的socket。而整个tw状态的socket并不是全部加入到定时器中,而是将tcp_death_row加入到定时器中,然后每次定时器超时通过tcp_death_row来查看定时器的超时情况,从而处理tw状态的sock。

而这里定时器分为两种,一种是长时间的定时器,它也就是tw_timer域,一种是短时间的定时器,它也就是twcal_timer域。

而这里还有两个hash表,一个是twcal_row,它对应twcal_timer这个定时器,也就是说当twcal_timer超时,它就会从twcal_row中取得对应的twsock。对应的cells保存的就是tw_timer定时器超时所用的twsock。

还有两个slot,一个是slot域,一个是twcal_hand域,分别表示当前对应的定时器(上面介绍的两个)所正在执行的定时器的slot。

而上面所说的recycle模式也就是指twcal_timer定时器。

来看结构。

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
struct inet_timewait_death_row {  

	//这几个域会在tcp_death_row中被初始化。  
	int         twcal_hand;  
	unsigned long       twcal_jiffie;  
	//短时间定时器。  
	struct timer_list   twcal_timer;  
	//twcal_timer定时器对应的hash表  
	struct hlist_head   twcal_row[INET_TWDR_RECYCLE_SLOTS];  

	spinlock_t      death_lock;  
	//tw的个数。  
	int         tw_count;  
	//超时时间。  
	int         period;  
	u32         thread_slots;  
	struct work_struct  twkill_work;  
	//长时间定时器  
	struct timer_list   tw_timer;  
	int         slot;  

	//短时间的定时器对应的hash表  
	struct hlist_head   cells[INET_TWDR_TWKILL_SLOTS];  
	struct inet_hashinfo    *hashinfo;  
	int         sysctl_tw_recycle;  
	int         sysctl_max_tw_buckets;  
};  

这里要注意INET_TWDR_TWKILL_SLOTS为8,而INET_TWDR_RECYCLE_SLOTS为32。

ok我们接着来看tcp_death_row的初始化。

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 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),  

	//可以看到它是链接到全局的inet_hashinfo中的。  
	.hashinfo   = &tcp_hashinfo,  
	//定时器,这里要注意超时函数。  
	.tw_timer   = TIMER_INITIALIZER(inet_twdr_hangman, 0,(unsigned long)&tcp_death_row),  
	//工作队列。其实也就是销毁twsock工作的工作队列。  
	.twkill_work    = __WORK_INITIALIZER(tcp_death_row.twkill_work,                   inet_twdr_twkill_work),  
	/* Short-time timewait calendar */  

	//twcal_hand用来标记twcal_timer定时器是否还在工作。  
	.twcal_hand = -1,  
	.twcal_timer    = TIMER_INITIALIZER(inet_twdr_twcal_tick, 0,(unsigned long)&tcp_death_row),  
};  

然后就是inet_twsk_schedule的实现,这个函数也就是tw状态的处理函数。他主要是用来基于超时时间来计算当前twsock的可用的位置。也就是来判断启动那个定时器,然后加入到那个队列。

因此这里的关键就是slot的计算。这里slot的计算是根据我们传递进来的timeo来计算的。

recycle模式下tcp_death_row的超时时间的就为2的INET_TWDR_RECYCLE_TICK幂。

我们一般桌面的hz为100,来看对应的值:

1
2
#elif HZ <= 128  
# define INET_TWDR_RECYCLE_TICK (7 + 2 - INET_TWDR_RECYCLE_SLOTS_LOG)  

可以看到这时它的值就为4.

而tw_timer的slot也就是长时间定时器的slot的计算是这样的,它也就是用我们传递进来的超时时间timeo/16(可以看到就是2的INET_TWDR_RECYCLE_TICK次方)然后向上取整。

而这里twdr的period被设置为

1
2
3
4
TCP_TIMEWAIT_LEN / INET_TWDR_TWKILL_SLOTS,  

//取slot的代码片断。  
slot = DIV_ROUND_UP(timeo, twdr->period);  

而我们下面取slot的时候也就是会用这个值来散列。可以看到散列表的桶的数目就为INET_TWDR_TWKILL_SLOTS个,因此这里也就是把时间分为INET_TWDR_TWKILL_SLOTS份,每一段时间内的超时twsock都放在一个桶里面,而大于60秒的都放在最后一个桶。

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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
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;  

	//得到slot。  
	slot = (timeo + (1 << INET_TWDR_RECYCLE_TICK) - 1) >> INET_TWDR_RECYCLE_TICK;  

	spin_lock(&twdr->death_lock);  

	/* Unlink it, if it was scheduled */  
	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 */  
		//如果大于timewait_len也就是2*MSL=60秒,则slot为cells的最后一项。  
		if (timeo >= timewait_len) {  
			//设为最后一项。  
			slot = INET_TWDR_TWKILL_SLOTS - 1;  
		} else {  
			//否则timeo除于period然后向上取整。  
			slot = DIV_ROUND_UP(timeo, twdr->period);  
			//如果大于cells的桶的大小,则也是放到最后一个位置。  
			if (slot >= INET_TWDR_TWKILL_SLOTS)  
				slot = INET_TWDR_TWKILL_SLOTS - 1;  
		}  
		//然后设置超时时间,  
		tw->tw_ttd = jiffies + timeo;  
		//而twdr的slot为当前正在处理的slot,因此我们需要以这个slot为基准来计算真正的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;  
			//定时器的超时时间。可以看到时间为我们传进来的timeo(只不过象tick对齐了)  
			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));  

				//和上面的tw_timer定时器类似,我们要通过当前正在执行的slot也就是twcal_hand来得到真正的slot。  
				slot = (twdr->twcal_hand + slot) & (INET_TWDR_RECYCLE_SLOTS - 1);  
			}  
		//取得该插入的桶。  
		list = &twdr->twcal_row[slot];  
	}  

	//将tw加入到对应的链表中。  
	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);  
}  

我们先来总结一下上面的代码。当我们进入tw状态,然后我们会根据计算出来的timeo的不同来加载到不同的hash表中。而对应的定时器一个(tw_timer)是每peroid启动一次,一个是每(slot << INET_TWDR_RECYCLE_TICK)启动一次。

下面的两张图很好的表示了recycle模式(twcal定时器)和非recycle模式的区别:

先是非recycle模式:

然后是recycle模式:

接下来我们来看两个超时函数的实现,这里我只简单的介绍下两个超时函数,一个是inet_twdr_hangman,一个是inet_twdr_twcal_tick。

在inet_twdr_hangman中,每次只是遍历对应的slot的队列,然后将队列中的所有sock删除,同时也从bind_hash中删除对应的端口信息。这个函数就不详细分析了。

而在inet_twdr_twcal_tick中,每次遍历所有的twcal_row,然后超时的进行处理(和上面一样),然后没有超时的继续处理).

这里有一个j的计算要注意,前面我们知道我们的twcal的超时时间可以说都是以INET_TWDR_RECYCLE_SLOTS对齐的,而我们这里在处理超时的同时,有可能上面又有很多sock加入到了tw状态,因此这里我们的超时检测的间隔就是1 << INET_TWDR_RECYCLE_TICK。

来看inet_twdr_twcal_tick的实现:

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
38
39
40
41
42
void inet_twdr_twcal_tick(unsigned long data)  
{  
	............................  
	if (twdr->twcal_hand < 0)  
		goto out;  

	//得到slot。  
	slot = twdr->twcal_hand;  
	//得到定时器启动时候的jiffes。  
	j = twdr->twcal_jiffie;  

	//遍历所有的twscok。  
	for (n = 0; n < INET_TWDR_RECYCLE_SLOTS; n++) {  
		//判断是否超时。  
		if (time_before_eq(j, now)) {  
			//处理超时的socket  
			struct hlist_node *node, *safe;  
			struct inet_timewait_sock *tw;  
			.......................................  
		} else {  
			if (!adv) {  
				adv = 1;  
				twdr->twcal_jiffie = j;  
				twdr->twcal_hand = slot;  
			}  

		//如果不为空,则将重新添加这些定时器  
		if (!hlist_empty(&twdr->twcal_row[slot])) {  
			mod_timer(&twdr->twcal_timer, j);  
				goto out;  
			}  
		}  
		//设置间隔  
		j += 1 << INET_TWDR_RECYCLE_TICK;  
		//更新  
		slot = (slot + 1) & (INET_TWDR_RECYCLE_SLOTS - 1);  
	}  
	//处理完毕则将twcal_hand设为-1.  
	twdr->twcal_hand = -1;  

	...............................  
}  

然后我们来看tcp怎么样进入tw状态。这里分为两种,一种是正常进入也就是在wait2收到一个fin,或者closing收到ack。这种都是比较简单的。我们就不分析了。

比较特殊的是,我们有可能会直接从wait1进入tw状态,或者是在wait2等待超时也将会直接进入tw状态。这个时候也就是没有收到对端的fin。

这个主要是为了处理当对端死在close_wait状态的时候,我们需要自己能够恢复到close状态,而不是一直处于wait2状态。

在看代码之前我们需要知道一个东西,那就是fin超时时间,这个超时时间我们可以通过TCP_LINGER2这个option来设置,并且这个值的最大值是sysctl_tcp_fin_timeout/HZ. 这里可以看到sysctl_tcp_fin_timeout是jiffies数,所以要转成秒。我这里简单的测试了下,linger2的默认值也就是60,刚好是2*MSL.

这里linger2也就是代表在tcp_wait2的最大生命周期。如果小于0则说明我们要跳过tw状态。

先来看在tcp_close中的处理,不过这里不理解为什么这么做的原因。

这里为什么有可能会为wait2状态呢,原因是如果设置了linger,则我们就会休眠掉,而休眠的时间可能我们已经收到ack,此时将会进入wait2的处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
if (sk->sk_state == TCP_FIN_WAIT2) {  
		struct tcp_sock *tp = tcp_sk(sk);  
		//如果小于0,则说明从wait2立即超时此时也就是相当于跳过tw状态,所以我们直接发送rst,然后进入close。  
		if (tp->linger2 < 0) {  
			tcp_set_state(sk, TCP_CLOSE);  
			tcp_send_active_reset(sk, GFP_ATOMIC);  
			NET_INC_STATS_BH(sock_net(sk),  
					LINUX_MIB_TCPABORTONLINGER);  
		} else {  
			//否则计算fin的时间,这里的超时时间是在linger2和3.5RTO之间取最大值。  
			const int tmo = tcp_fin_time(sk);  

			//如果超时时间很大,则说明我们需要等待时间很长,因此我们启动keepalive探测对端是否存活。  
			if (tmo > TCP_TIMEWAIT_LEN) {  
			inet_csk_reset_keepalive_timer(sk,  
				tmo - TCP_TIMEWAIT_LEN);  
			} else {  
				//否则我们直接进入tw状态。  
				tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);  
				goto out;  
			}  
		}  
	}  

还有从wait1直接进入tw,和上面类似,我就不介绍了。

最后我们来看当内核处于tw状态后,再次接收到数据包后如何处理。这里的处理函数就是tcp_timewait_state_process,而他是在tcp_v4_rcv中被调用的,它会先判断是否处于tw状态,如果是的话,进入tw的处理。

这个函数的返回值分为4种。

1
2
3
4
5
6
7
8
9
10
11
enum tcp_tw_status  
{  
	//这个代表我们成功处理了数据包。  
	TCP_TW_SUCCESS = 0,  
	//我们需要发送给对端一个rst。  
	TCP_TW_RST = 1,  
	//我们接收到了重传的fin,因此我们需要重传ack。  
	TCP_TW_ACK = 2,  
	//这个表示我们需要重新建立一个连接。  
	TCP_TW_SYN = 3  
};  

这里可能最后一个比较难理解,这里内核注释得很详细,主要是实现了RFC1122:

引用

RFC 1122:

“When a connection is […] on TIME-WAIT state […] [a TCP] MAY accept a new SYN from the remote TCP to reopen the connection directly, if it: (1) assigns its initial sequence number for the new connection to be larger than the largest sequence number it used on the previous connection incarnation,and

(2) returns to TIME-WAIT state if the SYN turns out to be an old duplicate".

来看这段处理代码:

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
switch (tcp_timewait_state_process(inet_twsk(sk), skb, th)) {  
	case TCP_TW_SYN: {  
		//取得一个sk。  
		struct sock *sk2 = inet_lookup_listener(dev_net(skb->dev),&tcp_hashinfo,  
			iph->daddr, th->dest,inet_iif(skb));  
		if (sk2) {  
			//从tw中删除,然后继续执行(也就是开始三次握手)。  
			inet_twsk_deschedule(inet_twsk(sk), &tcp_death_row);  
			inet_twsk_put(inet_twsk(sk));  
			sk = sk2;  
			goto process;  
		}  
		/* Fall through to ACK */  
	}  
	case TCP_TW_ACK:  
		//发送ack  
		tcp_v4_timewait_ack(sk, skb);  
		break;  
	//发送给对端rst。  
	case TCP_TW_RST:  
		goto no_tcp_socket;  
	//处理成功  
	case TCP_TW_SUCCESS:;  
	}  
	goto discard_it;  

tcp_timewait_state_process这个函数具体的实现我就不介绍了,它就是分为两部分,一部分处理tw_substate == TCP_FIN_WAIT2的情况,一部分是正常情况。在前一种情况,我们对于syn的相应是直接rst的。而后一种我们需要判断是否新建连接。

而对于fin的处理他们也是不一样的,wait2的话,它会将当前的tw重新加入到定时器列表(inet_twsk_schedule).而后一种则只是重新发送ack。

Linux的inode的理解

http://www.cnblogs.com/itech/archive/2012/05/15/2502284.html

一、inode是什么?

理解inode,要从文件储存说起。

文件储存在硬盘上,硬盘的最小存储单位叫做"扇区"(Sector)。每个扇区储存512字节(相当于0.5KB)。

操作系统读取硬盘的时候,不会一个个扇区地读取,这样效率太低,而是一次性连续读取多个扇区,即一次性读取一个"块"(block)。这种由多个扇区组成的"块",是文件存取的最小单位。"块"的大小,最常见的是4KB,即连续八个 sector组成一个 block。

文件数据都储存在"块"中,那么很显然,我们还必须找到一个地方储存文件的元信息,比如文件的创建者、文件的创建日期、文件的大小等等。这种储存文件元信息的区域就叫做inode,中文译名为"索引节点"。

二、inode的内容

inode包含文件的元信息,具体来说有以下内容: * 文件的字节数
* 文件拥有者的User ID
* 文件的Group ID
* 文件的读、写、执行权限
* 文件的时间戳,共有三个:ctime指inode上一次变动的时间,mtime指文件内容上一次变动的时间,atime指文件上一次打开的时间。
* 链接数,即有多少文件名指向这个inode
* 文件数据block的位置

可以用stat命令,查看某个文件的inode信息:

1
stat example.txt

总之,除了文件名以外的所有文件信息,都存在inode之中。至于为什么没有文件名,下文会有详细解释。

三、inode的大小

inode也会消耗硬盘空间,所以硬盘格式化的时候,操作系统自动将硬盘分成两个区域。一个是数据区,存放文件数据;另一个是inode区(inode table),存放inode所包含的信息。 每个inode节点的大小,一般是128字节或256字节。inode节点的总数,在格式化时就给定,一般是每1KB或每2KB就设置一个inode。假定在一块1GB的硬盘中,每个inode节点的大小为128字节,每1KB就设置一个inode,那么inode table的大小就会达到128MB,占整块硬盘的12.8%。

查看每个硬盘分区的inode总数和已经使用的数量,可以使用df命令。

1
df -i

查看每个inode节点的大小,可以用如下命令:

1
sudo dumpe2fs -h /dev/hda | grep "Inode size"

由于每个文件都必须有一个inode,因此有可能发生inode已经用光,但是硬盘还未存满的情况。这时,就无法在硬盘上创建新文件。

四、inode号码

每个inode都有一个号码,操作系统用inode号码来识别不同的文件。

这里值得重复一遍,Unix/Linux系统内部不使用文件名,而使用inode号码来识别文件。对于系统来说,文件名只是inode号码便于识别的别称或者绰号。表面上,用户通过文件名,打开文件。实际上,系统内部这个过程分成三步:首先,系统找到这个文件名对应的inode号码;其次,通过inode号码,获取inode信息;最后,根据inode信息,找到文件数据所在的block,读出数据。

使用ls -i命令,可以看到文件名对应的inode号码:

1
ls -i example.txt

五、目录文件

Unix/Linux系统中,目录(directory)也是一种文件。打开目录,实际上就是打开目录文件。

目录文件的结构非常简单,就是一系列目录项(dirent)的列表。每个目录项,由两部分组成:所包含文件的文件名,以及该文件名对应的inode号码。

ls命令只列出目录文件中的所有文件名:

1
ls /etc

ls -i命令列出整个目录文件,即文件名和inode号码:

1
ls -i /etc

如果要查看文件的详细信息,就必须根据inode号码,访问inode节点,读取信息。ls -l命令列出文件的详细信息。

1
ls -l /etc

六、硬链接

一般情况下,文件名和inode号码是"一一对应"关系,每个inode号码对应一个文件名。但是,Unix/Linux系统允许,多个文件名指向同一个inode号码。这意味着,可以用不同的文件名访问同样的内容;对文件内容进行修改,会影响到所有文件名;但是,删除一个文件名,不影响另一个文件名的访问。这种情况就被称为"硬链接"(hard link)。

ln命令可以创建硬链接:

1
ln 源文件 目标文件

运行上面这条命令以后,源文件与目标文件的inode号码相同,都指向同一个inode。inode信息中有一项叫做"链接数",记录指向该inode的文件名总数,这时就会增加1。反过来,删除一个文件名,就会使得inode节点中的"链接数"减1。当这个值减到0,表明没有文件名指向这个inode,系统就会回收这个inode号码,以及其所对应block区域。

这里顺便说一下目录文件的"链接数"。创建目录时,默认会生成两个目录项:".“和”..“。前者的inode号码就是当前目录的inode号码,等同于当前目录的"硬链接";后者的inode号码就是当前目录的父目录的inode号码,等同于父目录的"硬链接"。所以,任何一个目录的"硬链接"总数,总是等于2加上它的子目录总数(含隐藏目录),这里的2是父目录对其的“硬链接”和当前目录下的”.硬链接“。

七、软链接

除了硬链接以外,还有一种特殊情况。文件A和文件B的inode号码虽然不一样,但是文件A的内容是文件B的路径。读取文件A时,系统会自动将访问者导向文件B。因此,无论打开哪一个文件,最终读取的都是文件B。这时,文件A就称为文件B的"软链接"(soft link)或者"符号链接(symbolic link)。

这意味着,文件A依赖于文件B而存在,如果删除了文件B,打开文件A就会报错:"No such file or directory"。这是软链接与硬链接最大的不同:文件A指向文件B的文件名,而不是文件B的inode号码,文件B的inode"链接数"不会因此发生变化。

ln -s命令可以创建软链接。

1
ln -s 源文文件或目录 目标文件或目录

八、inode的特殊作用

由于inode号码与文件名分离,这种机制导致了一些Unix/Linux系统特有的现象。

  1. 有时,文件名包含特殊字符,无法正常删除。这时,直接删除inode节点,就能起到删除文件的作用。

  2. 移动文件或重命名文件,只是改变文件名,不影响inode号码。

  3. 打开一个文件以后,系统就以inode号码来识别这个文件,不再考虑文件名。因此,通常来说,系统无法从inode号码得知文件名。

第3点使得软件更新变得简单,可以在不关闭软件的情况下进行更新,不需要重启。因为系统通过inode号码,识别运行中的文件,不通过文件名。更新的时候,新版文件以同样的文件名,生成一个新的inode,不会影响到运行中的文件。等到下一次运行这个软件的时候,文件名就自动指向新版文件,旧版文件的inode则被回收。