专栏首页Linux内核及编程语言底层相关技术研究read方法返回0后还会有epollin事件吗

read方法返回0后还会有epollin事件吗

完整的问题是:

当read方法返回0,即我们收到了对方发给我们的fin包,使我们的socket处于RCV_SHUTDOWN状态,此后,该socket还会有epollin事件发生吗?

同理,我们调用shutdown方法,关闭了send端,使我们的socket处于SEND_SHUTDOWN状态,此后,还会有epollout事件吗?

其实,对认真读过之前几篇文章的同学来说,这个问题已经很简单了,答案就是会。

因为,当有任意epoll事件发生时,内核只是把该socket放到epoll的事件就绪队列里,等我们下次调用epoll_wait方法时,epoll内部会再调用这个队列里的各个socket的tcp_poll方法,检查该socket此时所有就绪的事件,然后将这些事件返回给用户。

也就是说,即使内核通知epoll,该socket有epollin事件,epoll内部还是会检查该socket是否还有其他事件,epoll会把所有就绪事件收集好之后,一起返回给用户。

tcp/epoll体系中关键的tcp_poll方法我们之前的文章已经分析过了,这里再拿来看下:

// 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;
        const struct tcp_sock *tp = tcp_sk(sk);
        int state;
        ...
        state = inet_sk_state_load(sk);
        ...
        mask = 0;
        ...
        // 该socket的既是RCV_SHUTDOWN,又是SEND_SHUTDOWN,或者状态是TCP_CLOSE
        // 对应的epoll事件都是EPOLLHUP
        if (sk->sk_shutdown == SHUTDOWN_MASK || state == TCP_CLOSE)
                mask |= EPOLLHUP;
        
        // 该socket是RCV_SHUTDOWN,比如对方用shutdown(sockfd, SHUT_WR)方法
        // 关闭它的SEND_SHUTDOWN,也就是关闭了我们的RCV_SHUTDOWN
        // 又比如,我们用shutdown(sockfd, SHUT_RD)方法,关闭我们自己的RCV_SHUTDOWN
        // 在此模式下,epoll事件为EPOLLIN
        if (sk->sk_shutdown & RCV_SHUTDOWN)
                mask |= EPOLLIN | EPOLLRDNORM | EPOLLRDHUP;

        // 当我们的socket处于TCP_ESTABLISHED等状态时
        if (state != TCP_SYN_SENT &&
            (state != TCP_SYN_RECV || tp->fastopen_rsk)) {
                ...
                // 如果我们的socket里有可读字节,epoll对应的事件就是EPOLLIN
                if (tcp_stream_is_readable(tp, target, sk))
                        mask |= EPOLLIN | EPOLLRDNORM;

                if (!(sk->sk_shutdown & SEND_SHUTDOWN)) {
                        // 如果我们的socket有可写空间,epoll事件就是EPOLLOUT
                        if (sk_stream_is_writeable(sk)) {
                                mask |= EPOLLOUT | EPOLLWRNORM;
                        } else {
                                ...
                        }
                } else
                        // 如果我们的socket关闭了SEND_SHUTDOWN,epoll事件就是EPOLLOUT
                        mask |= EPOLLOUT | EPOLLWRNORM;
                ...
        } else if (state == TCP_SYN_SENT && inet_sk(sk)->defer_connect) {
                ...
        }
        ...
        // 如果我们的socket发生错误了,epoll事件就是EPOLLERR
        if (sk->sk_err || !skb_queue_empty(&sk->sk_error_queue))
                mask |= EPOLLERR;

        return mask;
}
EXPORT_SYMBOL(tcp_poll);

该方法就是epoll检查socket有哪些就绪事件时调用的方法。

由该方法可见,只要socket处于RCV_SHUTDOWN状态,就一直有epollin事件,只要socket处于SEND_SHUTDOWN状态,就一直有epollout事件。

写段代码来证明下,先看epollin事件:

#include <arpa/inet.h>
#include <assert.h>
#include <errno.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>
#include <strings.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <unistd.h>

#define PORT 9999
#define MAX_EVENTS 10

static int tcp_listen() {
  int lfd, opt, err;
  struct sockaddr_in addr;

  lfd = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP);
  assert(lfd != -1);

  opt = 1;
  err = setsockopt(lfd, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
  assert(!err);

  bzero(&addr, sizeof(addr));
  addr.sin_family = AF_INET;
  addr.sin_addr.s_addr = INADDR_ANY;
  addr.sin_port = htons(PORT);

  err = bind(lfd, (struct sockaddr *)&addr, sizeof(addr));
  assert(!err);

  err = listen(lfd, 8);
  assert(!err);

  return lfd;
}

static void epoll_ctl_add(int epfd, int fd, int evts) {
  struct epoll_event ev;
  ev.events = evts;
  ev.data.fd = fd;
  int err = epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev);
  assert(!err);
}

static void handle_events(struct epoll_event *e, int epfd) {
  int err, m;
  char buf[8192];
  static int n;

  printf("sockfd %d: ", e->data.fd);

  if (e->events & EPOLLIN) {
    printf("EPOLLIN");
    e->events &= ~EPOLLIN;
    m = read(e->data.fd, buf, 1);
    assert(m == 0);
    printf("(read返回0) ");
  }

  if (e->events & EPOLLOUT) {
    printf("EPOLLOUT ");
    e->events &= ~EPOLLOUT;
        n++;
  }

  if (e->events & EPOLLHUP) {
    printf("EPOLLHUP ");
    e->events &= ~EPOLLHUP;
  }

  if (e->events & EPOLLERR) {
    printf("EPOLLERR ");
    e->events &= ~EPOLLERR;
  }

  assert(e->events == 0);
  printf("\n");

  if (n == 1) { // 连接建立成功后直接关闭receive端
    err = shutdown(e->data.fd, SHUT_RD);
    assert(!err);
  }

  while (n > 1) {
    err = write(e->data.fd, buf, sizeof(buf) / sizeof(buf[0]));
    if (err == -1) {
      assert(errno == EAGAIN);
      break;
    }
  }
}

int main(int argc, char *argv[]) {
  int epfd, lfd, cfd, err, n;
  struct epoll_event events[MAX_EVENTS];

  epfd = epoll_create1(0);
  assert(epfd != -1);

  lfd = tcp_listen();
  epoll_ctl_add(epfd, lfd, EPOLLIN);

  for (;;) {
    n = epoll_wait(epfd, events, MAX_EVENTS, -1);
    assert(n != -1);

    for (int i = 0; i < n; i++) {
      if (events[i].data.fd != lfd) {
        handle_events(&events[i], epfd);
        continue;
      }

      cfd = accept(lfd, NULL, NULL);
      assert(cfd != -1);

      err = fcntl(cfd, F_SETFL, O_NONBLOCK);
      assert(!err);

      epoll_ctl_add(epfd, cfd, EPOLLIN | EPOLLOUT | EPOLLET);
    }
  }

  return 0;
}

和之前一样,这里我们只要重点关注handle_events方法就好。

执行该程序后,用ncat对其进行连接,该程序所在终端的输出如下:

$ gcc server.c && ./a.out
sockfd 5: EPOLLOUT
sockfd 5: EPOLLIN(read返回0) EPOLLOUT
sockfd 5: EPOLLIN(read返回0) EPOLLOUT
sockfd 5: EPOLLIN(read返回0) EPOLLOUT
sockfd 5: EPOLLIN(read返回0) EPOLLOUT
# 一直输出上面相同行 #

可以看到,当我们用write方式一直触发epollout事件时,epollin事件也在同时发生。

所以,即使我们read返回0,也不能保证之后不会发生epollin事件。

我们再来看下epollout事件是否也是这样。

先将handle_events方法改成下面这样:

static void handle_events(struct epoll_event *e, int epfd) {
  int err;
  static int n;

  printf("sockfd %d: ", e->data.fd);

  if (e->events & EPOLLIN) {
    printf("EPOLLIN ");
    e->events &= ~EPOLLIN;
  }

  if (e->events & EPOLLOUT) {
    printf("EPOLLOUT ");
    e->events &= ~EPOLLOUT;
    n++;
  }

  if (e->events & EPOLLHUP) {
    printf("EPOLLHUP ");
    e->events &= ~EPOLLHUP;
  }

  if (e->events & EPOLLERR) {
    printf("EPOLLERR ");
    e->events &= ~EPOLLERR;
  }

  assert(e->events == 0);
  printf("\n");

  if (n == 1) { // 连接建立成功后直接关闭send端
    err = shutdown(e->data.fd, SHUT_WR);
    assert(!err);
  }
}

运行该程序后,用ncat对其建立tcp连接,然后一直在ncat终端输入数据,你会看到运行我们程序的终端有如下输出:

$ gcc server.c && ./a.out
sockfd 5: EPOLLOUT
sockfd 5: EPOLLOUT
sockfd 5: EPOLLOUT
sockfd 5: EPOLLIN EPOLLOUT
sockfd 5: EPOLLIN EPOLLOUT
sockfd 5: EPOLLIN EPOLLOUT
# 一直输出上面相同行 #

由上可见,即使我们关闭了send端,epollout事件还是会返回。

但如果我们不想要这种结果呢?比如说,当read返回0后,就不要再返回epollin事件,这怎么做呢?

其实说来也简单,你只要把你不想要的事件从epoll注册中移除就好了。

虽然epoll还是会调用tcp_poll方法,返回的socket事件还是包含所有的就绪事件,但它在返回给用户时,会过滤掉我们不感兴趣的事件。

所以,当read返回0时,你只要把epollin事件从epoll注册中取消,以后就再也不会有这个事件发生了。

本文分享自微信公众号 - Linux内核及JVM底层相关技术研究(ytcode),作者:wangyuntao

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2019-08-04

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • socket的epollin/epollout是何时触发的

    本篇文章的问题是,在 EPOLLET 模式下,socket的 EPOLLIN 和 EPOLLOUT 是何时触发的?

    wangyuntao
  • epoll和shutdown使用不当可能导致死循环

    本来是在研究epoll的另一个问题的,结果发现这个问题,所以这篇文章就先写这个问题吧。

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

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

    wangyuntao
  • 大话 Select、Poll、Epoll

    提到select、poll、epoll相信大家都耳熟能详了,三个都是IO多路复用的机制,可以监视多个描述符的读/写等事件。

    黄日成
  • 网络通信基础重难点解析 12 :Linux epoll 模型

    综合 select 和 poll 的一些优缺点,Linux 从内核 2.6 版本开始引入了更高效的 epoll 模型,本节我们来详细介绍 epoll 模型。

    范蠡
  • 碎片化 | 第四阶段-54-hibernate-spring整合流程-视频

    如清晰度低,可转PC网页观看高清版本: http://v.qq.com/x/page/g0568hww5e2.html Spring+Hibernate整合 ...

    码神联盟
  • 应用scrapy爬虫框架

    scrapy=scrap+python,是python自动化爬虫框架,相当于一个模板。当启动了一个scrapy工程后,会自动生成若干相互关联的文件,用户仅需根据...

    luanhz
  • Leetcode 46. Permutations

    版权声明:博客文章都是作者辛苦整理的,转载请注明出处,谢谢! https://blog.csdn....

    Tyan
  • 【leetcode刷题】20T20-搜索插入位置

    https://leetcode-cn.com/problems/search-insert-position

    木又AI帮
  • Leetcode 47. Permutations II

    版权声明:博客文章都是作者辛苦整理的,转载请注明出处,谢谢! https://blog.csdn....

    Tyan

扫码关注云+社区

领取腾讯云代金券