前言
最近打算开始一个新的部分, 关于 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 使用边缘触发的情况下需要注意的一些点.
来源: https://juejin.im/entry/5bf4d3e6f265da615a4170fc