/* * This is a TIME_WAIT sock. It works around the memory consumption * problems of sockets in such a state on heavily loaded servers, but * without violating the protocol specification. */ 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 *///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 #define tw_daddr __tw_common.skc_daddr #define tw_rcv_saddr __tw_common.skc_rcv_saddr int tw_timeout;//tw状态的超时时间 //这个用来标记我们是正常进入tw还是说由于超时等一系列原因进入(比如超时等一系列原因) volatile unsigned char tw_substate; unsigned char tw_rcv_wscale; /* Socket demultiplex comparisons on incoming packets. */ /* these three are in inet_sock */ //也就是标示sock的个域,源目的端口和地址 __be16 tw_sport; __be16 tw_dport; __u16 tw_num;//本地端口 kmemcheck_bitfield_begin(flags); /* And these are ours. */ unsigned int tw_ipv6only : 1, tw_transparent : 1, tw_pad : 6, /* 6 bits hole */ tw_tos : 8, tw_ipv6_offset : 16; kmemcheck_bitfield_end(flags); unsigned long tw_ttd; struct inet_bind_bucket *tw_tb;//链接到端口的hash表中 //链接到全局的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模式
/* * Move a socket to time-wait or dead fin-wait-2 state. */ /* * @sk: 被取代的传输控制块。 * @state: timewait控制块内部的状态,为FIN_WAIT2或TIME_WAIT * @timeo: 等待超时时间 //本端发送的fin已经收到确认,等待对方发送fin,或者主动关闭端收到了第二个fin进入time_wait状态 sock结构进入TIME_WAIT状态有两种情况:一种是在真正进入了TIME_WAIT状态,还有一种是真实的状态是FIN_WAIT_2的TIME_WAIT状态。之所以让FIN_WAIT_2状态在没有 接收到FIN包的情况下也可以进入TIME_WAIT状态是因为tcp_sock结构占用的资源要比tcp_timewait_sock结构占用的资源多,而且在TIME_WAIT下也可以处理连接的关闭。 内核在处理时通过inet_timewait_sock结构的tw_substate成员来区分这种两种情况。 */ //参考:http://blog.csdn.net/justlinux2010/article/details/9070057 /* tcp_tw_reuse参数用来设置是否可以在新的连接中重用TIME_WAIT状态的套接字。注意,重用的是TIME_WAIT套接字占用的端口号,而不是TIME_WAIT套接字的内存等。这个参数对客户端有意义, 在主动发起连接的时候会在调用的inet_hash_connect()中会检查是否可以重用TIME_WAIT状态的套接字。如果你在服务器段设置这个参数的话,则没有什么作用, 因为服务器端ESTABLISHED状态的套接字和监听套接字的本地IP、端口号是相同的,没有重用的概念。但并不是说服务器端就没有TIME_WAIT状态套接字。 tcp_fin_timeout参数 有些人对这个参数会有误解,认为这个参数是用来设置TIME_WAIT状态持续的时间的。linux的内核文档说的很明白,这个参数是用来设置保持在FIN_WAIT_2状态的时间. tcp_fin_timeout - INTEGER Time to hold socket in state FIN-WAIT-2, if it was closed by our side. Peer can be broken and never close its side, or even died unexpectedly. Default value is 60sec. Usual value used in 2.2 was 180 seconds, you may restore it, but remember that if your machine is even underloaded WEB server, you risk to overflow memory with kilotons of dead sockets, FIN-WAIT-2 sockets are less dangerous than FIN-WAIT-1, because they eat maximum 1.5K of memory, but they tend to live longer. Cf. tcp_max_orphans. */ void tcp_time_wait(struct sock *sk, int state, int timeo) { struct inet_timewait_sock *tw = NULL; const struct inet_connection_sock *icsk = inet_csk(sk); const struct tcp_sock *tp = tcp_sk(sk); int recycle_ok = 0; /* * 如果启用tw_recycle,且ts_recent_stamp有效,则记录 * 相关时间戳信息到对端信息管理块中 tcp_timestamps参数用来设置是否启用时间戳选项,tcp_tw_recycle参数用来启用快速回收TIME_WAIT套接字。tcp_timestamps参数会影响到 tcp_tw_recycle参数的效果。如果没有时间戳选项的话,tcp_tw_recycle参数无效, */ if (tcp_death_row.sysctl_tw_recycle && tp->rx_opt.ts_recent_stamp) /* * 调用的是tcp_v4_remember_stamp()。 如果没有时间戳选项,tp->rx_opt.ts_recent_stamp的值为0,这样局部变量recycle_ok的值为0,在后面就会使用默认的时间TCP_TIMEWAIT_LEN(60s) 作为TIME_WAIT状态的时间长度 允许重用timewait传输控制块,并且成功记录了时间戳,则recycle_ok为1 tcp_timestamps参数用来设置是否启用时间戳选项,tcp_tw_recycle参数用来启用快速回收TIME_WAIT套接字。tcp_timestamps参数会影响到tcp_tw_recycle参数的效果。如果没有时间戳选项的话,tcp_tw_recycle参数无效 */ recycle_ok = icsk->icsk_af_ops->remember_stamp(sk); /* * 如果当前系统中TIME_WATI状态的套接字数未 * 达到最大值,则允许分配timewait控制块。 * inet_twsk_alloc()用来分配timewait控制块,并根据 * 传输控制块设置其对应的属性和内部状态 */ if (tcp_death_row.tw_count < tcp_death_row.sysctl_max_tw_buckets) tw = inet_twsk_alloc(sk, state); /* * 如果timewait控制块分配成功,则做相应设置, * 同时进入TIME_WAIT状态 */ if (tw != NULL) { //所以在TIME_WAIT套接字数量超过系统限制或者内存不足 struct tcp_timewait_sock *tcptw = tcp_twsk((struct sock *)tw); /* * 根据超时重传时间计算TIME_WAIT状态的 * 超时时间,后者是前者的3.5倍。 * 为什么是3.5倍参见inet_twsk_schedule()函数 下面在来看看为什么rto的值要选择为icsk->icsk_rto的3.5倍,也就是RTO*3.5,而不是2倍、4倍呢?我们知道,在FIN_WAIT_2状态下接收到FIN包后,会给对 端发送ACK包,完成TCP连接的关闭。但是最后的这个ACK包可能对端没有收到,在过了RTO(超时重传时间)时间后,对端会重新发送FIN包,这时需要再次给对 端发送ACK包,所以TIME_WAIT状态的持续时间要保证对端可以重传两次FIN包。如果重传两次的话,TIME_WAIT的时间应该为RTO*(0.5+0.5+0.5)=RTO*1.5,但是 这里却是RTO*3.5。这是因为在重传情况下,重传超时时间采用一种称为“指数退避”的方式计算。例如:当重传超时时间为1S的情况下发生了数据重传,我们就用 重传超时时间为2S的定时器来重传数据,下一次用4S,一直增加到64S为止(参见tcp_retransmit_timer())。所以这里的RTO*3.5=RTO*0.5+RTO*1+RTO*2,其中 RTO*0.5是第一次发送ACK的时间到对端的超时时间(系数就是乘以RTO的值),RTO*1是对端第一次重传FIN包到ACK包到达对端的超时时间,RTO*2是对端第二次重传 FIN包到ACK包到达对端的超时时间。注意,重传超时时间的指数退避操作(就是乘以2)是在重传之后执行的,所以第一次重传的超时时间和第一次发送的超时时间 相同。整个过程及时间分布如下图所示(注意:箭头虽然指向对端,只是用于描述过程,数据包并未被接收到):参考:http://blog.csdn.net/justlinux2010/article/details/9070057 */ const int rto = (icsk->icsk_rto << 2) - (icsk->icsk_rto >> 1);//icsk->icsk_rto的值是超时重传的时间,这个值是根据网络情况动态计算的 /* * 从TCP控制块中获取对应的属性值 * 设置到timewait控制块中 ///更新对应的域。 */ 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; #if defined(CONFIG_IPV6) || defined(CONFIG_IPV6_MODULE) if (tw->tw_family == PF_INET6) { struct ipv6_pinfo *np = inet6_sk(sk); struct inet6_timewait_sock *tw6; tw->tw_ipv6_offset = inet6_tw_offset(sk->sk_prot); tw6 = inet6_twsk((struct sock *)tw); ipv6_addr_copy(&tw6->tw_v6_daddr, &np->daddr); ipv6_addr_copy(&tw6->tw_v6_rcv_saddr, &np->rcv_saddr); tw->tw_ipv6only = np->ipv6only; } #endif #ifdef CONFIG_TCP_MD5SIG /* * The timewait bucket does not have the key DB from the * sock structure. We just make a quick copy of the * md5 key being used (if indeed we are using one) * so the timewait ack generating code has the key. */ do { struct tcp_md5sig_key *key; memset(tcptw->tw_md5_key, 0, sizeof(tcptw->tw_md5_key)); tcptw->tw_md5_keylen = 0; key = tp->af_specific->md5_lookup(sk, sk); if (key != NULL) { memcpy(&tcptw->tw_md5_key, key->key, key->keylen); tcptw->tw_md5_keylen = key->keylen; if (tcp_alloc_md5sig_pool(sk) == NULL) BUG(); } } while (0); #endif /* Linkage updates. */ /* * 将timewait控制块添加到tcp_hashinfo的ebash散列表中, * 将被替代的TCP控制块从ehash散列表中删除。这样 * FIN_WAIT2和TIME_WAIT状态下也可以进行输入的处理。 * 同时将该timewait控制块添加到bhash散列表中,但 * 并不删除该散列表中被替代的TCP控制块,因为 * 只要inet->num不为0,这个绑定关系就存在, * 即使该套接字已经关闭 *///更新链表(下面会分析)。 __inet_twsk_hashdance(tw, sk, &tcp_hashinfo); /* Get the TIME_WAIT timeout firing. */ /* * TIME_WAIT的超时时间不得小于3.5倍的超时 * 重传的时间 *///如果传递进来的超时时间小于我们计算的,则让他等于我们计算的超时时间。 if (timeo < rto) timeo = rto; /* * 允许重用timewait传输控制块,并且成功记录了时间戳, * 则recycle_ok为1,此时会使用rto来设置真正的TIME-WAIT * 状态的时间(参见tcp_timewait_state_process()), * 否则使用固定的TCP_TIMEWAIT_LEN来设置TIME-WAIT状态的 * 时间。 如果没有时间戳选项,tp->rx_opt.ts_recent_stamp的值为0,这样局部变量recycle_ok的值为0,在后面就会使用默认的时间TCP_TIMEWAIT_LEN(60s) 作为TIME_WAIT状态的时间长度 *///如果打开recycle模式,则超时时间为我们基于rto计算的时间。 if (recycle_ok) {//在设置tcp_tw_recycle参数的情况下,tw->tw_timeout的值为rto,否则为TCP_TIMEWAIT_LEN。所以tcp_tw_recycle参数如果要实现对回收TIME_WAIT状态套接字的加速,需要这个时间rto小于TCP_TIMEWAIT_LEN。rto的值由下面的式子计算: 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; } /* * 进入TIME_WAIT状态,并启动TIME_WAIT定时器,超时时间 * 为timeo,但是上限为TCP_TIMEWAIT_LEN,即超时时间最多 * 不能超过TCP_TIMEWAIT_LEN。 */ //最关键的一个函数,我们后面会详细分析。 inet_twsk_schedule(tw, &tcp_death_row, timeo, TCP_TIMEWAIT_LEN); inet_twsk_put(tw); //这里后会在后面释放原来的struct sock } else { /* Sorry, if we're out of memory, just CLOSE this * socket up. We've got bigger problems than * non-graceful socket closings. */ LIMIT_NETDEBUG(KERN_INFO "TCP: time wait bucket table overflow "); } /* * 将TCP中的一些测量值更新到它路由缓存项的 * 度量值中,然后关闭并释放传输控制块 */ tcp_update_metrics(sk); tcp_done(sk); } void tcp_done(struct sock *sk)
__inet_twsk_hashdance函数将tw_sock加入到bind hash表和ESTABLISHED表中,这样在tw_sock被删除之前相应IP|端口不允许bind,也不允许建立:
/* * Enter the time wait state. This is called with locally disabled BH. * Essentially we whip up a timewait bucket, copy the relevant info into it * from the SK, and mess with hash chains and list linkage. 主要是用于更新对应的全局hash表 */ 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; /* Step 1: Put TW into bind hash. Original socket stays there too. Note, that any socket with inet->num != 0 MUST be bound in binding cache, even if it is closed. */ //下面这几步是将tw sock链接到bhash中。 bhead = &hashinfo->bhash[inet_bhashfn(twsk_net(tw), inet->inet_num, hashinfo->bhash_size)]; spin_lock(&bhead->lock);///链接到bhash。这里icsk的icsk_bind_hash也就是bash的一个元素。 tw->tw_tb = icsk->icsk_bind_hash;//加入到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); /* * Step 2: Hash TW into TIMEWAIT chain. * Should be done before removing sk from established chain * because readers are lockless and search established first. *///将tw sock加入到ehash的tw chain中。 inet_twsk_add_node_rcu(tw, &ehead->twchain); /* Step 3: Remove SK from established hash. */ //然后从全局的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); /* * Notes : * - We initially set tw_refcnt to 0 in inet_twsk_alloc() * - We add one reference for the bhash link * - We add one reference for the ehash link * - We want this refcnt update done before allowing other * threads to find this tw in ehash chain. */ atomic_add(1 + 1 + 1, &tw->tw_refcnt); spin_unlock(lock); }
在应用进程使用bind系统调用绑定与tw_sock相同的IP|端口对时内核会用到inet_csk_bind_conflict函数,但由于成功匹配到bind hash表中的tw_sock,会导致冲突,无法bind(Bind系统调用)。
而在建立连接时,inet_hash_connect函数会调用__inet_check_established检查即将建立的连接是否与已建立的连接冲突:
static int __inet_check_established(struct inet_timewait_death_row *death_row, struct sock *sk, __u16 lport, struct inet_timewait_sock **twp) { sk_nulls_for_each(sk2, node, &head->twchain) { if (sk2->sk_hash != hash) continue; if (likely(INET_TW_MATCH(sk2, net, acookie, saddr, daddr, ports, dif))) { //地址|端口匹配 tw = inet_twsk(sk2); if (twsk_unique(sk, sk2, twp)) //调用tcp_twsk_unique判断是否冲突 goto unique; //不冲突 else goto not_unique; //冲突 } } tw = NULL; unique: if (twp) { *twp = tw; //交给调用者处理 } else if (tw) { /* Silly. Should hash-dance instead... */ inet_twsk_deschedule(tw, death_row); inet_twsk_put(tw); } return 0; not_unique: spin_unlock(lock); return -EADDRNOTAVAIL; } 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); ... /* With PAWS, it is safe from the viewpoint of data integrity. Even without PAWS it is safe provided sequence spaces do not overlap i.e. at data rates <= 80Mbit/sec. Actually, the idea is close to VJ's one, only timestamp cache is held not per host, but per port pair and TW bucket is used as state holder. If TW bucket has been already destroyed we fall back to VJ's scheme and use initial timestamp retrieved from peer table. */ if (tcptw->tw_ts_recent_stamp && //开启时间戳选项且在TIME_WAIT状态下收到过包 (twp == NULL || (sysctl_tcp_tw_reuse && get_seconds() - tcptw->tw_ts_recent_stamp > 1))) { tp->write_seq = tcptw->tw_snd_nxt + 65535 + 2; if (tp->write_seq == 0) tp->write_seq = 1; tp->rx_opt.ts_recent = tcptw->tw_ts_recent; tp->rx_opt.ts_recent_stamp = tcptw->tw_ts_recent_stamp; sock_hold(sktw); return 1; } return 0; } /* 可见,当: (1)__inet_check_established函数的调用者不需要返回tw_sock的时候(即twp == NULL为真),或 (2)应用进程设置了net.ipv4.tcp_tw_reuse内核选项允许tw_sock重用时, tcp_twsk_unique函数会返回1,即不冲突。不冲突时如果是(1),则__inet_check_established函数会释放tw_sock; 否则会将tw_sock返回给调用者inet_hash_connect函数处理。在不冲突时, 情况(1)发生时到底意味着什么? 情况(1)没有发生时inet_hash_connect函数用tw_sock干什么?来看代码: 看来__inet_check_established函数的使用者是__inet_hash_connect函数: */
可见,当: (1)__inet_check_established函数的调用者不需要返回tw_sock的时候(即twp == NULL为真),或 (2)应用进程设置了net.ipv4.tcp_tw_reuse内核选项允许tw_sock重用时, tcp_twsk_unique函数会返回1,即不冲突。不冲突时如果是(1),则__inet_check_established函数会释放tw_sock; 否则会将tw_sock返回给调用者inet_hash_connect函数处理。在不冲突时, 情况(1)发生时到底意味着什么? 情况(1)没有发生时inet_hash_connect函数用tw_sock干什么?来看代码: 看来__inet_check_established函数的使用者是__inet_hash_connect函数:
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 **), int (*hash)(struct sock *sk, struct inet_timewait_sock *twp)) { struct inet_hashinfo *hinfo = death_row->hashinfo; const unsigned short snum = inet_sk(sk)->inet_num; struct inet_bind_hashbucket *head; struct inet_bind_bucket *tb; int ret; struct net *net = sock_net(sk); int twrefcnt = 1; if (!snum) {//端口未绑定 int i, remaining, low, high, port; static u32 hint; u32 offset = hint + port_offset; struct hlist_node *node; struct inet_timewait_sock *tw = NULL; inet_get_local_port_range(&low, &high); remaining = (high - low) + 1; local_bh_disable(); for (i = 1; i <= remaining; i++) { port = low + (i + offset) % remaining; if (inet_is_reserved_local_port(port)) continue; head = &hinfo->bhash[inet_bhashfn(net, port, hinfo->bhash_size)]; spin_lock(&head->lock); /* Does not bother with rcv_saddr checks, * because the established check is already * unique enough. //绑定到一个port的socket可能是通过bind 系统调用,也可能是调用connect系统调用时__inet_hash_connect函数选取的 */ inet_bind_bucket_for_each(tb, node, &head->chain) { if (net_eq(ib_net(tb), net) && tb->port == port) { if (tb->fastreuse >= 0) goto next_port; WARN_ON(hlist_empty(&tb->owners)); if (!check_established(death_row, sk, port, &tw)) goto ok; goto next_port; } } //当前端口没有被使用 tb = inet_bind_bucket_create(hinfo->bind_bucket_cachep, net, head, port); if (!tb) { spin_unlock(&head->lock); break; } tb->fastreuse = -1; goto ok; next_port: spin_unlock(&head->lock); } local_bh_enable(); return -EADDRNOTAVAIL; ok: hint += i; /* Head lock still held and bh's disabled //将socket加入port对应的tb的socket队列中,即将此socket与port相关联 要绑定的端口非0情况(1)才会发生,这时意味着应用进程在调用connect系统调用之前已经成功地使用了bind系统调用,既然bind时不冲突, 那么在connect时直接将tw_sock释放即可。而情况(1)没有发生时,tw_sock也会被释放并从hash表中摘出。 */ inet_bind_hash(sk, tb, port); if (sk_unhashed(sk)) { //如果socket没有被加入到“已建立连接”的连接表中 inet_sk(sk)->inet_sport = htons(port); twrefcnt += hash(sk, tw);//将socket加入到“已建立连接”的连接表中//将sk加入到ESTABLISHED hash表中,将tw_sock从这个表中摘出 } if (tw) twrefcnt += inet_twsk_bind_unhash(tw, hinfo);//将tw_sock从bind hash表中摘出 spin_unlock(&head->lock); if (tw) { inet_twsk_deschedule(tw, death_row);//释放tw_sock while (twrefcnt) { twrefcnt--; inet_twsk_put(tw); } } ret = 0; goto out; } head = &hinfo->bhash[inet_bhashfn(net, snum, hinfo->bhash_size)]; tb = inet_csk(sk)->icsk_bind_hash;//将tb加入到bind hash表中 spin_lock_bh(&head->lock); //条件为false时,会执行else分支,检查是否可用。这么看来,调用bind()成功并不意味着这个端口就真的可以用 if (sk_head(&tb->owners) == sk && !sk->sk_bind_node.next) {//有且仅有一个socket绑定到这个端口,无需冲突检查 hash(sk, NULL);//将socket加入到“已建立连接”的连接表中 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; } }
全局的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定时器。
struct inet_timewait_death_row { /* Short-time timewait calendar */ ///这几个域会在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; int tw_count;//tw的个数。 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。
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,///可以看到它是链接到全局的inet_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,来看对应的值
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被设置为
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秒的都放在最后一个桶。
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; /* timeout := RTO * 3.5 * * 3.5 = 1+2+0.5 to wait for two retransmits. * * RATIONALE: if FIN arrived and we entered TIME-WAIT state, * our ACK acking that FIN can be lost. If N subsequent retransmitted * FINs (or previous seqments) are lost (probability of such event * is p^(N+1), where p is probability to lose single packet and * time to detect the loss is about RTO*(2^N - 1) with exponential * backoff). Normal timewait length is calculated so, that we * waited at least for one retransmitted FIN (maximal RTO is 120sec). * [ BTW Linux. following BSD, violates this requirement waiting * only for 60sec, we should wait at least for 240 secs. * Well, 240 consumes too much of resources 8) * ] * This interval is not reduced to catch old duplicate and * responces to our wandering segments living for two MSLs. * However, if we use PAWS to detect * old duplicates, we can reduce the interval to bounds required * by RTO, rather than MSL. So, if peer understands PAWS, we * kill tw bucket after 3.5*RTO (it is important that this number * is greater than TS tick!) and detect old duplicates with help * of PAWS. *///得到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 */ if (timeo >= timewait_len) {//如果大于timewait_len也就是2*MSL=60秒,则slot为cells的最后一项。 slot = INET_TWDR_TWKILL_SLOTS - 1; } else {//否则timeo除于period然后向上取整。 slot = DIV_ROUND_UP(timeo, twdr->period); if (slot >= INET_TWDR_TWKILL_SLOTS)//如果大于cells的桶的大小,则也是放到最后一个位置。 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模式的区别:简单的介绍下两个超时函数,一个是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。
void inet_twdr_twcal_tick(unsigned long data) { struct inet_timewait_death_row *twdr; int n, slot; unsigned long j; unsigned long now = jiffies; int killed = 0; int adv = 0; twdr = (struct inet_timewait_death_row *)data; spin_lock(&twdr->death_lock); if (twdr->twcal_hand < 0) goto out; slot = twdr->twcal_hand;//得到slot。 j = twdr->twcal_jiffie;//得到定时器启动时候的jiffes。 for (n = 0; n < INET_TWDR_RECYCLE_SLOTS; n++) {//遍历所有的twscok。 if (time_before_eq(j, now)) {//判断是否超时。 struct hlist_node *node, *safe; struct inet_timewait_sock *tw; ///处理超时的socket inet_twsk_for_each_inmate_safe(tw, node, safe, &twdr->twcal_row[slot]) { __inet_twsk_del_dead_node(tw); __inet_twsk_kill(tw, twdr->hashinfo); #ifdef CONFIG_NET_NS NET_INC_STATS_BH(twsk_net(tw), LINUX_MIB_TIMEWAITKILLED); #endif inet_twsk_put(tw); killed++; } } 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; out: if ((twdr->tw_count -= killed) == 0) del_timer(&twdr->tw_timer); #ifndef CONFIG_NET_NS NET_ADD_STATS_BH(&init_net, LINUX_MIB_TIMEWAITKILLED, killed); #endif spin_unlock(&twdr->death_lock); }
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的处理。
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种。
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". 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。