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

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中存在这样的代码:

1
2
private final FileDescriptor eventFd;
private final FileDescriptor timerFd;

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

EVENTFD

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

创建:

1
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
#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

创建:

1
int timerfd_create(int clockid, int flags);

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

设置timer的周期及间隔:

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

参数中的数据结构如下:

1
2
3
4
5
6
7
8
9
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:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
#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会阻塞到定时器到期。打印结果:

1
2
3
4
5
6
printTime:  current time:xxx timer started
printTime: current time:xxx read: 1; total=1
printTime: current time:xxx read: 1; total=2
printTime: current time:xxx read: 1; total=3
printTime: current time:xxx read: 1; total=4
...

总结

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

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