上篇文章 一个有关tcp的非常有意思的问题 中我们讲到,在tcp建立连接后,如果一端关闭了连接,另一端的第一次write还是可以写成功的,文章中也分析了造成这种现象的具体原因。

那如果在此种情况下,read又会有什么样的结果呢?

其实具体结果已经在read的man文档中有详细介绍,不过我们还是从源码角度来证实下:

// net/ipv4/tcp.c
int tcp_recvmsg(struct sock *sk, struct msghdr *msg, size_t len, int nonblock,
                int flags, int *addr_len)
{
        struct tcp_sock *tp = tcp_sk(sk);
        int copied = 0; // 总共拷贝给用户的字节数,用于返回
        ...
        u32 *seq;
        ...
        seq = &tp->copied_seq; // 下一个拷贝给用户的字节
        ...
        do {
                u32 offset;
                ...
                skb_queue_walk(&sk->sk_receive_queue, skb) {
                        ...
                        offset = *seq - TCP_SKB_CB(skb)->seq;
                        ...
                        if (offset < skb->len)
                                goto found_ok_skb;
                        if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
                                goto found_fin_ok;
                        ...
                }
                ...
found_ok_skb:
                /* Ok so how much can we use? */
                used = skb->len - offset; // 当前buf剩余可拷贝给用户的字节数
                ...
                *seq += used;
                copied += used;
                len -= used;
                ...
                continue;
found_fin_ok:
                ...
                break;
        } while (len > 0);
        ...
        return copied;
        ...
}
EXPORT_SYMBOL(tcp_recvmsg);

由上可见,当我们发起read时,不管此时我们的socket是否已经收到fin包,我们都会先把socket中的未读字节读出来,并返回拷贝的字节数给用户,表示此次read成功。

如果我们把socket中的数据都读完了,然后检测到了最后的fin包,此时直接跳出read循环,返回copied的值(此时是0)给用户。

综上可见,read方法用返回值表示该socket的当前情况,如果返回值大于0,表示read成功,当前socket正常(即使此时socket已经处于CLOSE_WAIT状态),如果返回值等于0,表示该socket的对应的socket已经关闭,并且我们已经收到了fin包,进入了CLOSE_WAIT状态,一般在这种情况下,我们都会在应用层调用close方法,关闭我们自己的socket,进而完整的关闭整个tcp连接。

对应看下read的man文档,我们会发现,源码和文档中的描述是一致的。

至此,read相关的返回值我们就分析完毕了。

下面我们再来分析下,在同样的情景下,epoll相关操作会有什么样的反应呢?

我们先来看下收到fin包后,我们socket的处理流程:

// net/ipv4/tcp_input.c
static void tcp_data_queue(struct sock *sk, struct sk_buff *skb)
{
        struct tcp_sock *tp = tcp_sk(sk);
        bool fragstolen;
        int eaten;
        ...
        if (TCP_SKB_CB(skb)->seq == tp->rcv_nxt) {
                ...
                // 将当前接受到的tcp包加入到接受队列中
                eaten = tcp_queue_rcv(sk, skb, &fragstolen);
                ...
                if (TCP_SKB_CB(skb)->tcp_flags & TCPHDR_FIN)
                        tcp_fin(sk);
                ...
                return;
        }
        ...
}

该方法在收到fin包后调用了tcp_fin方法:

// net/ipv4/tcp_input.c
void tcp_fin(struct sock *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);
                ...
                break;
        ...
        }
        ...
        if (!sock_flag(sk, SOCK_DEAD)) {
                sk->sk_state_change(sk);
                ...
        }
}

由上可见,该方法在收到fin包后,设置该socket的shutdown情况为RCV_SHUTDOWN,并且设置其状态为TCP_CLOSE_WAIT。

之后调用了sk->sk_state_change方法,标识该socket有epoll事件发生,此时因调用epoll_wait而阻塞的线程也会从阻塞状态中退出,epoll_wait线程进而会去检测该socket准备好了哪些epoll事件,对应的检测方法为下面这个方法:

// net/ipv4/tcp.c
__poll_t tcp_poll(struct file *file, struct socket *sock, poll_table *wait)
{
        __poll_t mask;
        struct sock *sk = sock->sk;
        ...
        if (sk->sk_shutdown & RCV_SHUTDOWN)
                mask |= EPOLLIN | EPOLLRDNORM | EPOLLRDHUP;
        ...
        return mask;
}
EXPORT_SYMBOL(tcp_poll);

由上可见,当我们socket的shutdown处于RCV_SHUTDOWN状态时,epoll_wait返回给用户的事件为 EPOLLIN | EPOLLRDNORM | EPOLLRDHUP。

也就是说,当我们的socket收到fin包之后,监听该socket的对应的epoll_wait方法会从阻塞状态中退出,并调用上面的tcp_poll方法,该方法检测到这个socket此时已经准备好的epoll事件为 EPOLLIN | EPOLLRDNORM | EPOLLRDHUP,最后epoll_wait将这些事件返回给用户。

此时,用户的一般操作为继续对这个socket进行read,通过read返回0的形式,来表示对方socket已经关闭,我们的socket也可以关闭了。

至此,epoll相关的行为也以已经分析完毕了。

整个过程还是比较简单的。

有关epoll相关的源码分析系列文章,可以看下我之前写的这些:

Linux epoll 源码分析 1
Linux epoll 源码分析 2
Linux epoll 源码分析 3

结合上篇的文章我们可以看到,我们通过一个小问题,引申出了这么多问题,在我们一一搞清楚这些问题之后,我们才算是对最开始的问题有了一个完美的解释。

所以说,做技术的没有小问题,每一个小问题背后都需要我们有很多的知识储备才能彻底搞清楚。

这同时也告诉我们,工作中遇到的任何问题都不能忽视,它很可能是你进步的重要因素。

完。

更多原创文章,请关注我微信公众号:

底层技术研究


wangyuntao
30 声望3 粉丝