• 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;

  • 相关阅读:
    Java实现 LeetCode 69 x的平方根
    Java实现 LeetCode 68 文本左右对齐
    Java实现 LeetCode 68 文本左右对齐
    Java实现 LeetCode 68 文本左右对齐
    Java实现 LeetCode 67 二进制求和
    Java实现 LeetCode 67 二进制求和
    Java实现 LeetCode 67 二进制求和
    Java实现 LeetCode 66 加一
    Java实现 LeetCode 66 加一
    CxSkinButton按钮皮肤类
  • 原文地址:https://www.cnblogs.com/codestack/p/12818169.html
Copyright © 2020-2023  润新知