I/O模型系列之五:IO多路複用 select、poll、epoll

IO多路複用之select、poll、epoll

  IO多路複用:經過一種機制,一個進程能夠監視多個描述符,一旦某個描述符就緒(通常是讀就緒或者寫就緒),可以通知程序進行相應的讀寫操做。
html

  應用:適用於針對大量的io請求的狀況,對於服務器必須在同時處理來自客戶端的大量的io操做的時候,就很是適合java

  與多進程和多線程技術相比,I/O多路複用技術的最大優點就是系統開銷小,系統沒必要建立進程/線程,也沒必要維護這些進程/線程,從而大大減少了系統的開銷。linux

      目前支持I/O多路複用的系統調用有select, pselect, poll, epoll, 但他們 本質上都是同步I/O,由於他們都須要在讀寫事件就緒後本身負責進行讀寫,也就是說這個讀寫過程是 阻塞的,而異步I/O則無需本身負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。
  select, pselect, poll, epoll 都是屬於IO設計模式Reactor的IO策略。

1、IO多路複用使用場景

IO多路複用是指內核一旦發現進程指定的一個或者多個IO條件準備讀取,它就通知該進程。IO多路複用適用以下場合:
  1)當客戶處理多個描述符時(通常是交互式輸入和網絡套接口),必須使用I/O複用。
  2)當一個客戶同時處理多個套接口時,這種狀況是可能的,但不多出現。
  3)若是一個TCP服務器既要處理監聽套接口,又要處理已鏈接套接口,通常也要用到I/O複用。
  4)若是一個服務器即要處理TCP,又要處理UDP,通常要使用I/O複用。
  5)若是一個服務器要處理多個服務或多個協議,通常要使用I/O複用。

2、select

2.1 select基本原理

  select 函數監視的文件描述符分3類,分別是writefds、readfds、和exceptfds。調用後select函數會阻塞,直到有描述符就緒(有數據 可讀、可寫、或者有except),或者超時(timeout指定等待時間,若是當即返回設爲null便可),函數返回。當select函數返回後,能夠經過遍歷fdset,來找到就緒的描述符。

2.2 select基本流程

  

2.3 select函數原型

該函數准許進程指示內核等待多個事件中的任何一個發送,並只在有一個或多個事件發生或經歷一段指定的時間後才喚醒本身。函數原型以下:編程

#include <sys/select.h>
#include <sys/time.h>

int select(int maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,const struct timeval *timeout)
返回值:就緒描述符的數目,超時返回0,出錯返回-1
函數參數介紹以下:

(1)第一個參數maxfdp1指定待測試的描述字個數,它的值是待測試的最大描述字加1(所以把該參數命名爲maxfdp1).

    描述字0、12...(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); // 檢查集合中指定的文件描述符是否能夠讀寫 3timeout指定等待的時間,告知內核等待所指定描述字中的任何一個就緒可花多少時間。其timeval結構用於指定這段時間的秒數和微秒數。 struct timeval{ long tv_sec; //seconds long tv_usec; //microseconds };     這個參數有三種可能:     (1永遠等待下去:僅在有一個描述字準備好I/O時才返回。爲此,把該參數設置爲空指針NULL。     (2等待一段固定時間:在有一個描述字準備好I/O時返回,可是不超過由該參數所指向的timeval結構中指定的秒數和微秒數。     (3根本不等待:檢查描述字後當即返回,這稱爲輪詢。爲此,該參數必須指向一個timeval結構,並且其中的定時器值必須爲0

2.4 select優勢

  1. 跨平臺。(幾乎全部的平臺都支持)設計模式

  2. 時間精度高。(ns級別)數組

2.5 select缺點

  1. 最大限制:單個進程可以監視的文件描述符的數量存在最大限制。(基於數組存儲的趕腳)
緩存

    通常來講這個數目和系統內存關係很大,具體數目能夠cat /proc/sys/fs/file-max察看。它由FD_SETSIZE設置,32位機默認是1024個。64位機默認是2048.服務器

  2.時間複雜度: 對socket進行掃描時是線性掃描,即採用輪詢的方法,效率較低,時間複雜度O(n)網絡

   當套接字比較多的時候,每次select()都要經過遍歷FD_SETSIZE個Socket來完成調度,無論哪一個Socket是活躍的,都遍歷一遍。這會浪費不少CPU時間。
  它僅僅知道有I/O事件發生了,卻並不知道是哪那幾個流(可能有一個,多個,甚至所有),咱們只能無差異輪詢全部流,找出能讀出數據,或者寫入數據的流,對他們進行操做。因此 select具備O(n)的無差異輪詢複雜度,同時處理的流越多,無差異輪詢時間就越長。
  3.  內存拷貝: 須要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時複製開銷大
 

3、poll

  改進了select最大數量限制。數據結構

3.1 poll基本原理

   poll本質上和select沒有區別,它將用戶傳入的數組拷貝到內核空間,而後查詢每一個fd對應的設備狀態,若是設備就緒則在設備等待隊列中加入一項並繼續遍歷,若是遍歷完全部fd後沒有發現就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒後它又要再次遍歷fd。這個過程經歷了屢次無謂的遍歷。

3.2 poll基本流程

  相似select

3.3 poll函數原型

函數格式以下所示:

# include <poll.h>
# include <arpa/inet.h>
int
poll ( struct pollfd * fds, unsigned int nfds, int timeout);
1)pollfd結構體定義以下:     struct pollfd {       int fd;    /* 文件描述符 */       short events; /* 等待的事件 */       short revents; /* 實際發生了的事件 */     } ;  每個pollfd結構體指定了一個被監視的文件描述符。所以能夠傳遞多個結構體,指示poll()監視多個文件描述符。

(2)events域是監視該文件描述符的事件掩碼,由用戶來設置這個域。
    POLLIN         有數據可讀。     POLLRDNORM      有普通數據可讀。     POLLRDBAND      有優先數據可讀。     POLLPRI        有緊迫數據可讀。     POLLOUT        寫數據不會致使阻塞。     POLLWRNORM      寫普通數據不會致使阻塞。     POLLWRBAND      寫優先數據不會致使阻塞。     POLLMSGSIGPOLL    消息可用。
(3)revents域是文件描述符的操做結果事件掩碼,內核在調用返回時設置這個域。events域中請求的任何事件均可能在revents域中返回。
   此外,revents域中還可能返回下列事件:   
    POLLER   指定的文件描述符發生錯誤。
    POLLHUP   指定的文件描述符掛起事件。
    POLLNVAL  指定的文件描述符非法。
   這些事件在events域中無心義,由於它們在合適的時候老是會從revents中返回。   
(4)舉個栗子:要同時監視一個文件描述符是否可讀和可寫,
    咱們能夠設置 events 爲POLLIN |POLLOUT。
    在poll返回時,咱們能夠檢查revents中的標誌,對應於文件描述符請求的events結構體。
    若是POLLIN事件被設置,則文件描述符能夠被讀取而不阻塞。
    若是POLLOUT被設置,則文件描述符能夠寫入而不致使阻塞。
    這些標誌並非互斥的:它們可能被同時設置,表示這個文件描述符的讀取和寫入操做都會正常返回而不阻塞。
  
(5)nfds參數是數組fds元素的個數
(6)timeout參數指定等待的毫秒數,不管I
/O是否準備好,poll都會返回。
    timeout指定爲負數值表示無限超時,使poll()一直掛起直到一個指定事件發生;
    timeout爲0指示poll調用當即返回並列出準備好I/O的文件描述符,但並不等待其它的事件。
 
(7)返回值和錯誤代碼   
  成功時,poll()返回結構體中revents域不爲0的文件描述符個數;
  若是在超時前沒有任何事件發生,poll()返回0;
  失敗時,poll()返回
-1
    並設置errno爲下列值之一:   
    EBADF   一個或多個結構體中指定的文件描述符無效。   
    EFAULTfds   指針指向的地址超出進程的地址空間。   
    EINTR     請求的事件以前產生一個信號,調用能夠從新發起。   
    EINVALnfds  參數超出PLIMIT_NOFILE值。   
    ENOMEM   可用內存不足,沒法完成請求。

3.4 poll優勢

  1. 沒有最大鏈接數的限制。(基於鏈表來存儲的)

3.5 poll缺點

  1. 時間複雜度: 對socket進行掃描時是線性掃描,即採用輪詢的方法,效率較低,時間複雜度O(n)。

  它將用戶傳入的數組拷貝到內核空間,而後查詢每一個fd對應的設備狀態,若是設備就緒則在設備等待隊列中加入一項並繼續遍歷,若是遍歷完全部fd後沒有發現就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒後它又要再次遍歷fd。這個過程經歷了屢次無謂的遍歷。
  
  2.  內存拷貝:大量的fd數組被總體複製於用戶態和內核地址空間之間,而無論這樣的複製是否是有意義。
    大量的fd數組被總體複製於用戶態和內核地址空間之間,而無論這樣的複製是否是有意義。
  
  3.  水平觸發:若是報告了fd後,沒有被處理,那麼下次poll時會再次報告該fd。
 
注意:select和poll都須要在返回後,經過遍歷文件描述符來獲取已經就緒的socket。
      事實上,同時鏈接的大量客戶端在一時刻可能只有不多的處於就緒狀態,所以隨着監視的描述符數量的增加,其效率也會線性降低。

4、epoll

  epoll是在2.6內核中提出的,是以前的select和poll的加強版本。是爲處理大批量句柄而做了改進的poll。

  epoll使用一個文件描述符管理多個描述符,將用戶關係的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的拷貝只須要一次。

4.1 epoll基本原理

  epoll有兩大特色:

    1. 邊緣觸發,它只告訴進程哪些fd剛剛變爲就緒態,而且只會通知一次。

    2. 事件驅動,每一個事件關聯上fd,使用事件就緒通知方式,經過 epoll_ctl 註冊 fd,一旦該fd就緒,內核就會採用 callback 的回調機制來激活該fd,epoll_wait 即可以收到通知。

4.2 epoll基本流程

 一棵紅黑樹,一張準備就緒句柄鏈表,少許的內核cache,就幫咱們解決了大併發下的socket處理問題。

 1. 執行 epoll_create
     內核在epoll文件系統中建了個file結點,(使用完,必須調用close()關閉,不然致使fd被耗盡)
       在內核cache裏建了紅黑樹存儲epoll_ctl傳來的socket,
       在內核cache裏建了rdllist雙向鏈表存儲準備就緒的事件。
 2. 執行 epoll_ctl
    若是增長socket句柄,檢查紅黑樹中是否存在,存在當即返回,不存在則添加到樹幹上,而後向內核註冊回調函數,告訴內核若是這個句柄的中斷到了,就把它放到準備就緒list鏈表裏。

    ps:全部添加到epoll中的事件都會與設備(如網卡)驅動程序簡歷回調關係,相應的事件發生時,會調用回調方法。

 3. 執行 epoll_wait

    馬上返回準備就緒表裏的數據便可(將內核cache裏雙向列表中存儲的準備就緒的事件  複製到用戶態內存)

    當調用epoll_wait檢查是否有事件發生時,只須要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素便可。

    若是rdlist不爲空,則把發生的事件複製到用戶態,同時將事件數量返回給用戶。    

  

  

  

  

4.3 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);

1int epoll_create(int size);
  /*建立一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大。*/
  這個參數不一樣於select()中的第一個參數,給出最大監聽的fd
+1的值。
須要注意的是:
    當建立好epoll句柄後,它就是會佔用一個fd值,在linux下若是查看/proc/進程id/fd/,是可以看到這個fd的,
    因此在使用完epoll後,必須調用close()關閉,不然可能致使fd被耗盡。 (
2int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);   epoll的事件註冊函數.
  它不一樣與select()是在監聽事件時告訴內核要監聽什麼類型的事件epoll的事件註冊函數,而是在這裏先註冊要監聽的事件類型。
  第一個參數
epfd 是epoll_create()的返回值,
  第二個參數 op 表示動做,用三個宏來表示:
    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隊列裏
 (
3int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);   
    等待事件的產生
    
相似於select()調用。
    參數 events用來從內核獲得事件的集合,
    參數 maxevents告以內核這個events有多大,這個maxevents的值不能大於建立epoll_create()時的size,
    參數 timeout是超時時間(毫秒,0會當即返回,
-1將不肯定,也有說法說是永久阻塞)。
    該函數返回須要處理的事件數目,如返回0表示已超時。

4.4 epoll優勢

  1. 沒有最大鏈接數的限制。(基於 紅黑樹+雙鏈表 來存儲的:1G的內存上能監聽約10萬個端口)

  2. 時間複雜度低: 邊緣觸發和事件驅動,監聽回調,時間複雜度O(1)。

    只有活躍可用的fd纔會調用callback函數;即epoll最大的優勢就在於它只管「活躍」的鏈接,而跟鏈接總數無關,所以實際網絡環境中,Epoll的效率就會遠遠高於select和poll。

  3. 內存拷貝:利用mmap()文件映射內存加速與內核空間的消息傳遞,減小拷貝開銷。

4.5 epoll缺點

  1. 依賴於操做系統:Lunix

4.6 epoll應用場景

適合用epoll的應用場景:

  對於鏈接特別多,活躍的鏈接特別少

  典型的應用場景爲一個須要處理上萬的鏈接服務器,例如各類app的入口服務器,例如qq

不適合epoll的場景:

  鏈接比較少,數據量比較大,例如ssh

epoll 的驚羣問題:

  由於epoll 多用於多個鏈接,只有少數活躍的場景,可是萬一某一時刻,epoll 等的上千個文件描述符都就緒了,這時候epoll 要進行大量的I/O,此時壓力太大。

4.7 epoll兩種模式

epoll對文件描述符的操做有兩種模式:LT(level trigger) 和 ET(edge trigger)。LT是默認的模式,ET是「高速」模式。

  LT(水平觸發)模式下,只要有數據就觸發,緩衝區剩餘未讀盡的數據會致使 epoll_wait都會返回它的事件;

  ET(邊緣觸發)模式下,只有新數據到來才觸發,無論緩存區中是否還有數據,緩衝區剩餘未讀盡的數據不會致使epoll_wait返回

一、LT模式
  LT(level triggered)是缺省的工做方式,而且同時支持block和no-block socket。在這種作法中,內核告訴你一個文件描述符是否就緒了,而後你能夠對這個就緒的fd進行IO操做。 若是你不做任何操做,內核仍是會繼續通知你的,只要這個文件描述符還有數據可讀,每次 epoll_wait都會返回它的事件,提醒用戶程序去操做
 
 
二、ET模式
  ET(edge-triggered)是高速工做方式,只支持no-block socket。在這種模式下,當描述符從未就緒變爲就緒時,內核經過epoll告訴你。而後它會假設你知道文件描述符已經就緒,而且不會再爲那個文件描述符發送更多的就緒通知,直到你作了某些操做致使那個文件描述符再也不爲就緒狀態了(好比,你在發送,接收或者接收請求,或者發送接收的數據少於必定量時致使了一個EWOULDBLOCK 錯誤)。 可是請注意,若是一直不對這個fd做IO操做(從而致使它再次變成未就緒),內核不會發送更多的通知(only once)
  在它檢測到有 I/O 事件時,經過 epoll_wait 調用會獲得有事件通知的文件描述符,對於每個被通知的文件描述符,如可讀,則必須將該文件描述符一直讀到空,讓 errno 返回 EAGAIN (提示你的應用程序如今沒有數據可讀請稍後再試)爲止,不然下次的 epoll_wait 不會返回餘下的數據,會丟掉事件。
  ET模式在很大程度上減小了epoll事件被重複觸發的次數,所以效率要比LT模式高。epoll工做在ET模式的時候, 必須使用非阻塞套接口,以免因爲一個文件句柄的阻塞讀/阻塞寫操做把處理多個文件描述符的任務餓死。
 
注意:1.  在select/poll中, 進程只有在調用必定的方法後,內核纔對全部監視的文件描述符進行掃描,而epoll事先經過epoll_ctl()來註冊一個文件描述符, 一旦基於某個文件描述符就緒時,內核會採用相似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便獲得通知。 此處去掉了遍歷文件描述符,而是經過監聽回調的的機制。這正是epoll的魅力所在。
   2. 若是沒有大量的idle-connection或者dead-connection,epoll的效率並不會比select/poll高不少,可是當遇到大量的idle-connection,就會發現epoll的效率大大高於select/poll。

5、select、poll、epoll區別

它們三個都是  就緒設備 通知 。

一、支持一個進程所能打開的最大鏈接數

select

單個進程所能打開的最大鏈接數有FD_SETSIZE宏定義,其大小是32個整數的大小(在32位的機器上,大小就是32*32,同理64位機器上FD_SETSIZE爲32*64),固然咱們能夠對進行修改,而後從新編譯內核,可是性能可能會受到影響,這須要進一步的測試。

poll

poll本質上和select沒有區別,可是它沒有最大鏈接數的限制,緣由是它是基於鏈表來存儲的

epoll

雖然鏈接數有上限,可是很大,1G內存的機器上能夠打開10萬左右的鏈接,2G內存的機器能夠打開20萬左右的鏈接

二、FD劇增後帶來的IO效率問題

select

由於每次調用時都會對鏈接進行線性遍歷,因此隨着FD的增長會形成遍歷速度慢的「線性降低性能問題」。

poll

同上

epoll

由於epoll內核中實現是根據每一個fd上的callback函數來實現的,只有活躍的socket纔會主動調用callback,因此在活躍socket較少的狀況下,使用epoll沒有前面二者的線性降低的性能問題,可是全部socket都很活躍的狀況下,可能會有性能問題。

三、 消息傳遞方式

select

內核須要將消息傳遞到用戶空間,都須要內核拷貝動做

poll

同上

epoll

epoll經過mmap把對應設備文件片段映射到用戶空間上, 消息傳遞不經過內核, 內存與設備文件同步數據.

總結:

綜上,在選擇select,poll,epoll時要根據具體的使用場合以及這三種方式的自身特色。

一、表面上看epoll的性能最好,可是在鏈接數少而且鏈接都十分活躍的狀況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制須要不少函數回調。

二、select低效是由於每次它都須要輪詢。但低效也是相對的,視狀況而定,也可經過良好的設計改善

 

相關文章
相關標籤/搜索