在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了。