http://blog.chinaunix.net/uid-23095063-id-163160.html
1
|
|
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 |
|
http://blog.chinaunix.net/uid-23095063-id-163160.html
1
|
|
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 |
|
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 |
|
接收到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 |
|
如果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 |
|
如果是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 |
|
当返回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 |
|
如果是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 |
|
(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 |
|
可以看到返回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 |
|
我们前面提到过,__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()
函数的返回值。
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 |
|
然后我们要知道linux有两种方式执行tw状态的socket,一种是等待2×MSL时间(内核中是60秒),一种是基于RTO来计算超时时间。
基于RTO的超时时间也叫做recycle模式,这里内核会通过sysctl_tw_recycle(也就是说我们能通过sysctl来打开这个值)以及是否我们还保存有从对端接收到的最近的数据包的时间戳来判断是否进入打开recycle模式的处理。如果进入则会调用tcp_v4_remember_stamp来得到是否打开recycle模式。
下面就是这个片断的代码片断:
1 2 3 4 |
|
然后我们来看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 |
|
而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 |
|
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 |
|
然后我们来看__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 |
|
这里我们要知道还有一个专门的全局的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 |
|
这里要注意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 |
|
然后就是inet_twsk_schedule的实现,这个函数也就是tw状态的处理函数。他主要是用来基于超时时间来计算当前twsock的可用的位置。也就是来判断启动那个定时器,然后加入到那个队列。
因此这里的关键就是slot的计算。这里slot的计算是根据我们传递进来的timeo来计算的。
recycle模式下tcp_death_row的超时时间的就为2的INET_TWDR_RECYCLE_TICK幂。
我们一般桌面的hz为100,来看对应的值:
1 2 |
|
可以看到这时它的值就为4.
而tw_timer的slot也就是长时间定时器的slot的计算是这样的,它也就是用我们传递进来的超时时间timeo/16(可以看到就是2的INET_TWDR_RECYCLE_TICK次方)然后向上取整。
而这里twdr的period被设置为
1 2 3 4 |
|
而我们下面取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 |
|
我们先来总结一下上面的代码。当我们进入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 |
|
然后我们来看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 |
|
还有从wait1直接进入tw,和上面类似,我就不介绍了。
最后我们来看当内核处于tw状态后,再次接收到数据包后如何处理。这里的处理函数就是tcp_timewait_state_process,而他是在tcp_v4_rcv中被调用的,它会先判断是否处于tw状态,如果是的话,进入tw的处理。
这个函数的返回值分为4种。
1 2 3 4 5 6 7 8 9 10 11 |
|
这里可能最后一个比较难理解,这里内核注释得很详细,主要是实现了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 |
|
tcp_timewait_state_process这个函数具体的实现我就不介绍了,它就是分为两部分,一部分处理tw_substate == TCP_FIN_WAIT2的情况,一部分是正常情况。在前一种情况,我们对于syn的相应是直接rst的。而后一种我们需要判断是否新建连接。
而对于fin的处理他们也是不一样的,wait2的话,它会将当前的tw重新加入到定时器列表(inet_twsk_schedule).而后一种则只是重新发送ack。