前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >对又一个epoll问题的全面分析

对又一个epoll问题的全面分析

作者头像
KINGYT
发布2019-08-05 15:52:10
1.1K0
发布2019-08-05 15:52:10
举报

今天又碰到一个epoll相关的问题,花了一些时间解决,在这里记录下。

首先还是看段测试代码:

代码语言: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;
  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) {
    err = shutdown(e->data.fd, SHUT_WR);
    assert(!err);
  }
}

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方法。

该方法最主要的目的是,当客户端建立tcp连接到服务端时,服务端立即调用shutdown方法,关闭其send方向。

最开始我认为服务端socket会连续两次触发epollout事件,所以也会连续两次输出epollout。

第一次是tcp连接建立后,在把服务端的socket加入到epoll的监听队列时,会触发一次epollout事件,第二次是shutdown系统调用里会触发的epollout事件,这个上篇文章有说过原因。

但执行测试程序后,epollout居然连续输出了三次,也就是说明,服务端socket连续三次触发epollout事件。

这个我就不明白了。

想了好久原因,也用了各种方式测试,最终在又一次翻内核源码时找到了答案。

我们知道,在服务端socket调用shutdown方法发送fin包之后,其会进入TCP_FIN_WAIT1状态,对方收到fin包,会发送一个ack包,告诉服务端socket,它已经收到。

所以,最有可能再次触发epollout事件的地方就是这个ack包的处理逻辑部分。

我们来直接看下内核源码:

代码语言:javascript
复制
// net/ipv4/tcp_input.c
int tcp_rcv_state_process(struct sock *sk, struct sk_buff *skb)
{
        ...
        switch (sk->sk_state) {
        ...
        case TCP_FIN_WAIT1: {
                ...
                tcp_set_state(sk, TCP_FIN_WAIT2);
                sk->sk_shutdown |= SEND_SHUTDOWN;
                ...
                if (!sock_flag(sk, SOCK_DEAD)) {
                        // 下面这个方法触发epollout事件
                        sk->sk_state_change(sk);
                        break;
                }
                ...
        }
        ...
        }
        ...
}
EXPORT_SYMBOL(tcp_rcv_state_process);

果然如我们猜测的那样。

服务端socket在收到ack包后,会调用sk->sk_state_change方法,告知epoll,服务端的socket有新事件发生,而这个事件正是epollout事件。

口说无凭,如何证明一下真的是这个ack包导致的呢?

我们继续看。

首先我们先改下handle_events的代码,在shutdown之前,先sleep几秒,让我们有足够的时间做些其他事。

更改的结果如下:

代码语言: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) {
    printf("5秒钟之后将发送fin包给客户端,请于此请期间设置iptables规则\n");
    sleep(5);
    err = shutdown(e->data.fd, SHUT_WR);
    assert(!err);
  }
}

既然我们猜测是ack包导致的,那如果我们能让服务端socket无法收到这个ack包,理论上来说就不会有第三次epollout输出。

那怎么什么过滤这个ack包呢?用非常强大的iptables工具。

同时,我们也会辅以tcpdump的输出,来帮我们进一步确认结果。

我们先把实验过程的各个终端所有输出展示下。

下面是服务端输出:

代码语言:javascript
复制
$ gcc server.c && ./a.out
sockfd 5: EPOLLOUT
5秒钟之后将发送fin包给客户端,请于此请期间设置iptables规则
sockfd 5: EPOLLOUT
sockfd 5: EPOLLOUT

下面是客户端输出:

代码语言:javascript
复制
$ ncat -4 localhost 9999

下面是iptables输出:

代码语言:javascript
复制
$ sudo iptables -A INPUT -p tcp --dport 9999 --tcp-flags ACK ACK -j DROP && sudo iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-A INPUT -p tcp -m tcp --dport 9999 --tcp-flags ACK ACK -j DROP

$ sudo iptables -F && sudo iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT

下面是tcpdump的输出:

代码语言:javascript
复制
$ sudo tcpdump -i any -n# -tttt port 9999
    1  2019-08-02 18:20:57.715515 IP 127.0.0.1.58646 > 127.0.0.1.9999: Flags [S], seq 2234765919, win 65495, options [mss 65495,sackOK,TS val 3534452729 ecr 0,nop,wscale 7], length 0
    2  2019-08-02 18:20:57.715528 IP 127.0.0.1.9999 > 127.0.0.1.58646: Flags [S.], seq 1561670940, ack 2234765920, win 65483, options [mss 65495,sackOK,TS val 3534452729 ecr 3534452729,nop,wscale 7], length 0
    3  2019-08-02 18:20:57.715540 IP 127.0.0.1.58646 > 127.0.0.1.9999: Flags [.], ack 1, win 512, options [nop,nop,TS val 3534452729 ecr 3534452729], length 0
    
    4  2019-08-02 18:21:02.716015 IP 127.0.0.1.9999 > 127.0.0.1.58646: Flags [F.], seq 1, ack 1, win 512, options [nop,nop,TS val 3534457730 ecr 3534452729], length 0
    5  2019-08-02 18:21:02.718586 IP 127.0.0.1.58646 > 127.0.0.1.9999: Flags [.], ack 2, win 512, options [nop,nop,TS val 3534457732 ecr 3534457730], length 0
    
    6  2019-08-02 18:21:02.918653 IP 127.0.0.1.9999 > 127.0.0.1.58646: Flags [F.], seq 1, ack 1, win 512, options [nop,nop,TS val 3534457932 ecr 3534452729], length 0
    7  2019-08-02 18:21:02.918672 IP 127.0.0.1.58646 > 127.0.0.1.9999: Flags [.], ack 2, win 512, options [nop,nop,TS val 3534457932 ecr 3534457932,nop,nop,sack 1 {1:2}], length 0
    # 省略大量的fin包及ack包的输出
   20  2019-08-02 18:21:28.911939 IP 127.0.0.1.9999 > 127.0.0.1.58646: Flags [F.], seq 1, ack 1, win 512, options [nop,nop,TS val 3534483926 ecr 3534452729], length 0
   21  2019-08-02 18:21:28.911962 IP 127.0.0.1.58646 > 127.0.0.1.9999: Flags [.], ack 2, win 512, options [nop,nop,TS val 3534483926 ecr 3534483926,nop,nop,sack 1 {1:2}], length 0

实验的执行流程如下:

1. 开启tcpdump,监听客户端及服务端之间的tcp包。

2. 编译并启动服务端程序,此时服务端终端无任何输出,tcpdump也无任何输出。

3. 用ncat模拟客户端连接服务端,此时服务端终端输出第一次epollout,并紧接着输出上面的5秒提示那句话,同时tcpdump终端输出前三行,表示tcp三次握手成功,连接建立。

4. 在5秒钟之内,在iptables终端执行第一条命令,命令的大概意思就是丢弃到9999端口的tcp的ack包,也就是到服务端的ack包,该命令的输出内容可以参考上面。

5. 在5秒钟过后,服务端输出第二次epollout,证明shutdown方法确实通知了epoll,服务端socket有新epollout事件,而第三次epollout并没有输出。同时,tcpdump终端输出大量了fin和ack包,如上面的tcpdump的4到7行。

产生该行为的原因是,我们用iptables规则丢弃了客户端到服务端的ack包,服务端socket收不到这个ack包,就会重新发送fin包,所以就这样一直持续下去。

6. 等一段时间,在确定第三次epollout真的不会输出后,我们在iptables终端执行第二条命令,清除掉所有的iptables过滤规则。

过一段时间后,tcpdump终端会再一次输出服务端socket的fin重试包,以及客户端的ack包。 由于此次ack包不会被iptables过滤,所以服务端socket正常收到这个ack包,进而执行上面tcp_rcv_state_process方法内的逻辑,在该方法中,内核通知了epoll,服务端socket有新epollout事件。

也是在几乎同时,服务端终端输出第三次epollout。

实验结束。

该实验完美证实了,服务端socket的第三次epollout事件,就是由fin的ack包导致的。

不能再完美!

最后,在这里再推荐下我之前写的几篇相关文章:

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

socket的epollin/epollout是何时触发的

完。

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

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

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

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

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