首页 > 其他 > 详细

tcp 保活定时器分析 & Fin_WAIT_2 定时器

时间:2020-05-02 17:53:31      阅读:39      评论:0      收藏:0      [点我收藏+]

tcp keepalive定时器

http server 和client端需要防止“僵死”链接过多!也就是建立了tcp链接,但是没有报文交互, 或者client 由于主机突然掉电!但是server 不知道! 所以需要有一种检测机制,检查tcp连接是否活着在也就是有报文交互!!

也就是检测:对方是否down了

  在启用了保活定时器的情况下,如果连接超过空闲时间没有数据交互,则保活定时器超时,向对端发送保活探测包,

若(1)收到回复则说明对端工作正常,重置定时器等下下次达到空闲时间;

(2) 收到其他回复,则确定对端已重启,关闭连接;

(3) 超过探测次数仍未得到回复,则认为对端主机已经崩溃,关闭连接;

 

 

    case SO_KEEPALIVE://开启
        if (sk->sk_protocol == IPPROTO_TCP &&
            sk->sk_type == SOCK_STREAM)
            tcp_set_keepalive(sk, valbool);
        sock_valbool_flag(sk, SOCK_KEEPOPEN, valbool);
        break;
    case TCP_KEEPIDLE: //keepalive时间, 超过该时间才会开始探测
        val = keepalive_time_when(tp) / HZ;
        break;
    case TCP_KEEPINTVL://超过keepalive时间后,每次探测的间隔时间
        val = keepalive_intvl_when(tp) / HZ;
        break;
    case TCP_KEEPCNT://keepalive最大探测次数
        val = keepalive_probes(tp);
        break;

static inline int keepalive_time_when(const struct tcp_sock *tp)
{
    struct net *net = sock_net((struct sock *)tp);

    return tp->keepalive_time ? : net->ipv4.sysctl_tcp_keepalive_time;
}

static inline int keepalive_probes(const struct tcp_sock *tp)
{
    struct net *net = sock_net((struct sock *)tp);

    return tp->keepalive_probes ? : net->ipv4.sysctl_tcp_keepalive_probes;
}
static inline int keepalive_intvl_when(const struct tcp_sock *tp)
{
    struct net *net = sock_net((struct sock *)tp);

    return tp->keepalive_intvl ? : net->ipv4.sysctl_tcp_keepalive_intvl;
}


void tcp_set_keepalive(struct sock *sk, int val)
{
    if ((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_LISTEN))
        return;

    if (val && !sock_flag(sk, SOCK_KEEPOPEN))////第一次setsockopt enable 但是还没有启动定时器则启动定时器
        inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tcp_sk(sk)));//设置定时器回调函数 
    else if (!val)
        inet_csk_delete_keepalive_timer(sk);
}

 

net.ipv4.tcp_keepalive_intvl = 75 //每次探测间隔75秒
net.ipv4.tcp_keepalive_probes = 9 //9次
net.ipv4.tcp_keepalive_time = 7200 //2小时
系统默认会在连接空闲2小时后,开始探测,总共探测9次,每次间隔75秒。

Q1:定时器何时启动??

1、对非listen socket设置SO_KEEPALIVE的时候, 或者已经设置了SO_KEEPALIVE的socket上,设置TCP_KEEPIDLE的时候重置定时器时间

    case SO_KEEPALIVE:
#ifdef CONFIG_INET
        if (sk->sk_protocol == IPPROTO_TCP &&
            sk->sk_type == SOCK_STREAM)
            tcp_set_keepalive(sk, valbool);
#endif
        sock_valbool_flag(sk, SOCK_KEEPOPEN, valbool);
        break;
void tcp_set_keepalive(struct sock *sk, int val)
{
    if ((1 << sk->sk_state) & (TCPF_CLOSE | TCPF_LISTEN))
        return;//close和listen状态不需要设置定时器

    if (val && !sock_flag(sk, SOCK_KEEPOPEN))//第一次setsockopt
        inet_csk_reset_keepalive_timer(sk, keepalive_time_when(tcp_sk(sk)));
    else if (!val)//删除定时器
        inet_csk_delete_keepalive_timer(sk);
}

 

 

2、客户端收到synack,进入TCP_ESTABLISHED的时候,如果设置了SO_KEEPALIVE;

查看tcp_finish_connect 函数实现可知

 

 

static void tcp_keepalive_timer (unsigned long data)
{
    struct sock *sk = (struct sock *) data;
    struct inet_connection_sock *icsk = inet_csk(sk);
    struct tcp_sock *tp = tcp_sk(sk);
    u32 elapsed;

    /* Only process if socket is not in use. */
    bh_lock_sock(sk);
    if (sock_owned_by_user(sk)) { //应用程序在使用该sock则不处理
        /* Try again later. */
        inet_csk_reset_keepalive_timer (sk, HZ/20);
        goto out;
    }

    if (sk->sk_state == TCP_LISTEN) {
        pr_err("Hmm... keepalive on a LISTEN ???\n");
        goto out;
    }
    /* 连接释放期间,用作FIN_WAIT2定时器 */ 
    /*
     * 处理FIN_WAIT_2状态定时器时,TCP状态必须为
     * FIN_WAIT_2且套接字状态为DEAD。
     */ //tcp_rcv_state_process中收到第一个FIN ack后会进入TCP_FIN_WAIT2状态
    if (sk->sk_state == TCP_FIN_WAIT2 && sock_flag(sk, SOCK_DEAD)) {
        //TCP关闭过程中的定时器处理过程,从tcp_rcv_state_process跳转过来
         /*
         * 停留在FIN_WAIT_2状态的时间大于或等于0的情况下,
         * 如果FIN_WAIT_2定时器剩余时间大于0,则调用
         * tcp_time_wait()继续处理;否则给对端发送RST后
         * 关闭套接字。
         */
         /*
TIME_WAIT_2定时器超时触发,如果linger2<0,或者等待时间<=TIMEWAIT_LEN,
直接发送reset关闭连接;如果linger2>=0,且等待时间>TIMEWAIT_LEN,
则进入TIME_WAIT接管;
        */
        if (tp->linger2 >= 0) {/* 停留在FIN_WAIT_2的停留时间>=0 */
            const int tmo = tcp_fin_time(sk) - TCP_TIMEWAIT_LEN;/* 获取时间差值 */

            if (tmo > 0) { /* 差值>0,等待时间>TIME_WAIT时间,则进入TIME_WAIT状态 */
                tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
                goto out;
            }
        }
        tcp_send_active_reset(sk, GFP_ATOMIC);
        goto death;
    }

    if (!sock_flag(sk, SOCK_KEEPOPEN) || sk->sk_state == TCP_CLOSE)
        goto out;

    elapsed = keepalive_time_when(tp);

    /* It is alive without keepalive 8) */
     /*
     * 如果有已输出未确认的段,或者发送队列中还
     * 存在未发送的段,则无需作处理,只需重新设
     * 定保活定时器的超时时间。
     */
     /* 1 tp->packets_out判断是否有任何已经传输可是还没有确认的数据包。 
*  2 tcp_send_head用来判断是否有将要发送的包 
* 如果上面有任何一个条件为真,就说明这个连接并不是处于idle状态,此时我们就重启定 *时器。 
*/  
    if (tp->packets_out || tcp_send_head(sk))
        goto resched;
/* 连接经历的空闲时间,即上次收到报文至今的时间 */  
    elapsed = keepalive_time_elapsed(tp);
/*接下来比较idle时间有没有超过keep alive的设置的间隔时间,如果超过了,则说明我 *们需要 发送探测包了。如果没有,则我们需要重新调整keep alive的超时时间。 
*/  
    if (elapsed >= keepalive_time_when(tp)) {
        /* If the TCP_USER_TIMEOUT option is enabled, use that
         * to determine when to timeout instead.
         */
          /*
         * 如果持续空闲时间超过了允许时间,并且在未设置
         * 保活探测次数时,已发送保活探测段数超过了系统
         * 默认的允许数tcp_keepalive_probes;或者在已设置保活探测
         * 段的次数时,已发送次数超过了保活探测次数,则
         * 需要断开连接,给对方发送RST段,并报告相应错误,
         * 关闭相应的传输控制块。
         */
        if ((icsk->icsk_user_timeout != 0 &&
            elapsed >= icsk->icsk_user_timeout &&
            icsk->icsk_probes_out > 0) ||
            (icsk->icsk_user_timeout == 0 &&
            icsk->icsk_probes_out >= keepalive_probes(tp))) {
            tcp_send_active_reset(sk, GFP_ATOMIC);
            tcp_write_err(sk);
            goto out;
        }
        /* 发送保活段,并计算下次激活保活定时器的时间。*/
        if (tcp_write_wakeup(sk, LINUX_MIB_TCPKEEPALIVE) <= 0) {
            icsk->icsk_probes_out++;
            elapsed = keepalive_intvl_when(tp);
        } else {
            /* If keepalive was lost due to local congestion,
             * try harder.
             */
            elapsed = TCP_RESOURCE_PROBE_INTERVAL;
        }
    } else {
        /* It is tp->rcv_tstamp + keepalive_time_when(tp) */
        elapsed = keepalive_time_when(tp) - elapsed;
    }

    sk_mem_reclaim(sk);

resched:
    inet_csk_reset_keepalive_timer (sk, elapsed);
    goto out;

death:
    tcp_done(sk);

out:
    bh_unlock_sock(sk);
    sock_put(sk);
}

 

//?lrcvtime是最后一次接收到数据报的时间 
//rcv_tstamp是最后一次接收到ACK的时间 
static inline u32 keepalive_time_elapsed(const struct tcp_sock *tp)
{
    const struct inet_connection_sock *icsk = &tp->inet_conn;

    return min_t(u32, tcp_time_stamp - icsk->icsk_ack.lrcvtime,
              tcp_time_stamp - tp->rcv_tstamp);
}

 

 

 

/* Initiate keepalive or window probe from timer. */
/*
 * tcp_write_wakeup()用来输出持续探测段。如果传输
 * 控制块处于关闭状态,则直接返回失败,否
 * 则传输持续探测段,过程如下:
 * 1)如果发送队列不为空,则利用那些待发送
 *    段来发送探测段,当然这些待发送的段至
 *     少有一部分在对方的接收窗口内。
 * 2)如果发送队列为空,则构造需要已确认,
 *    长度为零的段发送给对端。也就是否则最终会发送序号为snd_una-1,长度为0的ack包
 * 其返回值如下:
 *  0: 表示发送持续探测段成功
 *  小于0: 表示发送持续探测段失败
 *  大于0: 表示由于本地拥塞而导致发送持续探测段失败。
 */
int tcp_write_wakeup(struct sock *sk, int mib)
{
    struct tcp_sock *tp = tcp_sk(sk);
    struct sk_buff *skb;

    if (sk->sk_state == TCP_CLOSE)
        return -1;

    skb = tcp_send_head(sk);
    if (skb && before(TCP_SKB_CB(skb)->seq, tcp_wnd_end(tp))) {
        int err;
        /*
         * 如果发送队列中有段需要发送,并且最先
         * 待发送的段至少有一部分在对端接收窗口
         * 内,那么可以直接利用该待发送的段来发
         * 送持续探测段。
         */
        unsigned int mss = tcp_current_mss(sk);
            /*
         * 获取当前的MSS以及待分段的段长。分段得到
         * 的新段必须在对方接收窗口内,待分段的段
         * 长初始化为SND.UNA-SND_WND-SKB.seq.
         */
        unsigned int seg_size = tcp_wnd_end(tp) - TCP_SKB_CB(skb)->seq;
/*
         * 如果该段的序号已经大于pushed_seq,则需要
         * 更新pushed_seq。
         */
        if (before(tp->pushed_seq, TCP_SKB_CB(skb)->end_seq))
            tp->pushed_seq = TCP_SKB_CB(skb)->end_seq;

        /* We are probing the opening of a window
         * but the window size is != 0
         * must have been a result SWS avoidance ( sender )
         */
          /*
         * 如果待分段段长大于剩余等待发送数据,或者段长度
         * 大于当前MSS,则对该段进行分段,分段段长取待分段
         * 段长与当前MSS两者中的最小值,以保证只发送出一个
         * 段到对方。
         */
        if (seg_size < TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq ||
            skb->len > mss) {
            seg_size = min(seg_size, mss);
            TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH;
            if (tcp_fragment(sk, skb, seg_size, mss, GFP_ATOMIC))
                return -1;
        } else if (!tcp_skb_pcount(skb))
            tcp_set_skb_tso_segs(skb, mss);
 /*
         * 将探测段发送出去,如果发送成功,
         * 则更新发送队首等标志。
         */
        TCP_SKB_CB(skb)->tcp_flags |= TCPHDR_PSH;
        err = tcp_transmit_skb(sk, skb, 1, GFP_ATOMIC);
        if (!err)
            tcp_event_new_data_sent(sk, skb);
        return err;
    } else {
 /*
         * 如果发送队列为空,则构造并发送一个需要已确认、
         * 长度为零的段给对端。如果处于紧急模式,则多发送
         * 一个序号为SND.UNA的段给对端。
         */
        if (between(tp->snd_up, tp->snd_una + 1, tp->snd_una + 0xFFFF))
            tcp_xmit_probe_skb(sk, 1, mib);
        return tcp_xmit_probe_skb(sk, 0, mib);
    }
}

 

 

tcp_keepalive_timer函数为保活定时器和FIN_WAIT_2定时器共用。。。。内核的实现。。复用代码

 

  当TCP主动关闭一端调用了close()来执行连接的完全关闭时会执行以下流程,本端发送FIN给对端,对端回复ACK,本端进入FIN_WAIT_2状态,此时只有对端发送了FIN,本端才会进入TIME_WAIT状态,为了防止对端不发送关闭连接的FIN包给本端,将会在进入FIN_WAIT_2状态时,设置一个FIN_WAIT_2定时器,如果该连接超过一定时限,则进入CLOSE状态;

上述是针对close调用完全关闭连接的情况,shutdown执行半关闭会不会启动FIN_WAIT_2定时器;-----》应该不会----

 

/*启动FIN_WAIT_2定时器两个相关逻辑差不多,1、进程调用close系统调用而socekt正处于TCP_FIN_WAIT2状态时 2、孤儿socket进入FIN_WAIT2状态时:从fin1-->fin2
在tcp_close函数中,如果判断状态为FIN_WAIT2,则需要进一步判断linger2配置;
如下所示,在linger2<0的情况下,关闭连接到CLOSE状态,并且发送rst;
在linger2 >= 0的情况下,需判断该值与TIME_WAIT等待时间TCP_TIMEWAIT_LEN值的关系,
如果linger2 > TCP_TIMEWAIT_LEN,则启动FIN_WAIT_2定时器,其超时时间为二者的差值;
如果linger2<0,则直接进入到TIME_WAIT状态,该TIME_WAIT的子状态是FIN_WAIT2,
实际上就是由TIME_WAIT控制块进行了接管,统一交给TIME_WAIT控制块来处理
     */
 /* 处于fin_wait2且socket即将关闭,用作FIN_WAIT_2定时器 */
    if (sk->sk_state == TCP_FIN_WAIT2) {
        struct tcp_sock *tp = tcp_sk(sk);
        if (tp->linger2 < 0) { /* linger2小于0,无需等待 */
            tcp_set_state(sk, TCP_CLOSE);
            tcp_send_active_reset(sk, GFP_ATOMIC);/* 发送rst */
            __NET_INC_STATS(sock_net(sk),
                    LINUX_MIB_TCPABORTONLINGER);
        } else {
            const int tmo = tcp_fin_time(sk); /* 获取FIN_WAIT_2超时时间 */

            if (tmo > TCP_TIMEWAIT_LEN) { /* FIN_WAIT_2超时时间> TIME_WAIT时间,加FIN_WAIT_2定时器 */
                inet_csk_reset_keepalive_timer(sk,
                        tmo - TCP_TIMEWAIT_LEN);
            } else {/* 小于TIME_WAIT时间,则进入TIME_WAIT */
                tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
                goto out;
            }
        }

 

/* 连接释放期间,用作FIN_WAIT2定时器 */ 
    /*
     * 处理FIN_WAIT_2状态定时器时,TCP状态必须为
     * FIN_WAIT_2且套接字状态为DEAD。
     */ //tcp_rcv_state_process中收到第一个FIN ack后会进入TCP_FIN_WAIT2状态
    if (sk->sk_state == TCP_FIN_WAIT2 && sock_flag(sk, SOCK_DEAD)) {
        //TCP关闭过程中的定时器处理过程,从tcp_rcv_state_process跳转过来
         /*
         * 停留在FIN_WAIT_2状态的时间大于或等于0的情况下,
         * 如果FIN_WAIT_2定时器剩余时间大于0,则调用
         * tcp_time_wait()继续处理;否则给对端发送RST后
         * 关闭套接字。
         */
         /*
TIME_WAIT_2定时器超时触发,如果linger2<0,或者等待时间<=TIMEWAIT_LEN,
直接发送reset关闭连接;如果linger2>=0,且等待时间>TIMEWAIT_LEN,
则进入TIME_WAIT接管;
        */
        if (tp->linger2 >= 0) {/* 停留在FIN_WAIT_2的停留时间>=0 */
            const int tmo = tcp_fin_time(sk) - TCP_TIMEWAIT_LEN;/* 获取时间差值 */

            if (tmo > 0) { /* 差值>0,等待时间>TIME_WAIT时间,则进入TIME_WAIT状态 */
                tcp_time_wait(sk, TCP_FIN_WAIT2, tmo);
                goto out;
            }
        }
        tcp_send_active_reset(sk, GFP_ATOMIC);
        goto death;
    }

 

那么在设置RCV_SHUTDOWN的tcp socket 中 recvmsg 会怎样呢??

查看tcp_recvmsg代码可知:会直接返回0;读不到数据没有发出reset;

if (sk->sk_shutdown & RCV_SHUTDOWN)
                break;//一个字节都没拷贝到,但如果shutdown关闭了socket,一样直接返回

Q:shutdown执行半关闭会不会启动FIN_WAIT_2定时器;-----应该不会----

 

int inet_shutdown(struct socket *sock, int how)
{
    
        /* Hack to wake up other listeners, who can poll for
           POLLHUP, even on eg. unconnected UDP sockets -- RR */
    default:
        sk->sk_shutdown |= how;
        if (sk->sk_prot->shutdown)
            sk->sk_prot->shutdown(sk, how);
        break;
    /* Wake up anyone sleeping in poll. */
    sk->sk_state_change(sk);//sock_def_wakeup
    release_sock(sk);
    return err;
}//也就是 设置sk_shut_down掩码调用tcp_port的tcp_shutdown

 

/*
 *    Shutdown the sending side of a connection. Much like close except
 *    that we don‘t receive shut down or sock_set_flag(sk, SOCK_DEAD).
 */
/*
tcp_shutdown函数完成设置关闭之后的状态,并且发送fin
;注意只有接收端关闭时,不发送fin,只是在recvmsg系统调用中判断状态
,不接收数据;
*/
void tcp_shutdown(struct sock *sk, int how)
{
    /*    We need to grab some memory, and put together a FIN,
     *    and then put it into the queue to be sent.
     *        Tim MacKenzie(tym@dibbler.cs.monash.edu.au) 4 Dec ‘92.
     */
    if (!(how & SEND_SHUTDOWN))
        return;
 /* 以下这几个状态发fin */
    /* If we‘ve already sent a FIN, or it‘s a closed state, skip this. */
    if ((1 << sk->sk_state) &
        (TCPF_ESTABLISHED | TCPF_SYN_SENT |
         TCPF_SYN_RECV | TCPF_CLOSE_WAIT)) {
        /* Clear out any half completed packets.  FIN if needed. */
        if (tcp_close_state(sk))
            tcp_send_fin(sk);
    }
}

 

 

tcp socket shutdown   SEND_SHUTDOWN;----可以看到只是 发送了一个fin;仅仅只是设置 掩码 sk->sk_shutdown |= how;

但是对于调用close --》tcp_close---》sk->sk_shutdown = SHUTDOWN_MASK;一个全部关闭,

 同时设置为孤儿socket  状态设置为sock_dead  对应了定时器中需要检测sock_dead状态

/* Detach socket from process context.
 * Announce socket dead, detach it from wait queue and inode.
 * Note that parent inode held reference count on this struct sock,
 * we do not release it in this function, because protocol
 * probably wants some additional cleanups or even continuing
 * to work with this socket (TCP).
 */
static inline void sock_orphan(struct sock *sk)
{
    write_lock_bh(&sk->sk_callback_lock);
    sock_set_flag(sk, SOCK_DEAD);
    sk_set_socket(sk, NULL);
    sk->sk_wq  = NULL;
    write_unlock_bh(&sk->sk_callback_lock);
}

 

 

 

 

 

 

tcp 保活定时器分析 & Fin_WAIT_2 定时器

原文:https://www.cnblogs.com/codestack/p/12818169.html

(0)
(0)
   
举报
评论 一句话评论(0
关于我们 - 联系我们 - 留言反馈 - 联系我们:wmxa8@hotmail.com
© 2014 bubuko.com 版权所有
打开技术之扣,分享程序人生!