TCP连接的停止-被动关闭

TCP连接的终止----被动关闭
  这篇文章是对《TCP连接的终止----主动关闭》的补充,重点关注的是被动关闭连接一端的状态迁移及内核中的​处理,被动连接的状态迁移可以参见《TCP连接的终止----主动关闭》中的状态迁移图,这里就不再画了
被动连接的关闭是从接收到FIN开始的,如果TCP是处于ESTABLISHED状态(我们的讨论假设连接处于此状态),这个FIN包会在tcp_rcv_established()中处理。在tcp_rcv_state_process()中的处理分为快速路径和慢速路径。如果TCP首部中第4个32位字除去保留的bit位和预测标志一致,skb包的序列号和sock结构下一个要接收到序号相等,并且skb包中的确认序列号是有效的,此时skb包会在快速路径中处理。判断时sock实例的预测标志的位图分布通常如下图所示:
TCP连接的停止-被动关闭
其中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连接的停止-被动关闭
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连接被动关闭一方的处理完成了。