上篇文章 一个有关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
结合上篇的文章我们可以看到,我们通过一个小问题,引申出了这么多问题,在我们一一搞清楚这些问题之后,我们才算是对最开始的问题有了一个完美的解释。
所以说,做技术的没有小问题,每一个小问题背后都需要我们有很多的知识储备才能彻底搞清楚。
这同时也告诉我们,工作中遇到的任何问题都不能忽视,它很可能是你进步的重要因素。
完。
更多原创文章,请关注我微信公众号: