1. 程式人生 > 程式設計 >netty中的epoll實現

netty中的epoll實現

前言

在java中,IO多路複用的功能通過nio中的Selector提供,在不同的作業系統下jdk會通過spi的方式載入不同的實現,比如在macos下是KQueueSelectorProviderKQueueSelectorProvider底層使用了kqueue來進行IO多路複用;在linux 2.6以後的版本則是EPollSelectorProviderEPollSelectorProvider底層使用的是epoll。雖然jdk自身提供了selector的epoll實現,netty仍實現了自己的epoll版本,根據netty開發者在StackOverflow的回答,主要原因有兩個:

  1. 支援更多socket option,比如TCP_CORK和SO_REUSEPORT
  2. 使用了邊緣觸發(ET)模式

接下來就來看看netty自己實現的epoll版本的大概邏輯。

總體介紹

使用方式

在netty中,如果需要使用netty自己的epoll實現,需要在專案中新增netty-transport-native-epoll依賴,然後將程式碼中的NioEvnetLoopNioSocketChannelNioServerSocketChannel等替換為Epoll開頭的類即可。具體參考Using the Linux native transport

與jdk原生實現的區別

總的來說,不管是jdk還是netty的版本,都是直接呼叫了linux的epoll來提供IO多路複用,netty的epoll實現與jdk的區別主要有兩個:

  1. 使用了邊緣觸發(可以參考我的另一篇文章
  2. 使用了eventfd和timerfd來實現喚醒和超時控制,而jdk的實現則是使用了pipe和epoll自帶的超時機制

具體實現

初始化

EpollEventLoop在初始化時會建立三個fd:epollFd、eventFd、timerFd。epollFd用於進一步呼叫epoll_wait,而另外兩個fd的作用前面已經提到了。除此之外,EpollEventLoop內部還維護了一個selectStrategy變數,selectStrategy用於決定當前的loop中的行為,內容不算複雜,具體的就不再展開了。

EpollEventLoop還維護了一個EpollEventArray

型別的物件events,events就是epoll呼叫時的第二個引數,表示感興趣的描述符集合,這個變數會被傳遞到native方法中。

此外EpollEventLoop還有一個IntObjectMap<AbstractEpollChannel>型別的channels欄位,表示當前EventLoop註冊的所有Channel物件,其中key是channel對應的fd(檔案描述符),因為epoll中接受的引數和返回的結果都是以整數形式的檔案描述符表示的,value就是一個Channel物件,後續對Channel進行讀寫都會從這裡查詢(注:這裡使用的IntObjectMap是netty自己實現的集合,主要目的是提升使用原生型別作為key或者value時的集合的效能,類似的實現還有hppc、FastUtil等等)。

註冊感興趣的連線

EpollEventLoopdoRegister方法中實現了註冊連線的邏輯,就是呼叫EpollEventLoopadd方法:

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