netty中的epoll实现

前言

在java中,IO多路复用的功能经过nio中的Selector提供,在不一样的操做系统下jdk会经过spi的方式加载不一样的实现,好比在macos下是KQueueSelectorProviderKQueueSelectorProvider底层使用了kqueue来进行IO多路复用;在linux 2.6之后的版本则是EPollSelectorProviderEPollSelectorProvider底层使用的是epoll。虽然jdk自身提供了selector的epoll实现,netty仍实现了本身的epoll版本,根据netty开发者在StackOverflow的回答,主要缘由有两个:html

  1. 支持更多socket option,好比TCP_CORK和SO_REUSEPORT
  2. 使用了边缘触发(ET)模式

接下来就来看看netty本身实现的epoll版本的大概逻辑。java

整体介绍

使用方式

在netty中,若是须要使用netty本身的epoll实现,须要在项目中添加netty-transport-native-epoll依赖,而后将代码中的NioEvnetLoopNioSocketChannelNioServerSocketChannel等替换为Epoll开头的类便可。具体参考Using the Linux native transportlinux

与jdk原生实现的区别

总的来讲,不论是jdk仍是netty的版本,都是直接调用了linux的epoll来提供IO多路复用,netty的epoll实现与jdk的区别主要有两个:macos

  1. 使用了边缘触发(能够参考个人另外一篇文章
  2. 使用了eventfd和timerfd来实现唤醒和超时控制,而jdk的实现则是使用了pipe和epoll自带的超时机制

具体实现

初始化

EpollEventLoop在初始化时会建立三个fd:epollFd、eventFd、timerFd。epollFd用于进一步调用epoll_wait,而另外两个fd的做用前面已经提到了。除此以外,EpollEventLoop内部还维护了一个selectStrategy变量,selectStrategy用于决定当前的loop中的行为,内容不算复杂,具体的就再也不展开了。api

EpollEventLoop还维护了一个EpollEventArray类型的对象events,events就是epoll调用时的第二个参数,表示感兴趣的描述符集合,这个变量会被传递到native方法中。socket

此外EpollEventLoop还有一个IntObjectMap<AbstractEpollChannel>类型的channels字段,表示当前EventLoop注册的全部Channel对象,其中key是channel对应的fd(文件描述符),由于epoll中接受的参数和返回的结果都是以整数形式的文件描述符表示的,value就是一个Channel对象,后续对Channel进行读写都会从这里查找(注:这里使用的IntObjectMap是netty本身实现的集合,主要目的是提高使用原生类型做为key或者value时的集合的性能,相似的实现还有hppc、FastUtil等等)。ide

注册感兴趣的链接

EpollEventLoopdoRegister方法中实现了注册链接的逻辑,就是调用EpollEventLoopadd方法:oop

void add(AbstractEpollChannel ch) throws IOException {
        assert inEventLoop();
        int fd = ch.socket.intValue();
        Native.epollCtlAdd(epollFd.intValue(), fd, ch.flags);
        AbstractEpollChannel old = channels.put(fd, ch);
    }
复制代码

能够看到这里调用了Native.epolCtlAdd,从名字就能够看出来,底层是调用了epoll_ctl方法,而后op参数为EPOLL_ADD。post

事件循环

EpollEventLoop的主体就在它的run方法里,在run方法的主循环中会先经过selectStrategy决定要进行的操做是epollWait仍是epollBusyWait。epollWait和epollBusyWait的区别就在于前者会计算出适合的超时时间而后调用一次epoll_wait直到有描述符就绪或超时,然后者会循环调用epoll_wait并将超时时间设置为0(也就是当即返回)直到有链接就绪为止。性能

经过epollWait或者epollBusyWait得到的结果会保存在events当中,因此接下来就是调用processReady处理events中的各个就绪的fd。处理的过程就是根据fd从channels查到对应的channel而后进行读写等操做,详细的读写就再也不展开介绍了。

超时和唤醒

前面提到了,netty的epoll逻辑中使用了eventfd和timerfd来实现唤醒和超时控制,evnetfd和timerfd从linux 2.6.22版本开始加入内核,其主要功能就是提供事件通知机制。eventfd能够建立一个文件描述符,在这个描述符上能够传递无符号整数,能够用来做为控制信息。timerfd也是建立一个文件描述符,在这个描述符上能够读取定时器事件,timerfd能够支持到纳秒级别。因为eventfd和timerfd都是基于描述符的,因此和select/poll/epoll这些api都比较契合。

EpollEventLoop在初始化时会首先建立epollfd、eventfd和timerfd,而后把eventfd和timerfd都加入到epoll的监听队列当中。eventfd用来作唤醒的支持,当须要唤醒EpollEventLoop时,就往eventfd写入一个数,这时eventfd就会变得可读,epoll就会及时返回。timerfd则做为epoll的超时控制,当须要超时的时候就在timerfd上设置一个时间间隔,超时时间到了以后timerfd就会变得可读,epoll也就会及时返回。这里使用timerfd做为超时控制而不是使用epoll自带的超时的缘由大概有两个,一是使用timerfd能够用统一的处理方式对待超时事件和IO事件,二是timerfd支持的超时时间精度更高。

顺便提一下,在jdk原生的实现中,唤醒是经过pipe实现的,Selector内部维护了一个pipe,初始化时将pipe的read端加入epoll的监听队列,当须要唤醒时就在pipe的write端写入数据,这样epoll就会及时返回。epoll返回后若是发现pipe可读,则将pipe中的数据读取完。

其余

在以前的文章中提到过,将fd注册到epoll时若是采用了边缘触发,那么建议的使用方式是将fd设置为非阻塞模式,而且在描述符就绪时须要将就绪数据所有读取完(遇到EAGAIN)为止,不然可能会出现再也没法收到就绪通知的状况。

而在netty的epoll实现中,全部的socket都是以ET模式注册的,而eventfd和timerfd则稍有不一样。在netty 4.1.38.Final之前的版本,eventfd在注册到epollfd时使用时LT而不是ET,在每次processReady时若是eventfd可读则都会对其调用一次read。timerfd在注册到epollfd时使用的时ET,可是在每次processReady时若是timerfd可读也会对其调用一次read。而在4.1.38.Final版本,eventfd和timerfd都使用了ET,可是并不在processReady方法中读取这两个fd。对于eventfd,会在每次write返回EAGAIN时调用一次read,由于eventfd内部只能存储一个整数,因此当write出现EAGAIN时就说明目前有数据须要读取。而对于timerfd则只会在epollWait出现超时的时候调用一次read,其余状况下不会对timerfd调用read。由于在netty的实现中,每次进行epoll_wait时都会从新设置timerfd的超时时间,而每次更新timerfd的超时时间时,timerfd就会从新变为不可读状态,也就不用对其调用read了。

相关文章
相关标签/搜索