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空转问题,在select
和poll
模式下都不会出现,所以我们要记住是EPOLL
的CPU空转问题。
那么为什么Netty的NIO已经使用了EPOLL
的情况下,Netty还要自己花大心思实现一个自己的EPOLL
呢,作者给出了解释:
- Netty的
EPOLL
用的是边缘触发,性能更好。 - Netty的
EPOLL
开放了更多的参数。
注意:慎用Netty的EPOLL
,这个建议来自闪电侠,新美大推送系统负责人,专注Netty。原因有二,其一,Netty的EPOLL
有BUG,目前他也没有排查出这个BUG出在那,可能出在内核里。其二,由于Netty的EPOLL
和NIO模式底层都是EPOLL
,所以性能不会差特别多。
再说一下我们为什么还要分析Netty的EPOLL
:
- Netty的
EPOLL
使用边缘触发,我们通过分析可以更了解如何使用边缘触发。 - Netty的
EPOLL
底层使用了一些Linux的函数,对我们理解EPOLL
很有帮助,比如可能很多人理解EPOLL
是给SOCKET
用的,其实EPOLL
可以监听任何支持EPOLL
的文件描述符的IO情况。
我们可以看一下EpollEventLoop
中存在这样的代码:
private final FileDescriptor eventFd;
private final FileDescriptor timerFd;
这就是我们说的两种文件描述符,eventfd
和timerfd
。我们先讲解一下这两种文件描述符的作用。
EVENTFD
eventfd
是一个Linux系统提供的一个系统调用,通过一个共享的64位计数器完成进程间通讯。我们看下涉及到的几个系统调用,这里提一下,有点对不起大家,由于我自己的是mac系统没法对代码进行调试,所以本篇文章给出的代码都是网上找来的,代码逻辑看过,但是真的跑起来可能不是一回事,这个有Linux操作系统的同学可以跑一下。
创建:
int eventfd(unsigned int initval, int flags);
创建的时候可以传入一个计数器的初始值initval
。flags
是标记,具体有以下几种,使用时跟selectionKey
一样,用|
表示多个:
EFD_CLOEXEC
:fork
子进程的时候不继承父进程的这个文件描述符。多线程时基本都需要设置。EFD_NONBLOCK
:如果没有设置了这个标志位,那read操作将会阻塞直到计数器中有值。如果设置这个标志位,计数器值为0的时候也会立即返回-1。EFD_SEMAPHORE
:信号量模式。在计数器中的值大于0的情况下,read
操作时返回1,计数器减一。如果没有设置,返回计数器中的值,计数器归0。
读写操作:
eventfd_write
:写操作。表示向计数器中写入一个数值。多次写入会进行累加操作。eventfd_read
:读操作。表示从计数器中读取,根据EFD_SEMAPHORE
和EFD_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可以进行如下操作:read
、select(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
会阻塞到定时器到期。
总结
这一篇文章主要是写了eventfd
和timerfd
的作用。
我们后面会在Netty的EPOLL
中看到这两种文件描述符的作用。Netty的EPOLL
使用eventfd
做唤醒操作,使用timerfd
控制超时。我们会在后面的文章中清楚的看到EPOLL
如何监控各种类型的文件描述符以及EPOLL
使用边缘触发的情况下需要注意的一些点。