Java NIO SelectorProvider與IO多路複用

weixin_34208283發表於2017-04-28

最近在學習Netty,看了好多資料,也看了一部分<<Netty in action>>這本書,發現完全不能理解它的設計,它的元件.

聯想到Netty主要是一個NIO框架,於是覺得是因為對NIO的瞭解不夠而導致的.然後就查閱NIO的相關資料,發現還是不能理解其原理.

不得不說,Google了很多資料,包括英文的和中文的,大多數都是NIO的具體用法,而對於其核心元件,比如selector,他們的作用,實現原理,卻並沒有說明.看完網上的介紹之後,讓我更加懵懵噠了.

既然查詢不到結果,就想自己檢視原始碼瞭解其原理.於是檢視了Oracle JDK1.8.0_91中和NIO相關的部分的原始碼,以及openjdk1.7的部分原始碼.因為Oracle JDK1.8.0_91中,對於一些類的實現,並沒有給出,只是給出的.class檔案.即使我們可以通過反編譯來獲得,但是終究還是太麻煩.所以這部分原始碼,就從openjdk1.7來獲得.

Java NIO SelectorProvider

檢視Oracle JDK1.8.0_91原始碼時,我們可以看到Selector這個元件,是由SelectorProvider建立的.

4108852-1e0a61939a3ed322.png

我們看一下SelectorProvider.provider()方法的具體實現:

4108852-85ac758e3ecaa710.png

檢視loadProviderFromProperty()方法和loadProviderAsService()方法的原始碼:

4108852-1cb9b777fdcadf61.png

我們可以看到,SelectorProvider.provider()方法會在System Property中不存在java.nio.channels.spi.SelectorProvider屬性和不能找到SelectorProvider的實現類時,建立一個預設的sun.nio.ch.DefaultSelectorProvider來作為SelectorProvider.

我們從open jdk7中檢視sun.nio.ch.DefaultSelectorProvider的原始碼:

4108852-ad38859eca05267f.png

open jdk7的原始碼中,提供了三個版本的sun.nio.ch.DefaultSelectorProvider的實現:

4108852-9deff58f06357930.png

我們這裡選擇的是solaris版本的.

sun.nio.ch.DefaultSelectorProvider的原始碼中,我們可以看到,如果是linux機器,並且其核心版本大於2.6,建立的就是EPollSelectorProvider,否則的話,就建立PollSelectorProvider.

這就是我們今天要介紹的重點-IO多路複用.

IO多路複用

IO多路複用就是我們說的select,poll, epoll,接下來我們會逐個介紹.

Select

基本概念

IO多路複用是指核心一旦發現程式指定的一個或者多個IO條件準備讀取,它就通知該程式。IO多路複用適用如下場合:

  • 當客戶處理多個描述字時(一般是互動式輸入和網路套介面),必須使用I/O複用。

  • 當一個客戶同時處理多個套介面時,而這種情況是可能的,但很少出現。

  • 如果一個TCP伺服器既要處理監聽套介面,又要處理已連線套介面,一般也要用到I/O複用。

  • 如果一個伺服器即要處理TCP,又要處理UDP,一般要使用I/O複用。

  • 如果一個伺服器要處理多個服務或多個協議,一般要使用I/O複用。

與多程式和多執行緒技術相比,I/O多路複用技術的最大優勢是系統開銷小,系統不必建立程式/執行緒,也不必維護這些程式/執行緒,從而大大減小了系統的開銷。

select函式

該函式准許程式指示核心等待多個事件中的任何一個傳送,並只在有一個或多個事件發生或經歷一段指定的時間後才喚醒。函式原型如下:

**int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set exceptset,const struct timeval timeout)

函式引數介紹如下:

(1)第一個引數maxfdp1指定待測試的描述字個數,它的值是待測試的最大描述字加1(因此把該引數命名為maxfdp1),描述字0、1、2...maxfdp1-1均將被測試。因為檔案描述符是從0開始的。

(2)中間的三個引數readset、writeset和exceptset指定我們要讓核心測試讀、寫和異常條件的描述字。如果對某一個的條件不感興趣,就可以把它設為空指標。struct fd_set可以理解為一個集合,這個集合中存放的是檔案描述符,可通過以下四個巨集進行設定:

          void FD_ZERO(fd_set *fdset);           //清空集合

          void FD_SET(int fd, fd_set *fdset);   //將一個給定的檔案描述符加入集合之中

          void FD_CLR(int fd, fd_set *fdset);   //將一個給定的檔案描述符從集合中刪除

          int FD_ISSET(int fd, fd_set *fdset);   // 檢查集合中指定的檔案描述符是否可以讀寫 

(3)timeout告知核心等待所指定描述字中的任何一個就緒可花多少時間。其timeval結構用於指定這段時間的秒數和微秒數。

         struct timeval{

                   long tv_sec;   //seconds

                   long tv_usec;  //microseconds

       };

這個引數有三種可能:

(1)永遠等待下去:僅在有一個描述字準備好I/O時才返回。為此,把該引數設定為空指標NULL。

(2)等待一段固定時間:在有一個描述字準備好I/O時返回,但是不超過由該引數所指向的timeval結構中指定的秒數和微秒數。

(3)根本不等待:檢查描述字後立即返回,這稱為輪詢。為此,該引數必須指向一個timeval結構,而且其中的定時器值必須為0。

基本原理圖

4108852-5c78e26976133281.png

poll

基本知識

poll的機制與select類似,與select在本質上沒有多大差別,管理多個描述符也是進行輪詢,根據描述符的狀態進行處理,但是poll沒有最大檔案描述符數量的限制。poll和select同樣存在一個缺點就是,包含大量檔案描述符的陣列被整體複製於使用者態和核心的地址空間之間,而不論這些檔案描述符是否就緒,它的開銷隨著檔案描述符數量的增加而線性增大。

poll函式

int poll ( struct pollfd * fds, unsigned int nfds, int timeout);

pollfd結構體定義如下:

struct pollfd {

int fd;         /* 檔案描述符 */
short events;         /* 等待的事件 */
short revents;       /* 實際發生了的事件 */
} ; 

每一個pollfd結構體指定了一個被監視的檔案描述符,可以傳遞多個結構體,指示poll()監視多個檔案描述符。每個結構體的events域是監視該檔案描述符的事件掩碼,由使用者來設定這個域。revents域是檔案描述符的操作結果事件掩碼,核心在呼叫返回時設定這個域。events域中請求的任何事件都可能在revents域中返回。合法的事件如下:

  POLLIN         有資料可讀。

  POLLRDNORM       有普通資料可讀。

  POLLRDBAND      有優先資料可讀。

  POLLPRI         有緊迫資料可讀。

  POLLOUT            寫資料不會導致阻塞。

  POLLWRNORM       寫普通資料不會導致阻塞。

  POLLWRBAND        寫優先資料不會導致阻塞。

  POLLMSGSIGPOLL     訊息可用。

  此外,revents域中還可能返回下列事件:
  POLLER     指定的檔案描述符發生錯誤。

  POLLHUP   指定的檔案描述符掛起事件。

  POLLNVAL  指定的檔案描述符非法。

這些事件在events域中無意義,因為它們在合適的時候總是會從revents中返回。

使用poll()和select()不一樣,你不需要顯式地請求異常情況報告。

POLLIN | POLLPRI等價於select()的讀事件,POLLOUT |POLLWRBAND等價於select()的寫事件。POLLIN等價於POLLRDNORM |POLLRDBAND,而POLLOUT則等價於POLLWRNORM。例如,要同時監視一個檔案描述符是否可讀和可寫,我們可以設定 events為POLLIN |POLLOUT。在poll返回時,我們可以檢查revents中的標誌,對應於檔案描述符請求的events結構體。如果POLLIN事件被設定,則檔案描述符可以被讀取而不阻塞。如果POLLOUT被設定,則檔案描述符可以寫入而不導致阻塞。這些標誌並不是互斥的:它們可能被同時設定,表示這個檔案描述符的讀取和寫入操作都會正常返回而不阻塞。

timeout引數指定等待的毫秒數,無論I/O是否準備好,poll都會返回。timeout指定為負數值表示無限超時,使poll()一直掛起直到一個指定事件發生;timeout為0指示poll呼叫立即返回並列出準備好I/O的檔案描述符,但並不等待其它的事件。這種情況下,poll()就像它的名字那樣,一旦選舉出來,立即返回。

成功時,poll()返回結構體中revents域不為0的檔案描述符個數;如果在超時前沒有任何事件發生,poll()返回0;失敗時,poll()返回-1,並設定errno為下列值之一:

  EBADF         一個或多個結構體中指定的檔案描述符無效。

  EFAULTfds   指標指向的地址超出程式的地址空間。

  EINTR      請求的事件之前產生一個訊號,呼叫可以重新發起。

  EINVALnfds  引數超出PLIMIT_NOFILE值。

  ENOMEM       可用記憶體不足,無法完成請求。

epoll

基本知識

epoll是在2.6核心中提出的,是之前的select和poll的增強版本。相對於select和poll來說,epoll更加靈活,沒有描述符限制。epoll使用一個檔案描述符管理多個描述符,將使用者關係的檔案描述符的事件存放到核心的一個事件表中,這樣在使用者空間和核心空間的copy只需一次。

epoll介面

epoll操作過程需要三個介面,分別如下:

#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

(1) int epoll_create(int size);
建立一個epoll的控制程式碼,size用來告訴核心這個監聽的數目一共有多大。這個引數不同於select()中的第一個引數,給出最大監聽的fd+1的值。需要注意的是,當建立好epoll控制程式碼後,它就是會佔用一個fd值,在linux下如果檢視/proc/程式id/fd/,是能夠看到這個fd的,所以在使用完epoll後,必須呼叫close()關閉,否則可能導致fd被耗盡。

(2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epoll的事件註冊函式,它不同與select()是在監聽事件時告訴核心要監聽什麼型別的事件epoll的事件註冊函式,它不同與select()是在監聽事件時告訴核心要監聽什麼型別的事件,而是在這裡先註冊要監聽的事件型別。第一個引數是epoll_create()的返回值,第二個參數列示動作,用三個巨集來表示:

EPOLL_CTL_ADD:註冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;

第三個引數是需要監聽的fd,第四個引數是告訴核心需要監聽什麼事,struct epoll_event結構如下:

struct epoll_event {
  __uint32_t events;  /* Epoll events */
  epoll_data_t data;  /* User data variable */
};

events可以是以下幾個巨集的集合:

EPOLLIN :表示對應的檔案描述符可以讀(包括對端SOCKET正常關閉);
EPOLLOUT:表示對應的檔案描述符可以寫;
EPOLLPRI:表示對應的檔案描述符有緊急的資料可讀(這裡應該表示有帶外資料到來);
EPOLLERR:表示對應的檔案描述符發生錯誤;
EPOLLHUP:表示對應的檔案描述符被結束通話;
EPOLLET: 將EPOLL設為邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來說的。
EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL佇列裡

(3) int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  等待事件的產生,類似於select()呼叫。引數events用來從核心得到事件的集合,maxevents告之核心這個events有多大,這個maxevents的值不能大於建立epoll_create()時的size,引數timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。該函式返回需要處理的事件數目,如返回0表示已超時。

工作模式

epoll對檔案描述符的操作有兩種模式:LT(level trigger)和ET(edge trigger)。LT模式是預設模式,LT模式與ET模式的區別如下:

LT模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程式,應用程式可以不立即處理該事件。下次呼叫epoll_wait時,會再次響應應用程式並通知此事件。

ET模式:當epoll_wait檢測到描述符事件發生並將此事件通知應用程式,應用程式必須立即處理該事件。如果不處理,下次呼叫epoll_wait時,不會再次響應應用程式並通知此事件。

ET模式在很大程度上減少了epoll事件被重複觸發的次數,因此效率要比LT模式高。epoll工作在ET模式的時候,必須使用非阻塞套介面,以避免由於一個檔案控制程式碼的阻塞讀/阻塞寫操作把處理多個檔案描述符的任務餓死。

參考資料

Linux IO模式及 select、poll、epoll詳解
IO多路複用之select總結
IO多路複用之poll總結
IO多路複用之epoll總結

相關文章