netty中的epoll實現
前言
在java中,IO多路複用的功能通過nio中的Selector
提供,在不同的作業系統下jdk會通過spi的方式載入不同的實現,比如在macos下是KQueueSelectorProvider
,KQueueSelectorProvider
底層使用了kqueue來進行IO多路複用;在linux 2.6以後的版本則是EPollSelectorProvider
,EPollSelectorProvider
底層使用的是epoll。雖然jdk自身提供了selector的epoll實現,netty仍實現了自己的epoll版本,根據netty開發者在StackOverflow的回答,主要原因有兩個:
- 支援更多socket option,比如TCP_CORK和SO_REUSEPORT
- 使用了邊緣觸發(ET)模式
接下來就來看看netty自己實現的epoll版本的大概邏輯。
總體介紹
使用方式
在netty中,如果需要使用netty自己的epoll實現,需要在專案中新增netty-transport-native-epoll依賴,然後將程式碼中的NioEvnetLoop
、NioSocketChannel
、NioServerSocketChannel
等替換為Epoll開頭的類即可。具體參考Using the Linux native transport
與jdk原生實現的區別
總的來說,不管是jdk還是netty的版本,都是直接呼叫了linux的epoll來提供IO多路複用,netty的epoll實現與jdk的區別主要有兩個:
- 使用了邊緣觸發(可以參考我的另一篇文章)
- 使用了eventfd和timerfd來實現喚醒和超時控制,而jdk的實現則是使用了pipe和epoll自帶的超時機制
具體實現
初始化
EpollEventLoop
在初始化時會建立三個fd:epollFd、eventFd、timerFd。epollFd用於進一步呼叫epoll_wait,而另外兩個fd的作用前面已經提到了。除此之外,EpollEventLoop
內部還維護了一個selectStrategy
變數,selectStrategy
用於決定當前的loop中的行為,內容不算複雜,具體的就不再展開了。
EpollEventLoop
還維護了一個EpollEventArray
此外EpollEventLoop
還有一個IntObjectMap<AbstractEpollChannel>
型別的channels欄位,表示當前EventLoop註冊的所有Channel物件,其中key是channel對應的fd(檔案描述符),因為epoll中接受的引數和返回的結果都是以整數形式的檔案描述符表示的,value就是一個Channel物件,後續對Channel進行讀寫都會從這裡查詢(注:這裡使用的IntObjectMap是netty自己實現的集合,主要目的是提升使用原生型別作為key或者value時的集合的效能,類似的實現還有hppc、FastUtil等等)。
註冊感興趣的連線
EpollEventLoop
的doRegister
方法中實現了註冊連線的邏輯,就是呼叫EpollEventLoop
的add
方法:
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。
事件迴圈
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了。