TCP套接口的FIN_WAIT_2状态超时

2019-07-12 18:22发布

PROC文件tcp_fin_timeout默认为60秒,内核中相应的变量为init_net.ipv4.sysctl_tcp_fin_timeout,不过其以jiffies表示,默认值为TCP_FIN_TIMEOUT,即(60 * HZ)。此值表示一个不再被应用层使用(执行了close调用)的TCP连接处于FIN_WAIT_2状态的时长,如果在此时间内未能接收到对端的FIN结束报文,内核将复位此连接。但是除此之外,如果应用层是执行shutdown(SHUT_WR)操作关闭了套接口的发送,TCP连接还可进行接收操作,此种情况下的TCP连接处于FIN_WAIT_2状态不受tcp_fin_timeout的时间限制,将会永久的等待对端去关闭连接或者本地使用close关闭。 $ cat /proc/sys/net/ipv4/tcp_fin_timeout 60 $ static struct ctl_table ipv4_net_table[] = { { .procname = "tcp_fin_timeout", .data = &init_net.ipv4.sysctl_tcp_fin_timeout, }, } #define TCP_TIMEWAIT_LEN (60*HZ) #define TCP_FIN_TIMEOUT TCP_TIMEWAIT_LEN static int __net_init tcp_sk_init(struct net *net) { net->ipv4.sysctl_tcp_retries1 = TCP_RETR1; net->ipv4.sysctl_tcp_retries2 = TCP_RETR2; net->ipv4.sysctl_tcp_orphan_retries = 0; net->ipv4.sysctl_tcp_fin_timeout = TCP_FIN_TIMEOUT; }

linger2时长

sysctl_tcp_fin_timeout是针对所有套接口的全局配置,内核针对单个套接口的FIN_WAIT_2超时时间提供了linger2套接口选项,其值优先级高于全局的sysctl_tcp_fin_timeout。应用层可通过setsockopt系统调用设置套接口的linger2值,用户层以秒值下发,内核中将其装换为jiffies为单位的值保存在TCP套接口结构的linger2成员中。linger2的值不能够大于sysctl_tcp_fin_timeout的时间值,否则将其置为0。另外,如果用户层下发的值小于0,linger2设置为-1。TCP_LINGER2的设置逻辑如下: static int do_tcp_setsockopt(struct sock *sk, int level, int optname, char __user *optval, unsigned int optlen) { struct tcp_sock *tp = tcp_sk(sk); switch (optname) { case TCP_LINGER2: if (val < 0) tp->linger2 = -1; else if (val > net->ipv4.sysctl_tcp_fin_timeout / HZ) tp->linger2 = 0; else tp->linger2 = val * HZ; break; } } 内核中获取FIN_WAIT_2超时时间由函数tcp_fin_time实现。由其代码可见如果linger2的时间值不为零,取其值,否则,使用sysctl_tcp_fin_timeout的时间值。但是,最终的FIN_WAIT_2超时时间还与当前连接的超时重传时间RTO有关,其不能大于RTO的3.5倍(rto << 2) - (rto >> 1)的结果值。RTO*3.5表示的时长可允许对端的FIN报文重传2次。 static inline int tcp_fin_time(const struct sock *sk) { int fin_timeout = tcp_sk(sk)->linger2 ? : sock_net(sk)->ipv4.sysctl_tcp_fin_timeout; const int rto = inet_csk(sk)->icsk_rto; if (fin_timeout < (rto << 2) - (rto >> 1)) fin_timeout = (rto << 2) - (rto >> 1); return fin_timeout; }

超时定时器设置

FIN_WAIT_2状态的超时定时器设置分两种情况,其一是由应用层的shutdown系统调用所引发;其二是由close系统调用引发。先看第一种情况,shutdown可关闭接收或者发送方向的流量(仅关闭发送方向时触发FIN报文发送),导致套接口处于半关闭状态,并且套接口状态走到FIN_WAIT_1(关闭接收方向套接口状态不变),等待对端响应ACK报文。如果对端不响应ACK报文,本端FIN报文会进行超时重传,直到出错处理。 反之,当接收到对端回应的ACK报文时,处理流程进入函数tcp_rcv_state_process的TCP_FIN_WAIT1分支。由于此时应用层并没有close套接口,其SOCK_DEAD标志未设置,仅是将套接口的状态设置为TCP_FIN_WAIT2,退出处理流程,留待本端应用层close系统调用去结束连接;或者对端发送FIN报文来结束连接。 套接口的SOCK_DEAD标志没有置位还有一种可能是,应用层调用了close接口,但是设置了套接口的SOCK_LINGER选项,注意其与TCP_LINGER2不同,设置了此选项后,tcp_close函数不会立即置位SOCK_DEAD,而是等待sk_lingertime规定的时长,所以即使应用层调用了tcp_close操作,如果还在sk_lingertime时长内,SOCK_DEAD标志也还没有设置。通常应用层未设置SOCK_LINGER选项,这不是默认情况。如果是应用层进程在退出时自动调用了套接口的close函数,内核将忽略SOCK_LINGER选项设置不执行等待。 int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb) { switch (sk->sk_state) { case TCP_FIN_WAIT1: { tcp_set_state(sk, TCP_FIN_WAIT2); sk->sk_shutdown |= SEND_SHUTDOWN; if (!sock_flag(sk, SOCK_DEAD)) { sk->sk_state_change(sk); break; } 但是,如果本地的应用层直接使用close调用完全关闭双向的连接而不是shutdown,看一下tcp_rcv_state_process函数接下来的处理。如果用户层设置的套接口linger2值小于零,内核将不会在FIN_WAIT_2状态等待,直接销毁套接口。如果FIN_WAIT_2的超时时间(tmo)大于TCP_TIMEWAIT_LEN(60秒)的时长,启动keepalive定时器,定时时长为二者之差(tmo - TCP_TIMEWAIT_LEN)。以上对tcp_fin_time函数的介绍可知,默认情况下其值等于TCP_TIMEWAIT_LEN的值,内核默认不执行此分支。 如果完全关闭的套接口在FIN_WAIT_1状态时,接收到的是一个带有FIN标志的报文(FIN+ACK报文)或者此处的套接口还未被应用层释放。启动keepalive定时器,定时时长为FIN_WAIT_2的超时时间。需要注意的是sock_owned_by_user能够成立的条件十分苛刻:仅在tcp_close函数释放套接口owner前,release_sock函数的spin_lock_bh未获得锁而等待时发生。 对于HTTP服务端来说,如果客户端发送了FIN报文结束连接,HTTP服务端通常回复FIN+ACK报文,实现3个报文结束连接。所以通常情况下客户端会启动tmo时长的keepalive定时器。 if (tp->linger2 < 0) { tcp_done(sk); NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONDATA); return 1; } tmo = tcp_fin_time(sk); if (tmo > TCP_TIMEWAIT_LEN) { inet_csk_reset_keepalive_timer(sk, tmo - TCP_TIMEWAIT_LEN); } else if (th->fin || sock_owned_by_user(sk)) { inet_csk_reset_keepalive_timer(sk, tmo); } else { tcp_time_wait(sk, TCP_FIN_WAIT2, tmo); goto discard; } break; } } 除了以上情况之外,如果FIN_WAIT_2的超时时间小于等于TCP_TIMEWAIT_LEN的值,并且接收到的报文不带有FIN标志(仅ACK),此种情况为TCPIP协议中定义的FIN_WAIT_2状态。启动TIME_WAIT定时器。定时时长设置为FIN_WAIT_2的超时时间,如果其小于当前连接的重传超时时间RTO的话,使用RTO时间,以允许FIN报文至少重传一次。tcp_time_wait函数最后销毁TCP套接口。 void tcp_time_wait(struct sock *sk, int state, int timeo) { struct inet_timewait_sock *tw; if (timeo < rto) timeo = rto; inet_twsk_schedule(tw, timeo); tcp_done(sk); } 最后,接着半连接的套接口流程。如果应用层使用close系统调用主动关闭此半连接时,套接口的状态已经处于TCP_FIN_WAIT2了(tcp_rcv_state_process函数中设置)。如果TCP套接口的linger2小于零,直接发送重置reset报文;否则,根据FIN_WAIT_2的超时时间与TCP_TIMEWAIT_LEN的大小关系,启动keepalive定时器或者time_wait定时器,防止对端一直不发送FIN结束报文导致本端套接口不能释放。 但是,如果本地应用层不close此半连接,并且对端也不结束连接,不发送FIN报文,此连接将一直存在。 void tcp_close(struct sock *sk, long timeout) { if (sk->sk_state == TCP_FIN_WAIT2) { struct tcp_sock *tp = tcp_sk(sk); if (tp->linger2 < 0) { tcp_set_state(sk, TCP_CLOSE); tcp_send_active_reset(sk, GFP_ATOMIC); __NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONLINGER); } else { const int tmo = tcp_fin_time(sk); if (tmo > TCP_TIMEWAIT_LEN) { inet_csk_reset_keepalive_timer(sk, tmo - TCP_TIMEWAIT_LEN); } else { tcp_time_wait(sk, TCP_FIN_WAIT2, tmo); goto out; } } } }

定时器处理

如上所述套接口的FIN_WAIT_2状态没有单独的定时器,其使用TCP的保活定时器keepalive和time_wait定时器实现计时。保活定时器超时处理函数为tcp_keepalive_timer,如果TCP套接口的linger2大于等于零,并且tcp_fin_time得到的超时时间大于TCP_TIMEWAIT_LEN(60秒)时间时,启动time_wait定时器,定时时长设置为二者差值;否则,直接发送重置reset报文到对端。默认情况下tcp_fin_time时间等于TCP_TIMEWAIT_LEN时间,故在FIN_WAIT_2状态超时的处理为发送连接重置reset报文。 static void tcp_keepalive_timer (struct timer_list *t) { struct sock *sk = from_timer(sk, t, sk_timer); struct tcp_sock *tp = tcp_sk(sk); if (sk->sk_state == TCP_FIN_WAIT2 && sock_flag(sk, SOCK_DEAD)) { if (tp->linger2 >= 0) { const int tmo = tcp_fin_time(sk) - TCP_TIMEWAIT_LEN; if (tmo > 0) { tcp_time_wait(sk, TCP_FIN_WAIT2, tmo); goto out; } } tcp_send_active_reset(sk, GFP_ATOMIC); goto death; } } 定时器time_wait的超时处理函数为tw_timer_handler,清理time_wait套接口。 static void tw_timer_handler(struct timer_list *t) { struct inet_timewait_sock *tw = from_timer(tw, t, tw_timer); if (tw->tw_kill) __NET_INC_STATS(twsk_net(tw), LINUX_MIB_TIMEWAITKILLED); else __NET_INC_STATS(twsk_net(tw), LINUX_MIB_TIMEWAITED); inet_twsk_kill(tw); }   内核版本 4.15.0