IO多路復用之select、poll、epoll


本文轉載自IO多路復用之select、poll、epoll

導語

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

應用:適用於針對大量的io請求的情況,對於服務器必須在同時處理來自客戶端的大量的io操作的時候,就非常適合

與多進程和多線程技術相比,I/O多路復用技術的最大優勢就是系統開銷小,系統不必創建進程/線程,也不必維護這些進程/線程,從而大大減小了系統的開銷。

目前支持I/O多路復用的系統調用有select, pselect, poll, epoll, 但他們本質上都是同步I/O,因為他們都需要在讀寫事件就緒后自己負責進行讀寫,也就是說這個讀寫過程是阻塞的,而異步I/O則無需自己負責進行讀寫,異步I/O的實現會負責把數據從內核拷貝到用戶空間。

select, pselect, poll, epoll 都是屬於IO設計模式Reactor的IO策略。

IO多路復用使用場景

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

  • 當客戶處理多個描述符時(一般是交互式輸入和網絡套接口),必須使用I/O復用。
  • 當一個客戶同時處理多個套接口時,這種情況是可能的,但很少出現。
  • 如果一個TCP服務器既要處理監聽套接口,又要處理已連接套接口,一般也要用到I/O復用。
  • 如果一個服務器即要處理TCP,又要處理UDP,一般要使用I/O復用。
  • 如果一個服務器要處理多個服務或多個協議,一般要使用I/O復用。

select

select基本原理

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

select基本流程

img

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

select優點

  1. 跨平台。(幾乎所有的平台都支持)
  2. 時間精度高。(ns級別)

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的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時復制開銷大。

poll

改進了select最大數量限制。

poll基本原理

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

poll基本流程

類似select

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   可用內存不足,無法完成請求。 

poll優點

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

poll缺點

  1. 時間復雜度: 對socket進行掃描時是線性掃描,即采用輪詢的方法,效率較低,時間復雜度O(n)。
      它將用戶傳入的數組拷貝到內核空間,然后查詢每個fd對應的設備狀態,如果設備就緒則在設備等待隊列中加入一項並繼續遍歷,如果遍歷完所有fd后沒有發現就緒設備,則掛起當前進程,直到設備就緒或者主動超時,被喚醒后它又要再次遍歷fd。這個過程經歷了多次無謂的遍歷。
  2. 內存拷貝:大量的fd數組被整體復制於用戶態和內核地址空間之間,而不管這樣的復制是不是有意義。
    大量的fd數組被整體復制於用戶態和內核地址空間之間,而不管這樣的復制是不是有意義。
  3. 水平觸發:如果報告了fd后,沒有被處理,那么下次poll時會再次報告該fd。

注意:select和poll都需要在返回后,通過遍歷文件描述符來獲取已經就緒的socket。
事實上,同時連接的大量客戶端在一時刻可能只有很少的處於就緒狀態,因此隨着監視的描述符數量的增長,其效率也會線性下降。

epoll

epoll是在2.6內核中提出的,是之前的select和poll的增強版本。是為處理大批量句柄而作了改進的poll。
epoll使用一個文件描述符管理多個描述符,將用戶關系的文件描述符的事件存放到內核的一個事件表中,這樣在用戶空間和內核空間的拷貝只需要一次。``

epoll基本原理

epoll有兩大特點:

  1. 邊緣觸發,它只告訴進程哪些fd剛剛變為就緒態,並且只會通知一次。
  2. 事件驅動,每個事件關聯上fd,使用事件就緒通知方式,通過 epoll_ctl 注冊 fd,一旦該fd就緒,內核就會采用 callback 的回調機制來激活該fd,epoll_wait 便可以收到通知。

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不為空,則把發生的事件復制到用戶態,同時將事件數量返回給用戶。

img
img
img
img

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);
int epoll_create(int size);   /*創建一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大。*/  

這個參數不同於select()中的第一個參數,給出最大監聽的fd+1的值。   需要注意的是:    當創建好epoll句柄后,它就是會占用一個fd值,在linux下如果查看/proc/進程id/fd/,是能夠看到這個fd的,    所以在使用完epoll后,必須調用close()關閉,否則可能導致fd被耗盡。

int 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隊列里 

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表示已超時。

4.4 epoll優點

  1. 沒有最大連接數的限制。(基於 紅黑樹+雙鏈表 來存儲的:1G的內存上能監聽約10萬個端口)
  2. 時間復雜度低: 邊緣觸發和事件驅動,監聽回調,時間復雜度O(1)。
    只有活躍可用的fd才會調用callback函數;即epoll最大的優點就在於它只管“活躍”的連接,而跟連接總數無關,因此實際網絡環境中,Epoll的效率就會遠遠高於select和poll。
  3. 內存拷貝:利用mmap()文件映射內存加速與內核空間的消息傳遞,減少拷貝開銷。

4.5 epoll缺點

依賴於操作系統: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返回。

1、LT模式
  LT(level triggered)是缺省的工作方式,並且同時支持block和no-block socket。在這種做法中,內核告訴你一個文件描述符是否就緒了,然后你可以對這個就緒的fd進行IO操作。如果你不作任何操作,內核還是會繼續通知你的,只要這個文件描述符還有數據可讀,每次 epoll_wait都會返回它的事件,提醒用戶程序去操作
img

2、ET模式
  ET(edge-triggered)是高速工作方式,只支持no-block socket。在這種模式下,當描述符從未就緒變為就緒時,內核通過epoll告訴你。然后它會假設你知道文件描述符已經就緒,並且不會再為那個文件描述符發送更多的就緒通知,直到你做了某些操作導致那個文件描述符不再為就緒狀態了(比如,你在發送,接收或者接收請求,或者發送接收的數據少於一定量時導致了一個EWOULDBLOCK 錯誤)。但是請注意,如果一直不對這個fd作IO操作(從而導致它再次變成未就緒),內核不會發送更多的通知(only once)
在它檢測到有 I/O 事件時,通過 epoll_wait 調用會得到有事件通知的文件描述符,對於每一個被通知的文件描述符,如可讀,則必須將該文件描述符一直讀到空,讓 errno 返回 EAGAIN (提示你的應用程序現在沒有數據可讀請稍后再試)為止,否則下次的 epoll_wait 不會返回余下的數據,會丟掉事件。

![img](https://img2018.cnblogs.com/blog/728328/201903/728328-20190327144034158-1713268245.png)

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

注意:1. 在select/poll中,進程只有在調用一定的方法后,內核才對所有監視的文件描述符進行掃描,而epoll事先通過epoll_ctl()來注冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會采用類似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便得到通知。此處去掉了遍歷文件描述符,而是通過監聽回調的的機制。這正是epoll的魅力所在。

  1. 如果沒有大量的idle-connection或者dead-connection,epoll的效率並不會比select/poll高很多,但是當遇到大量的idle-connection,就會發現epoll的效率大大高於select/poll。

五、select、poll、epoll區別

它們三個都是 就緒設備 通知 。
1、支持一個進程所能打開的最大連接數

select 單個進程所能打開的最大連接數有FD_SETSIZE宏定義,其大小是32個整數的大小(在32位的機器上,大小就是3232,同理64位機器上FD_SETSIZE為3264),當然我們可以對進行修改,然后重新編譯內核,但是性能可能會受到影響,這需要進一步的測試。
poll poll本質上和select沒有區別,但是它沒有最大連接數的限制,原因是它是基於鏈表來存儲的
epoll 雖然連接數有上限,但是很大,1G內存的機器上可以打開10萬左右的連接,2G內存的機器可以打開20萬左右的連接

2、FD劇增后帶來的IO效率問題

select 因為每次調用時都會對連接進行線性遍歷,所以隨着FD的增加會造成遍歷速度慢的“線性下降性能問題”。
poll 同上
epoll 因為epoll內核中實現是根據每個fd上的callback函數來實現的,只有活躍的socket才會主動調用callback,所以在活躍socket較少的情況下,使用epoll沒有前面兩者的線性下降的性能問題,但是所有socket都很活躍的情況下,可能會有性能問題。

3、 消息傳遞方式

select 內核需要將消息傳遞到用戶空間,都需要內核拷貝動作
poll 同上
epoll epoll通過mmap把對應設備文件片斷映射到用戶空間上, 消息傳遞不通過內核, 內存與設備文件同步數據.

總結:
綜上,在選擇select,poll,epoll時要根據具體的使用場合以及這三種方式的自身特點。
1、表面上看epoll的性能最好,但是在連接數少並且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多函數回調。
2、select低效是因為每次它都需要輪詢。但低效也是相對的,視情況而定,也可通過良好的設計改善
img

摘錄地址

聊聊IO多路復用之select、poll、epoll詳解
IO多路復用之select總結
IO多路復用之poll總結
IO多路復用之epoll總結
高並發網絡編程之epoll詳解
epoll原理詳解及epoll反應堆模型
Linux五種IO模型性能分析
LT(水平觸發)和ET(邊緣觸發)
java BIO/NIO/AIO 學習


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM