zoukankan      html  css  js  c++  java
  • tcp 保活定时器分析 & Fin_WAIT_2 定时器

    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 ???
    ");
            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的段给对端。
     * Current solution: to send TWO zero-length segments in urgent mode:
     * one is with SEG.SEQ=SND.UNA to deliver urgent pointer, another is
     * out-of-date with SND.UNA-1 to probe window.
    */ 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状态;涉及到TCP_LINGER2 选项

    上述是针对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);
    }

     

     SO_LINGER,该选项是socket层面的选项,通过struct linger结构来设置信息,如果启用该选项,那么使用close()和shutdown()(注意:关闭socket,将会等待发送队列中的数据发送完成或者等待超时;

    如果不启用该选项,那么调用会立即返回,关闭任务在后台完成;注意:如果是调用exit()函数关闭socket,那么无论是否启用SO_LINGER选项,socket总会在后台执行linger等待

    TCP_LINGER2,该选项是TCP层面的,用于设定孤儿套接字在FIN_WAIT2状态的生存时间,该选项可以用来替代系统级别的tcp_fin_timeout配置;

    在用于移植的代码中不应该使用该选项;另外,需要注意,不要混淆该选项与socket的SO_LINGER选项;

    记住: tcp的fin_time_wait2状态---》可能导致 发出rst 或者进入timewait状态 ---》 TIME_WAIT定时器超时触发,定时器超时,将tw控制块从ehash和bhash中删除,在收到数据段会发送reset;

  • 相关阅读:
    为什么折价溢价的摊销属于借款费用?
    长期待摊费用
    合同负债
    未决诉讼
    AMD Ryzen 3600超频4.3
    强制删除软件
    wallpaper壁纸download下载失败
    ryzen3600超频4.3
    维度规约(降维)算法在WEKA代写中应用
    python代写虎扑社区论坛数据爬虫分析报告
  • 原文地址:https://www.cnblogs.com/codestack/p/12818169.html
Copyright © 2011-2022 走看看