Rxsi Blog GameServer Developer

握手&挥手源码解析:第一次挥手

2022-09-13
Rxsi
TCP

第一次挥手

在 TCP 中有两个函数可以关闭连接,分别是closeshutdown

tcp_close

close 函数对应的底层实现函数是 tcp_close ,具体实现如下:

void tcp_close(struct sock *sk, long timeout)
{
    lock_sock(sk); // 对sock进行锁定
    __tcp_close(sk, timeout); // 实际作用函数
    release_sock(sk);
    sock_put(sk);
}

void __tcp_close(struct sock *sk, long timeout)
{
    struct sk_buff *skb;
    int data_was_unread = 0;
    int state;

    sk->sk_shutdown = SHUTDOWN_MASK; // 修改shutdown标志为3,代表同时关闭读写端

    if (sk->sk_state == TCP_LISTEN) { // 如果处于listen状态,在此状态还没有接收缓冲,因此处理较为简单
        tcp_set_state(sk, TCP_CLOSE); // 修改状态为关闭

        /* Special case. */
        inet_csk_listen_stop(sk);

        goto adjudge_to_death;
    }

    /*  We need to flush the recv. buffs.  We do this only on the
     *  descriptor close, not protocol-sourced closes, because the
     *  reader process may not have drained the data yet!
     */
    while ((skb = __skb_dequeue(&sk->sk_receive_queue)) != NULL) { // 当接收队列不为空,注意这些未读取的包是直接丢弃!!!
        u32 len = TCP_SKB_CB(skb)->end_seq - TCP_SKB_CB(skb)->seq; // 这个包体包含的序列号范围长度

        if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN) // 如果这个包是FIN包,那么长度-1,难道-1之后就是0?TODO
            len--;
        data_was_unread += len; // 未读取的序列号长度
        __kfree_skb(skb); // 释放掉这个包
    }

    /* If socket has been already reset (e.g. in tcp_reset()) - kill it. */
    if (sk->sk_state == TCP_CLOSE) // 已经关闭
        goto adjudge_to_death;

    if (unlikely(tcp_sk(sk)->repair)) { // 不知是什么标志,源码中没有注释TODO
        sk->sk_prot->disconnect(sk, 0);
    } else if (data_was_unread) { // 如果有未处理的数据包被清除了,可见这里是一种暴力的操作,不遵循四次挥手
        /* Unread data was tossed, zap the connection. */
        NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONCLOSE);
        tcp_set_state(sk, TCP_CLOSE); // 设置为关闭状态
        tcp_send_active_reset(sk, sk->sk_allocation); // 给对端发送一个RST包
    } else if (sock_flag(sk, SOCK_LINGER) && !sk->sk_lingertime) { // linger状态的处理
        /* Check zero linger _after_ checking for unread data. */
        sk->sk_prot->disconnect(sk, 0);
        NET_INC_STATS(sock_net(sk), LINUX_MIB_TCPABORTONDATA);
    } else if (tcp_close_state(sk)) { // 根据tcp当前状态修改为下一个状态
        // TCP_ESTABLISHED -> TCP_FIN_WAIT1
        // TCP_CLOSE_WAIT -> TCP_LAST_ACK
        tcp_send_fin(sk); // 在修改完状态之后下发FIN,这个FIN包可能是第一次挥手也可能是第三次挥手,看具体语境
    }

    sk_stream_wait_close(sk, timeout); // 阻塞等待sock状态的变化为!(FIN_WAIT_1、CLOSING、LAST_ACK)里面使用的是一个 do{}while() 的循环

adjudge_to_death:
    // 设置套接字状态为DEAD状态,使之成为孤儿套接字,同时更新系统的孤儿套接字数
    state = sk->sk_state;
    sock_hold(sk);
    sock_orphan(sk); // 这里会把sock设置为DEAD状态

    local_bh_disable();
    bh_lock_sock(sk);
    /* remove backlog if any, without releasing ownership. */
    __release_sock(sk);

    this_cpu_inc(tcp_orphan_count);

    /* Have we already been destroyed by a softirq or backlog? */
    if (state != TCP_CLOSE && sk->sk_state == TCP_CLOSE)
        goto out;
    // 处理FIN_WAIT2状态
    if (sk->sk_state == TCP_FIN_WAIT2) {
        struct tcp_sock *tp = tcp_sk(sk);
        if (tp->linger2 < 0) { // TCP_LINGER2设置为小于0,说明不需要等待FIN_WAIT_2状态,因此立即转换为CLOSED状态
            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_WAIT2状态的时长,如果设置了TCP_LINGER2,那么读取该值,否则读取tcp_fin_timeout(默认60s)参数,且最小值是 rto<<2 - rot>>1
            if (tmo > TCP_TIMEWAIT_LEN) { // 如果等待时长大于1分钟,TCP_TIMEWAIT_LEN==60,因为TIME_WAIT状态要刚好是1分钟,因此这里要利用保活定时器消耗掉这个差值
                inet_csk_reset_keepalive_timer(sk,
                        tmo - TCP_TIMEWAIT_LEN); // 重置keepalive定时器,这个定时器是保活机制和FIN_WAIT_2共用的
            } else {
                tcp_time_wait(sk, TCP_FIN_WAIT2, tmo); // 进入TIME_WAIT状态,这里就会释放掉重量的sock结构,转而使用inet_timewait_sock结构,因此TIME_WAIT状态占用的内存会少很多
                goto out;
            }
        }
    }
    if (sk->sk_state != TCP_CLOSE) {
        if (tcp_check_oom(sk, 0)) { // 看是否是OOM了,如果是说明网络还是没问题的,那么发出RST报文
            tcp_set_state(sk, TCP_CLOSE); // 关闭
            tcp_send_active_reset(sk, GFP_ATOMIC); // RST报文
            __NET_INC_STATS(sock_net(sk),
                    LINUX_MIB_TCPABORTONMEMORY);
        } else if (!check_net(sock_net(sk))) { // 网络有问题,直接关闭吧
            /* Not possible to send reset; just close */
            tcp_set_state(sk, TCP_CLOSE); // 关闭
        }
    }

    if (sk->sk_state == TCP_CLOSE) { // 关闭成功,一些收尾工作
        struct request_sock *req;

        req = rcu_dereference_protected(tcp_sk(sk)->fastopen_rsk,
                        lockdep_sock_is_held(sk)); // 如果有request_sock那么要回收
        /* We could get here with a non-NULL req if the socket is
         * aborted (e.g., closed with unread data) before 3WHS
         * finishes.
         */
        if (req)
            reqsk_fastopen_remove(sk, req, false); // 回收req
        inet_csk_destroy_sock(sk); // 销毁sock结构体
    }
    /* Otherwise, socket is reprieved until protocol close. */

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

tcp_shutdown

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)) // 如果不是关闭SEND_SHUTDOWN或者SHUTDOWN_MASK,即没有关闭发送端;这里可以看到RCV_SHUTDOWN不会改变tcp状态。经过测试,当主动调用shutdown(fd, RCV_SHUTDOWN)后,如果此时没有数据了,那么会立即返回0,如果是放在IO多路复用里面,那么会一直有可读事件,而每次读取获得都是长度都是0;而如果后续又收到了数据,还是可以成功读取。
        return;

    /* 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); // 发送FIN报文
    }
}

tcp_send_fin

不管是在 tcp_close 还是 tcp_shutdown 中,要发送FIN报文,都是通过 tcp_send_fin 函数进行的,具体实现如下:

void tcp_send_fin(struct sock *sk)
{
    struct sk_buff *skb, *tskb, *tail = tcp_write_queue_tail(sk); // write_queue存储的是未发送出去的数据包
    struct tcp_sock *tp = tcp_sk(sk);

    /* Optimization, tack on the FIN if we have one skb in write queue and
     * this skb was not yet sent, or we are under memory pressure.
     * Note: in the latter case, FIN packet will be sent after a timeout,
     * as TCP stack thinks it has already been transmitted.
     */
    tskb = tail;
    if (!tskb && tcp_under_memory_pressure(sk)) // 如果没有未发送的数据包,但是内存紧张
        tskb = skb_rb_last(&sk->tcp_rtx_queue); // rtx_queue存储的是已经发送但是还没有收到回复的数据包,以红黑树根据序列号进行组织

    if (tskb) { // 有可以利用的数据包
        TCP_SKB_CB(tskb)->tcp_flags |= TCPHDR_FIN; // 给这个包打上FIN标志
        TCP_SKB_CB(tskb)->end_seq++;
        tp->write_seq++;
        if (!tail) { // 证明这里是因为内存紧张把一个已经发送过的数据包打上FIN标签之后再利用,需要修改发送序号
            /* This means tskb was already sent.
             * Pretend we included the FIN on previous transmit.
             * We need to set tp->snd_nxt to the value it would have
             * if FIN had been sent. This is because retransmit path
             * does not change tp->snd_nxt.
             */
            WRITE_ONCE(tp->snd_nxt, tp->snd_nxt + 1);
            return;
        }
    } else {
        skb = alloc_skb_fclone(MAX_TCP_HEADER, sk->sk_allocation); // 没有可以再利用的数据包,那么需要重新构建一个
        if (unlikely(!skb))
            return;

        INIT_LIST_HEAD(&skb->tcp_tsorted_anchor);
        skb_reserve(skb, MAX_TCP_HEADER);
        sk_forced_mem_schedule(sk, skb->truesize);
        /* FIN eats a sequence byte, write_seq advanced by tcp_queue_skb(). */
        tcp_init_nondata_skb(skb, tp->write_seq,
                     TCPHDR_ACK | TCPHDR_FIN); // 打上FIN标志
        tcp_queue_skb(sk, skb); // 放入到write_queue尾部
    }
    __tcp_push_pending_frames(sk, tcp_current_mss(sk), TCP_NAGLE_OFF); // 立即发送
}

第一次挥手丢失,会发生什么?

当客户端发出 FIN 报文之后,会进入 FIN_WAIT1 状态,由于没有接收到服务端的 ACK 报文,因此会进行重发。数据包的重发依然是由 tcp_retransmit_timer 负责,其中在 tcp_write_timeout 函数中就有对重发次数的判断:

static int tcp_write_timeout(struct sock *sk)
{
    // 忽略无关代码
        if (sock_flag(sk, SOCK_DEAD)) { // 当前sock已经处于DEAD状态,说明已经发出了FIN报文
            const bool alive = icsk->icsk_rto < TCP_RTO_MAX;

            retry_until = tcp_orphan_retries(sk, alive); // 获取处于DEAD状态时的最大重发次数,读取配置tcp_orphan_retries(默认是0,但是里面会处理为8)
            do_reset = alive ||
                !retransmits_timed_out(sk, retry_until, 0); // 如果连接还活跃但是已经耗尽了最大重发次数,那么在关闭时就可尝试发送一个RST报文,然后重置为CLOSE状态,否则直接关闭
            if (tcp_out_of_resources(sk, do_reset))
                return 1;
        }
    }
// ...
}

当 FIN 报文丢失之后,会经过tcp_orphan_retries(默认值为0)次数的重试之后才会直接关闭 TCP,虽然默认值为 0,但是实际在代码中会被处理为 8 次。

rxsi@VM-20-9-debian:~$ cat /proc/sys/net/ipv4/tcp_orphan_retries 
0 // 虽然是0,但是实际代码中值是8

这里要注意, FIN_WAIT_1 状态是没有超时时间的,所以当迟迟没有收到 ACK 回复时,只会依赖于 FIN 包达到最大重传次数而关闭。这与 FIN_WAIT_2 状态是不同的。


Similar Posts

Comments