• TCP连接的终止被动关闭


      这篇文章是对 《TCP连接的终止----主动关闭》的补充,重点关注的是被动关闭连接一端的状态迁移及内核中的 处理,被动连接的状态迁移可以参见 《TCP连接的终止----主动关闭》中的状态迁移图,这里就不再画了
    被动连接的关闭是从接收到FIN开始的,如果TCP是处于ESTABLISHED状态(我们的讨论假设连接处于此状态),这个FIN包会在tcp_rcv_established()中处理。在tcp_rcv_state_process()中的处理分为快速路径和慢速路径。如果TCP首部中第4个32位字除去保留的bit位和预测标志一致,skb包的序列号和sock结构下一个要接收到序号相等,并且skb包中的确认序列号是有效的,此时skb包会在快速路径中处理。判断时sock实例的预测标志的位图分布通常如下图所示:
    其中S对应于TCP首部(struct tcphdr)中的doff成员(tcp首部的长度,以4字节为单位),?通常为0,为1的bit位对应的是ACK标志,snd_wnd则是本端发送窗口的大小。
    从上面的预测标志的分布来看,如果设置了FIN标志的话,则检查预测标志时失败,所以会在慢速路径中处理FIN包。
    在慢速路径处理中,会首先检查skb包的校验和及包是否是有效的,检查通过后会调用tcp_ack()处理ack的情况,接下来的处理中真正和FIN相关的操作是在调用的tcp_data_queue()中处理的,这个处理是通过调用tcp_fin()函数来处理的,从最初的TCP层接收函数tcp_v4_rcv()到tcp_fin()的处理的代码流程图如下所示:
    tcp_fin()中首先调用inet_csk_schedule_ack()设置相关成员,表明需要发送ACK;因为接收了对端发送的FIN包,表明对端已经不会再发送数据包(可以发送ACK),因此关闭接收通道;还要修改sock结构的标志,表示连接将要结束;然后根据sock实例的状态会跳转到不同的分支进行处理,如果是ESTABLISHED状态下,内核会调用tcp_set_state()修改sock实例的状态,并且设置延迟发送ACK的标志,如下所示:
    static void tcp_fin(struct sk_buff *skb, struct sock *sk, struct tcphdr *th)
    {
        struct tcp_sock *tp = tcp_sk(sk);
    
        inet_csk_schedule_ack(sk);
    
        sk->sk_shutdown |= RCV_SHUTDOWN;
        sock_set_flag(sk, SOCK_DONE);
    
        switch (sk->sk_state) {
        case TCP_SYN_RECV:
        case TCP_ESTABLISHED:
            /* Move to CLOSE_WAIT */
            tcp_set_state(sk, TCP_CLOSE_WAIT);
            inet_csk(sk)->icsk_ack.pingpong = 1;
            break;
    
        ......
        }
    }
    tcp_fin()后面的清理乱序队列、状态更改时可能要唤醒相关进程这些操作不是我们关心的,就不作过多说明了。
    在tcp_fin()中虽然设置了发送ACK的相关标志,但是要有一个引发ACK发送的操作,或者是给内核发送ACK的一个提示。这个操作是在上层函数tcp_rcv_established()函数中进行的,通过间接调用__tcp_ack_snd_check()中完成。__tcp_ack_snd_check()中会判断当前发送ACK是要立即发送还是延迟发送,如果立即发送则调用tcp_send_ack()来发送ACK,否则调用tcp_send_delayed_ack()延迟发送,代码如下所示:
    static void __tcp_ack_snd_check(struct sock *sk, int ofo_possible)
    {
        struct tcp_sock *tp = tcp_sk(sk);
    
            /* More than one full frame received... */
        if (((tp->rcv_nxt - tp->rcv_wup) > inet_csk(sk)->icsk_ack.rcv_mss
             /* ... and right edge of window advances far enough.
              * (tcp_recvmsg() will send ACK otherwise). Or...
              */
             && __tcp_select_window(sk) >= tp->rcv_wnd) ||
            /* We ACK each frame or... */
            tcp_in_quickack_mode(sk) ||
            /* We have out of order data. */
            (ofo_possible && skb_peek(&tp->out_of_order_queue))) {
            /* Then ack it now */
            tcp_send_ack(sk);
        } else {
            /* Else, send delayed ack. */
            tcp_send_delayed_ack(sk);
        }
    }
    判断的条件是这样的,只要满足以下条件就会立即发送ACK:
    1、接收窗口中有多个全尺寸段还未确认
    2、当前处于快速确认模式下
    3、在启用判断乱序队列的情况下,乱序队列中存在段
    这三个条件中,我们可以确定的是第2个条件,我们首先来看判断是否处于快速确认模式的函数tcp_in_quickack_mod()函数的实现,如下所示:
    static inline int tcp_in_quickack_mode(const struct sock *sk)
    {
        const struct inet_connection_sock *icsk = inet_csk(sk);
        return icsk->icsk_ack.quick && !icsk->icsk_ack.pingpong;
    }
    在tcp_fin()中如果是ESTABLISHED状态下接收到FIN,会设置pingpong的值为1,所以可以肯定此时不处于快速确认模式下,至于是否要立即发送ACK取决于另外两个判断条件了。
    我们接下来的讨论是在从对接收到的FIN确认后开始的。这时TCP连接的关闭已经进行一半了,接下来就是等待本端的上层应用调用close()来执行本端的关闭连接操作,这个操作我们在 《TCP连接的终止----主动关闭》中讲到过,是由tcp_close()来完成的。所以我们还是来看tcp_close(),只是这次sock实例的状态不一样,这时的状态应该为CLOSE_WAIT。
    在tcp_close()中我们这次只关注一些和状态相关的一些处理,其他的队列清理、内存回收等就不再介绍了,所以我们只关注下面这部分代码:
    void tcp_close(struct sock *sk, long timeout)
    {
        struct sk_buff *skb;
        int data_was_unread = 0;
        int state;
    
        ......
    
        if (data_was_unread) {
            ......
    
        } else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) {
            ......
    
        } else if (tcp_close_state(sk)) {
            tcp_send_fin(sk);
        }
    
        ......
    }
    在tcp_close_state()中sock实例的状态会由CLOSE_WAIT迁移到LAST_ACK状态,返回值为TCP_ACTION_FIN,表示要发送FIN。因此,在第三个if判断中条件为true,所以会调用tcp_send_fin()给对端发送FIN。
    当本端接收到TCP连接关闭的最后一个ACK时,由tcp_rcv_state_process()函数来处理,相关的代码如下所示:
    int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb,
                  struct tcphdr *th, unsigned len)
    {
        struct tcp_sock *tp = tcp_sk(sk);
        struct inet_connection_sock *icsk = inet_csk(sk);
        int queued = 0;
        int res;
    
        ......
    
        /* step 5: check the ACK field */
        if (th->ack) {
            int acceptable = tcp_ack(sk, skb, FLAG_SLOWPATH) > 0;
    
            switch (sk->sk_state) {
            ......
    
            case TCP_LAST_ACK:
                if (tp->snd_una == tp->write_seq) {
                    tcp_update_metrics(sk);
                    tcp_done(sk);
                    goto discard;
                }
                break;
            }
        } else
            goto discard;
    
        ......
    
        switch (sk->sk_state) {
        case TCP_CLOSE_WAIT:
        case TCP_CLOSING:
        case TCP_LAST_ACK:
            if (!before(TCP_SKB_CB(skb)->seq, tp->rcv_nxt))
                break;
    
        case TCP_FIN_WAIT1:
        case TCP_FIN_WAIT2:
            /* RFC 793 says to queue data in these states,
             * RFC 1122 says we MUST send a reset.
             * BSD 4.4 also does reset.
             */
            if (sk->sk_shutdown & RCV_SHUTDOWN) {
                if (TCP_SKB_CB(skb)->end_seq != TCP_SKB_CB(skb)->seq &&
                    after(TCP_SKB_CB(skb)->end_seq - th->fin, tp->rcv_nxt)) {
                    NET_INC_STATS_BH(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
                    tcp_reset(sk);
                    return 1;
                }
            }
            /* Fall through */
        case TCP_ESTABLISHED:
            tcp_data_queue(sk, skb);
            queued = 1;
            break;
        }
    
        /* tcp_data could move socket to TIME-WAIT */
        if (sk->sk_state != TCP_CLOSE) {
            tcp_data_snd_check(sk);
            tcp_ack_snd_check(sk);
        }
    
        if (!queued) {
    discard:
            __kfree_skb(skb);
        }
        return 0;
    }
    
    如果刚好是期望的ACK包,则会在第19-23行代码中处理,调用tcp_done()将套接字状态设置为TCP_CLOSE,并且调用inet_csk_destroy_sock()释放sock实例占用的资源,并且调用sock_put()释放传输控制块(真正的调用sk_free()一般情况下不会是这里,但是这里较少引用计数后,上层再调用sock_put()时就会触发sk_free()操作)。我们知道在主动关闭一端正常情况下会通过定时器来释放描述TIME_WAIT状态的sock结构或者放在twcal_row队列中等待释放,这些释放方式比较明显。还有一个种就是这里看到的通过inet_csk_destroy_sock()来间接完成释放。
    有时也可能接收到其他包,如果是包含数据的包,在44-51的处理中会发送RST给对端,如果只是单纯的ACK包,但是确认的序列号不对,则会在tcp_data_queue()中释放掉。
    至此,TCP连接被动关闭一方的处理完成了。
  • 相关阅读:
    AD预测论文研读系列2
    hdu 5795
    sg函数的应用
    二分查找
    快速幂
    筛选法素数打表
    多校hdu-5775 Bubble sort(线段树)
    多校hdu5754(博弈)
    多校hdu5738 寻找
    多校hdu5726 线段树+预处理
  • 原文地址:https://www.cnblogs.com/javawebsoa/p/2978285.html
Copyright © 2020-2023  润新知