上篇文章 一个有关tcp的很是有意思的问题 中咱们讲到,在tcp创建链接后,若是一端关闭了链接,另外一端的第一次write仍是能够写成功的,文章中也分析了形成这种现象的具体缘由。微信
那若是在此种状况下,read又会有什么样的结果呢?socket
其实具体结果已经在read的man文档中有详细介绍,不过咱们仍是从源码角度来证明下:tcp
// 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)给用户。spa
综上可见,read方法用返回值表示该socket的当前状况,若是返回值大于0,表示read成功,当前socket正常(即便此时socket已经处于CLOSE_WAIT状态),若是返回值等于0,表示该socket的对应的socket已经关闭,而且咱们已经收到了fin包,进入了CLOSE_WAIT状态,通常在这种状况下,咱们都会在应用层调用close方法,关闭咱们本身的socket,进而完整的关闭整个tcp链接。线程
对应看下read的man文档,咱们会发现,源码和文档中的描述是一致的。3d
至此,read相关的返回值咱们就分析完毕了。code
下面咱们再来分析下,在一样的情景下,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
结合上篇的文章咱们能够看到,咱们经过一个小问题,引伸出了这么多问题,在咱们一一搞清楚这些问题以后,咱们才算是对最开始的问题有了一个完美的解释。
因此说,作技术的没有小问题,每个小问题背后都须要咱们有不少的知识储备才能完全搞清楚。
这同时也告诉咱们,工做中遇到的任何问题都不能忽视,它极可能是你进步的重要因素。
完。
更多原创文章,请关注我微信公众号: