IO複用: select 和poll 到epoll

linux 提供了select、poll和epoll三種接口來實現多路IO複用。下面總結下這三種接口。html

select

該函數容許進程指示內核等待多個事件中的任何一個發生,並只在有一個或多個事件發生或經歷一段指定的時間後才喚醒它。linux

函數接口:數組

   1: #include <sys/select.h>
   2: #include <sys/time.h>
   3:  
   4: int select(int maxfdp1, fd_set *readset, fd_set *writeset, fd_set *exceptset, 
   5:            const struct timeval* timeout);

參數說明:安全

  1. maxfdp1 : 指定待測試的描述符個數,它的值爲待測試的最大描述符加1,注意爲待測試的最大描述符加1,即從0到maxfdp1均將被測試。
  2. readset:  既是輸入參數,也是輸出參數,輸入參數時指明讀事件關心的描述符集合,輸出參數保存準備好讀的描述符集合。
  3. writeset:   既是輸入參數,也是輸出參數,輸入參數時指明寫事件關心的描述符集合,輸出參數保存準備好寫的描述符集合。
  4. exceptset: 既是輸入參數,也是輸出參數,輸入參數時指明異常條件關心的描述符集合,輸出參數保存有異常發生的描述符集合。
  5. timeout: 等待的時間, 結構爲
       1: struct timeval {
       2:   long tv_set; // 秒
       3:   long tv_usec;  // 微秒 10^-6
       4: }

    該參數是一個 相對時間,即距離當前的時間,該參數有三種可能:
    (1) 永遠等待下去,僅有在至少一個描述符準備好的IO時才返回,此時該參數值應該爲空指針。
    (2) 等待一段時間,在有至少一個描述符準備好IO時返回,或者達到指定的時間,即timeval指定的秒數和微秒數。
    (3) 不等待: 檢查描述符後當即返回,成爲輪詢,此時timeval的值(其中的秒數和微秒數)都必須爲0

返回值:服務器

     有就緒的描述符,返回值爲就緒的描述符的個數(早期的select若是返回時多個描述符集合的同一位爲1,即某個描述符又可讀有可寫,那麼在函數返回值時只計一次,而如今的版本則修正了該問題,按照事件分開計算),超時返回0, 出錯則返回-1.網絡

select的幾點說明:數據結構

  1. select能夠關心的描述符有上限限制,一般爲1024,該值是因爲fd_set集合的大小是由宏FD_SETSIZE限制的,select用戶態和內核態交互的fd_set的集合大小就被該參數限制,只有修改該參數,才能夠支持更大集合的描述符。而修改該參數就須要從新編譯內核。一般可使用多進程的方式來避免這個問題。(注意是關心的描述符個數限制,而不是maxfdp1)。常見的使用slect的多進程模型是這樣的: 一個進程專門accept,成功後將fd經過unix socket傳遞給子進程處理,父進程能夠根據子進程負載分派。好比能夠用1個父進程+4個子進程,就能夠承載了過4000個的負載。
  2. 注意fd_set*類型的三個參數,是同時做爲輸入參數和輸出參數來使用的,即該參數在返回時被修改了,所以,下次調用select的時候須要從新初始化。其值的讀取和設置須要使用提供的訪問該結構的方法,分別爲:
       1: void FD_ZERO(fd_set *fdset)   //清空集合
       2: void FD_SET(int fd, fd_set *fdset)  // 設置fdset中fd對應的位
       3: void FD_CLR(int fd, fd_set *fdset)  // 清除fdset中fd對應的位
       4: void FD_ISSET(int fd, fd_set* fdset)  // 檢測fdset中fd對應的位是否設置。
  3. 我的理解: select的工做方式是對maxfdp1之內的描述符都檢測,而後內核逐個檢測這些描述符,根據fd_set集合中設置的關心的描述符,將對應描述符的狀態設置到fd_set中。返回給用戶。而一般尤爲在網絡IO中,大部分描述符是不活躍的,而當描述符集合增大是,該輪詢式的開銷是線性增加的,會致使開銷愈來愈大。

poll

poll起源於SVR3, 最初侷限於流設備,SVR4取消了這種限制,容許poll工做在任何描述符上。它提供了和select相似的功能。併發

函數接口:app

   1: #include <poll.h>
   2:  
   3: int poll(struct pollfd* fdarray, unsigned long nfds, int timeout);
   4:  
   5: struct pollfd {
   6:   int fd;  // 須要檢測的文件描述符
   7:   short event; // fd上關心的事件
   8:   short revent;  // fd上發生的時間,即返回值
   9: };

參數說明:less

  1. pollfd:該參數是一個結構體,結構體中包含文件描述符,和文件關心的事件,以及用來保存返回事件的revent。該參數既是輸入參數又是輸出參數。
  2. nfds: 該參數指明數組中元素的個數。Unix98 爲該參數定義了名爲nfds_t的新的數據類型。
  3. timeout: poll函數返回前的等待時間,單位是毫秒數。 可能取值以下:

    timeout的值 說明
    INFTIM(常被定義爲一個負值) 永遠等待
    0 當即返回,不阻塞進程
    >0 等待指定數目的毫秒數

返回值:

    發生錯誤的時候,返回-1, 定時器到時以前沒有任何描述符就緒,返回0. 不然返回就緒描述符的個數,即revents成員非0的描述符個數。

poll函數的幾點說明:

  1. poll使用單獨的參數nfds表示關心的描述符的個數,所以再也不有select的數量限制,分配一個pollfd結構的數組並把該數組中元素的數目通知內核成了調用者的責任,內核再也不須要知道相似fd_set的固定大小的數據類型。
  2. pollfd中關心的事件和返回的事件分別由events和revents兩個參數表示。所以再也不須要每次都從新設置。而若是咱們再也不關心某個描述符,能夠把與它對應的pollfd結構中的fd成員設置成一個負值。poll函數將忽略這樣的pollfd結構的events成員,返回時將它的revents成員的值置爲0。
  3. poll每次都會對數組中的描述符所有輪詢的檢測一遍,這點仍是和select相似的。所以還存在當描述符數量較大時的開銷問題。

poll的原理:

poll是一個系統調用,其內核入口函數爲sys_poll,sys_poll幾乎不作任何處理直接調用do_sys_poll,do_sys_poll的執行過程能夠分爲三個部分:
       1,將用戶傳入的pollfd數組拷貝到內核空間,由於拷貝操做和數組長度相關,時間上這是一個O(n)操做,這一步的代碼在do_sys_poll中包括從函數開始到調用do_poll前的部分。
       2,查詢每一個文件描述符對應設備的狀態,若是該設備還沒有就緒,則在該設備的等待隊列中加入一項並繼續查詢下一設備的狀態。查詢完全部設備後若是沒有一個設備就緒,這時則須要掛起當前進程等待,直到設備就緒或者超時,掛起操做是經過調用schedule_timeout執行的。設備就緒後進程被通知繼續運行,這時再次遍歷全部設備,以查找就緒設備。這一步由於兩次遍歷全部設備,時間複雜度也是O(n),這裏面不包括等待時間。相關代碼在do_poll函數中。
       3,將得到的數據傳送到用戶空間並執行釋放內存和剝離等待隊列等善後工做,向用戶空間拷貝數據與剝離等待隊列等操做的的時間複雜度一樣是O(n),具體代碼包括do_sys_poll函數中調用do_poll後到結束的部分。

epoll

epoll是linux下多路IO複用select/poll的加強版本,它能顯著提供程序在大量併發鏈接只有少許活躍狀況下的系統CPU利用率。

函數結構:

它有三個主要結構,分別爲epoll_create,epoll_ctl,epoll_wait。

epoll_create

   1: #include <sys/epoll.h>
   2:  
   3: int epoll_create(int size)

該函數用於建立一個epoll的文件描述符,

參數說明:

    建立一個epoll的句柄,size用來告訴內核這個監聽的數目一共有多大(能夠不是監聽的總數,該參數用來提示內存分配空間的)。這個參數不一樣於select()中的第一個參數,給出最大監聽的fd+1的值。須要注意的是,當建立好epoll句柄後,它就是會佔用一個fd值,在linux下若是查看/proc/進程id/fd/,是可以看到這個fd的,因此在使用完epoll後,必須調用close()關閉,不然可能致使fd被耗盡。

返回值:

   成功的返回文件描述符,出錯返回-1,並設置errno

  錯誤類型:

    EINVAL: size不是正數

    ENFILE: 文件描述符達到系統的文件描述符限制

    ENOMEM: 沒有足夠的內存建立內核對象。

epoll_ctl

   1: #include <sys/epoll.h>
   2:  
   3: int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
   4:  
   5: typedef union epoll_data {
   6:   void * ptr;
   7:   int fd;
   8:   _uint32_t u32;
   9:   _uint64_t u64;
  10: }epoll_data_t;
  11:  
  12: struct epoll_event {
  13:   _uint32 events;  // epoll事件
  14:   epoll_data_t data;  // 用戶變量。
  15: };

函數說明:

    epoll的事件註冊函數,它不一樣與select()是在監聽事件時告訴內核要監聽什麼類型的事件,而是在這裏先註冊要監聽的事件類型。第一個參數是epoll_create()的返回值,第二個參數表示動做,用三個宏來表示:
EPOLL_CTL_ADD:註冊新的fd到epfd中;
EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
EPOLL_CTL_DEL:從epfd中刪除一個fd;

   events能夠是如下幾個宏的集合:
EPOLLIN :表示對應的文件描述符能夠讀(包括對端SOCKET正常關閉);
EPOLLOUT:表示對應的文件描述符能夠寫;
EPOLLPRI:表示對應的文件描述符有緊急的數據可讀(這裏應該表示有帶外數據到來);
EPOLLERR:表示對應的文件描述符發生錯誤;
EPOLLHUP:表示對應的文件描述符被掛斷;
EPOLLET: 將EPOLL設爲邊緣觸發(Edge Triggered)模式,這是相對於水平觸發(Level Triggered)來講的。
EPOLLONESHOT:只監聽一次事件,當監聽完此次事件以後,若是還須要繼續監聽這個socket的話,須要再次把這個socket加入到EPOLL隊列裏

返回值:

    成功返回0, 失敗返回-1, 並設置errno

   錯誤類型:

     EBADF: epfd不是一個有效的描述符。

      EEXIST: op爲EPOLL_CTL_ADD,而且提供的描述符fd已經在epfd中。此時應該用EPOLL_CTL_MOD

      EINVAL: epfd不是一個epoll文件描述符,或者fd和epfd同樣,揮着請求的操做op不是一個有效的操做。

      ENOENT: op是EPOLL_CTL_MOD或者EPOLL_CTL_DEL,而且fd不在epfd中。

     ENOMEM: 沒有足夠的內存區執行相應的op操做。

     EPERM: epoll不支持目標的文件描述符fd

      

epoll_wait

   1: #include <sys/epoll.h>
   2:  
   3: int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);

   函數說明:

     epfd是經過epoll_create常見的描述符,events是接受返回事件的結構,maxevents是數組的最大值,timeout是設置超時時間的,單位是毫秒。

    等待事件的產生,相似於select()調用。參數events用來從內核獲得事件的集合,maxevents告以內核這個events有多大,這個 maxevents的值必須大於0,maxevents 是epoll_wait能夠處理的鏈接事件的最大限度值,這個值通常要小於或等於epoll_create的那個size,固然若是設置成比size還大 的話也無所謂,size是epoll總體能夠監聽的最大fd數量。maxevents的意義是防止epoll的API在填寫你傳進去的指針events的 時候,超過指針指向的內存的大小從而致使內存溢出。參數timeout是超時時間(毫秒,0會當即返回,-1將不肯定,也有說法說是永久阻塞)。該函數返回須要處理的事件數目,如返回0表示已超時。

當成功返回時,每一個epoll_event結構中將包含epoll_ct中的用戶數據。

返回值:

  成功返回準備好的描述符數,超時仍沒有描述符準備好返回0, 出錯返回-1,並設置errno

  錯誤類型:

  EBADF: epfd不是一個有效的文件描述符

   EFAULT: 指向events的內存沒有寫權限

   EINTR: 調用在IO 準備好和超時以前被信號中斷

   EINVAL: epfd不是有個epoll文件描述符,或者maxevents小於等於0

 

epoll的幾點說明:

  1. epoll一樣只告知那些就緒的文件描述符,並且當咱們調用epoll_wait()得到就緒文件描述符時,返回的不是實際的描述符,而是一個表明就緒描述符數量的值,你只須要去epoll指定的一個數組中依次取得相應數量的文件描述符便可,這裏也使用了內存映射(mmap)技術,這樣便完全省掉了這些文件描述符在系統調用時複製的開銷。
  2. 另外一個本質的改進在於epoll採用基於事件的就緒通知方式。在select/poll中,進程只有在調用必定的方法後,內核纔對全部監視的文件描述符進行掃描,而epoll事先經過epoll_ctl()來註冊一個文件描述符,一旦基於某個文件描述符就緒時,內核會採用相似callback的回調機制,迅速激活這個文件描述符,當進程調用epoll_wait()時便獲得通知。

epoll的ET和LT

   ET模式僅當狀態發生變化的時候纔得到通知(只告訴進程哪些文件描述符剛剛變爲就緒狀態,),這裏所謂的狀態的變化並不包括緩衝區中還有未處理的數據,也就是說,若是要採用ET模式,須要一直read/write直到出錯爲止,不少人反映爲何採用ET模式只接收了一部分數據就再也得不到通知了,大多由於這樣;而LT模式是隻要有數據沒有處理就會一直通知下去的.

 

EPOLL 和poll、select的區別

接下來分析epoll,與poll/select不一樣,epoll再也不是一個單獨的系統調用,而是由epoll_create/epoll_ctl/epoll_wait三個系統調用組成,後面將會看到這樣作的好處。
       先來看sys_epoll_create(epoll_create對應的內核函數),這個函數主要是作一些準備工做,好比建立數據結構,初始化數據並最終返回一個文件描述符(表示新建立的虛擬epoll文件),這個操做能夠認爲是一個固定時間的操做。
        epoll是作爲一個虛擬文件系統來實現的,這樣作至少有如下兩個好處:
        1,能夠在內核裏維護一些信息,這些信息在屢次epoll_wait間是保持的,好比全部受監控的文件描述符。
        2, epoll自己也能夠被poll/epoll;
       具體epoll的虛擬文件系統的實現和性能分析無關,再也不贅述。
       在sys_epoll_create中還能看到一個細節,就是epoll_create的參數size在現階段是沒有意義的,只要大於零就行。
       接着是sys_epoll_ctl(epoll_ctl對應的內核函數),須要明確的是每次調用sys_epoll_ctl只處理一個文件描述符,這裏主要描述當op爲EPOLL_CTL_ADD時的執行過程,sys_epoll_ctl作一些安全性檢查後進入ep_insert,ep_insert裏將 ep_poll_callback作爲回掉函數加入設備的等待隊列(假定這時設備還沒有就緒),因爲每次poll_ctl只操做一個文件描述符,所以也能夠認爲這是一個O(1)操做
        ep_poll_callback函數很關鍵,它在所等待的設備就緒後被系統回掉,執行兩個操做:
       1,將就緒設備加入就緒隊列,這一步避免了像poll那樣在設備就緒後再次輪詢全部設備找就緒者,下降了時間複雜度,由O(n)到O(1);
       2,喚醒虛擬的epoll文件;
       最後是sys_epoll_wait,這裏實際執行操做的是ep_poll函數。該函數等待將進程自身插入虛擬epoll文件的等待隊列,直到被喚醒(見上面ep_poll_callback函數描述),最後執行ep_events_transfer將結果拷貝到用戶空間。因爲只拷貝就緒設備信息,因此這裏的拷貝是一個O(1)操做。
       還有一個讓人關心的問題就是epoll對EPOLLET的處理,即邊沿觸發的處理,粗略看代碼就是把一部分水平觸發模式下內核作的工做交給用戶來處理,直覺上不會對性能有太大影響,感興趣的朋友歡迎討論。
POLL/EPOLL對比:
       表面上poll的過程能夠看做是由一次epoll_create/若干次epoll_ctl/一次epoll_wait/一次close等系統調用構成,實際上epoll將poll分紅若干部分實現的緣由正是由於服務器軟件中使用poll的特色(好比Web服務器):
       1,須要同時poll大量文件描述符;
       2,每次poll完成後就緒的文件描述符只佔全部被poll的描述符的不多一部分。
       3,先後屢次poll調用對文件描述符數組(ufds)的修改只是很小;
       傳統的poll函數至關於每次調用都重起爐竈,從用戶空間完整讀入ufds,完成後再次徹底拷貝到用戶空間,另外每次poll都須要對全部設備作至少作一次加入和刪除等待隊列操做,這些都是低效的緣由。
        epoll將以上狀況都細化考慮,不須要每次都完整讀入輸出ufds,只需使用epoll_ctl調整其中一小部分,不須要每次epoll_wait都執行一次加入刪除等待隊列操做,另外改進後的機制使的沒必要在某個設備就緒後搜索整個設備數組進行查找,這些都能提升效率。另外最明顯的一點,從用戶的使用來講,使用epoll沒必要每次都輪詢全部返回結果已找出其中的就緒部分,O(n)變O(1),對性能也提升很多。
       此外這裏還發現一點,是否是將epoll_ctl改爲一次能夠處理多個fd(像semctl那樣)會提升些許性能呢?特別是在假設系統調用比較耗時的基礎上。不過關於系統調用的耗時問題還會在之後分析。

 

refer:http://kaiyuan.blog.51cto.com/930309/341121

         http://www.360doc.com/content/09/0727/15/1894_4486873.shtml

         http://blog.csdn.net/ljx0305/article/details/4065058

         http://blog.endlesscode.com/2010/03/27/select-poll-epoll-intro/

相關文章
相關標籤/搜索