在java中,IO多路复用的功能经过nio中的Selector
提供,在不一样的操做系统下jdk会经过spi的方式加载不一样的实现,好比在macos下是KQueueSelectorProvider
,KQueueSelectorProvider
底层使用了kqueue来进行IO多路复用;在linux 2.6之后的版本则是EPollSelectorProvider
,EPollSelectorProvider
底层使用的是epoll。虽然jdk自身提供了selector的epoll实现,netty仍实现了本身的epoll版本,根据netty开发者在StackOverflow的回答,主要缘由有两个:html
接下来就来看看netty本身实现的epoll版本的大概逻辑。java
在netty中,若是须要使用netty本身的epoll实现,须要在项目中添加netty-transport-native-epoll依赖,而后将代码中的NioEvnetLoop
、NioSocketChannel
、NioServerSocketChannel
等替换为Epoll开头的类便可。具体参考Using the Linux native transportlinux
总的来讲,不论是jdk仍是netty的版本,都是直接调用了linux的epoll来提供IO多路复用,netty的epoll实现与jdk的区别主要有两个:macos
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
EpollEventLoop
的doRegister
方法中实现了注册链接的逻辑,就是调用EpollEventLoop
的add
方法: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了。