TCP套接口的FIN_WAIT_2状态超时

发布于:2023-01-22 ⋅ 阅读:(6) ⋅ 点赞:(0) ⋅ 评论:(0)

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定时器。

bind失败的解决方案
docx

5星
超过95%的资源
27KB

下载
        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);
}