首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >对上一篇文章中tcp问题的进一步思考

对上一篇文章中tcp问题的进一步思考

作者头像
KINGYT
发布2019-07-22 15:48:50
5260
发布2019-07-22 15:48:50
举报

上篇文章 一个有关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

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

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

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

完。

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2019-07-19,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 Linux内核及JVM底层相关技术研究 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档