前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >read方法返回0后还会有epollin事件吗

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

作者头像
KINGYT
发布2019-08-06 14:49:56
2.4K0
发布2019-08-06 14:49:56
举报

完整的问题是:

当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方法我们之前的文章已经分析过了,这里再拿来看下:

代码语言:javascript
复制
// 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事件:

代码语言:javascript
复制
#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对其进行连接,该程序所在终端的输出如下:

代码语言:javascript
复制
$ 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方法改成下面这样:

代码语言:javascript
复制
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终端输入数据,你会看到运行我们程序的终端有如下输出:

代码语言:javascript
复制
$ 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注册中取消,以后就再也不会有这个事件发生了。

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

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

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

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

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