Netty源码分析——EPOLL前传之EVENTFD、TIMERFD

1,476 阅读5分钟

Netty源码分析——EPOLL前传之EVENTFD、TIMERFD

前言

最近打算开始一个新的部分,关于Netty的EPOLL。很多人有一个误解,包括一些经常使用Netty的都会认为Netty的NIO就是select(底层用的JDK的select),而Netty的EPOLL性能好是因为底层是epoll

这个地方是不对的。Netty的NIO底层就是EPOLL。Netty的EPOLL底层是自己通过调用系统调用,通过TIMERFD实现的自己的EPOLL。可能有人要问Netty的NIO底层调用的是select啊,其实JVM在select这个native方法底层会进行判断,如果当前系统支持EPOLL就自动启用EPOLL。这也是为什么JDK的NIO会出现CPU空转问题,这个CPU空转问题,在selectpoll模式下都不会出现,所以我们要记住是EPOLL的CPU空转问题。

那么为什么Netty的NIO已经使用了EPOLL的情况下,Netty还要自己花大心思实现一个自己的EPOLL呢,作者给出了解释:

  1. Netty的EPOLL用的是边缘触发,性能更好。
  2. Netty的EPOLL开放了更多的参数。

注意:慎用Netty的EPOLL,这个建议来自闪电侠,新美大推送系统负责人,专注Netty。原因有二,其一,Netty的EPOLL有BUG,目前他也没有排查出这个BUG出在那,可能出在内核里。其二,由于Netty的EPOLL和NIO模式底层都是EPOLL,所以性能不会差特别多。

再说一下我们为什么还要分析Netty的EPOLL

  1. Netty的EPOLL使用边缘触发,我们通过分析可以更了解如何使用边缘触发。
  2. Netty的EPOLL底层使用了一些Linux的函数,对我们理解EPOLL很有帮助,比如可能很多人理解EPOLL是给SOCKET用的,其实EPOLL可以监听任何支持EPOLL的文件描述符的IO情况。

我们可以看一下EpollEventLoop中存在这样的代码:

private final FileDescriptor eventFd;
private final FileDescriptor timerFd;

这就是我们说的两种文件描述符,eventfdtimerfd。我们先讲解一下这两种文件描述符的作用。

EVENTFD

eventfd是一个Linux系统提供的一个系统调用,通过一个共享的64位计数器完成进程间通讯。我们看下涉及到的几个系统调用,这里提一下,有点对不起大家,由于我自己的是mac系统没法对代码进行调试,所以本篇文章给出的代码都是网上找来的,代码逻辑看过,但是真的跑起来可能不是一回事,这个有Linux操作系统的同学可以跑一下。

创建:

int eventfd(unsigned int initval, int flags);

创建的时候可以传入一个计数器的初始值initvalflags是标记,具体有以下几种,使用时跟selectionKey一样,用|表示多个:

  • EFD_CLOEXECfork子进程的时候不继承父进程的这个文件描述符。多线程时基本都需要设置。
  • EFD_NONBLOCK:如果没有设置了这个标志位,那read操作将会阻塞直到计数器中有值。如果设置这个标志位,计数器值为0的时候也会立即返回-1。
  • EFD_SEMAPHORE:信号量模式。在计数器中的值大于0的情况下,read操作时返回1,计数器减一。如果没有设置,返回计数器中的值,计数器归0。

读写操作:

  • eventfd_write:写操作。表示向计数器中写入一个数值。多次写入会进行累加操作。
  • eventfd_read:读操作。表示从计数器中读取,根据EFD_SEMAPHOREEFD_NONBLOCK返回结果。

DEMO:

#include <sys/eventfd.h>
#include <unistd.h>
#include <iostream>

int main() {
    int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC);
    eventfd_write(efd, 2);
    eventfd_write(efd, 3);
    eventfd_write(efd, 4);
    eventfd_t count;
    int read_result = eventfd_read(efd, &count);
    
    // 第一次读
    std::cout << "read_result=" << read_result << std::endl;
    std::cout << "count=" << count << std::endl;
    
    read_result = eventfd_read(efd, &count);
    // 由于是非阻塞模式,所以这里会打印-1
    std::cout << "read_result=" << read_result << std::endl;
    // 第二次读,由于读失败了,所以count的值不变还是9
    std::cout << "count=" << count << std::endl;
    close(efd);
}

运行结果:
read_result=0
count=9
read_result=-1
count=9

TIMERFD

继续看一下timerfd

创建:

int timerfd_create(int clockid, int flags);

创建一个timerfd,返回的fd可以进行如下操作:readselect(poll、epoll)close。这里可以看到我们是可以用EPOLL来监听timerfd的。

设置timer的周期及间隔:

int timerfd_settime(int fd, int flags, const struct itimerspec *new_value,struct itimerspec *old_value);

参数中的数据结构如下:

struct timespec { 
    time_t tv_sec; /* Seconds */ 
    long tv_nsec; /* Nanoseconds */ 
}; 

struct itimerspec { 
    struct timespec it_interval; /* Interval for periodic timer */ 
    struct timespec it_value; /* Initial expiration */ 
};

DEMO:

#include <sys/timerfd.h> 
#include <sys/time.h> 
#include <time.h> 
#include <unistd.h> 
#include <stdlib.h> 
#include <stdio.h> 
#include <stdint.h>

#define handle_error(msg) \ 
do { perror(msg); exit(EXIT_FAILURE); } while (0) 

void printTime() { 
    struct timeval tv; 
    gettimeofday(&tv, NULL); 
    printf("printTime: current time:%ld.%ld ", tv.tv_sec, tv.tv_usec); 
} 

int main(int argc, char *argv[]) { 
  struct timespec now; 
  if (clock_gettime(CLOCK_REALTIME, &now) == -1) {
    handle_error("clock_gettime"); 
  }

  // 初始化定时器的参数,初始时间和定时间隔
  struct itimerspec new_value; 
  new_value.it_value.tv_sec = now.tv_sec + atoi(argv[1]); 
  new_value.it_value.tv_nsec = now.tv_nsec; 
  new_value.it_interval.tv_sec = atoi(argv[2]); 
  new_value.it_interval.tv_nsec = 0; 

  // 创建定时器
  int fd = timerfd_create(CLOCK_REALTIME, 0); 
  if (fd == -1) {
    handle_error("timerfd_create"); 
  }

  if (timerfd_settime(fd, TFD_TIMER_ABSTIME, &new_value, NULL) == -1) {
    handle_error("timerfd_settime"); 
  }

  printTime(); 
  printf("timer started\n"); 

  for (uint64_t tot_exp = 0; tot_exp < atoi(argv[3]);) { 
      uint64_t exp; 
      // 阻塞等待定时器到期。返回值是未处理的到期次数。
      // 比如定时间隔为2秒,但过了10秒才去读取,则读取的值是5。
      ssize_t s = read(fd, &exp, sizeof(uint64_t)); 
      if (s != sizeof(uint64_t)) {
        handle_error("read"); 
      }

      tot_exp += exp; 
      printTime(); 
      printf("read: %llu; total=%llu\n", exp, tot_exp); 
  } 
  exit(EXIT_SUCCESS); 
}

注意我们读取timerfd会阻塞到定时器到期。

总结

这一篇文章主要是写了eventfdtimerfd的作用。

我们后面会在Netty的EPOLL中看到这两种文件描述符的作用。Netty的EPOLL使用eventfd做唤醒操作,使用timerfd控制超时。我们会在后面的文章中清楚的看到EPOLL如何监控各种类型的文件描述符以及EPOLL使用边缘触发的情况下需要注意的一些点。