select,poll,epoll簡介

轉載自:http://blog.163.com/xychenbaihu@yeah/blog/static/132229655201322375435117/

select,poll,epoll簡介:

 

select

 select本質上是通過設置或者檢查存放fd標誌位的數據結構來進行下一步處理。這樣所帶來的缺點是:

1、 單個進程可監視的fd數量被限制,數組有大小限制;

2 、需要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時複製開銷大;

3 、對socket進行掃描時是線性掃描;

4、select也是“水平觸發”,如果報告了fd後,沒有被處理,那麼下次selectl時會再次報告該fd;

 

poll

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

1、它沒有最大連接數的限制,原因是它是基於鏈表來存儲的;

2、和select一樣大量的fd的數組被整體複製於用戶態和內核地址空間之間,而不管這樣的複製是不是有意義。

3、poll也是線性掃描;

4、poll還有一個特點是“水平觸發”,如果報告了fd後,沒有被處理,那麼下次poll時會再次報告該fd。

epoll

 epoll支持水平觸發和邊緣觸發;

1、最大的特點在於邊緣觸發,它只告訴進程哪些fd剛剛變爲就需態,並且只會通知一次。

2、在前面說到的複製問題上,epoll使用mmap減少複製開銷(使用mmap,內核和用戶態共享數據)。

3、還有一個特點是,epoll使用“事件”的就緒通知方式,通過epoll_ctl註冊fd,一旦該fd就緒,內核就會採用類似callback的回調機制來激活該fd,epoll_wait便可以收到通知。

4、雖然也有限制,可以認爲無限大。

select,poll,epoll的比較:

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

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

           epoll管理鏈接數上限:/proc/sys/fs/epoll/max_user_watchs

           根據epoll的實現,在64位環境下,epoll在內核中需要爲每個fd消耗160Bytes。這部分內存可以通過slabtop查看。

其次,根據linux內核2.6.32.43中對tcp協議棧的實現,分析內核中socket相關數據結構的內存開銷。內核爲每個應用層中打開的socket維護structsocket_alloc數據結構,它包含struct socket和struct inode結構,分別對應socket在tcp中的表示和vfs中的inode數據結構。

            在網絡層中,還需要struct sock數據結構來表示socket。

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

 select

 因爲每次調用時都會對連接進行線性遍歷,所以隨着FD的增加會造成遍歷速度慢的“線性下降性能問題”。
 poll  同上

 epoll

 因爲epoll內核中實現是根據每個fd上的callback函數來實現的,只有活躍的socket纔會主動調用callback,所以在活躍socket較少的情況下,使用epoll沒有前面兩者的線性下降的性能問題,但是所有socket都很活躍的情況下,可能會有性能問題。

3、消息傳遞方式

 select  內核需要將消息傳遞到用戶空間,都需要內核拷貝動作。
 poll  同上
 epoll  epoll通過內核和用戶空間通過mmap共享一塊內存來實現的。

 

綜上比較:

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

       從表象看epoll的性能最好,但是在連接數少,並且連接都十分活躍的情況下,select和poll的性能可能比epoll好,畢竟epoll的通知機制需要很多回調函數來完成。

       epoll在事件管理上,使用的是紅黑樹,可以快速的增刪事件和快速查找事件。

 

以上參考博客:

       http://xingyunbaijunwei.blog.163.com/blog/static/76538067201241685556302/

 

select的接口,及解釋:

      select()

select()用來監控多個文件描述符,當fd變得可讀/可寫時,select()將標記可讀/可寫fd。select()現在被認爲是低效的fd監控接口,在實際項目中通常用epoll()來代替select()。

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

int select(int maxfd, fd_set* readfds, fd_set* writefds, fd_set* exceptfds, struct timeval* timeout);

void FD_CLR(int fd, fd_set* set);
int  FD_ISSET(int fd, fd_set* set);
void FD_SET(int fd, fd_set* set);
void FD_ZERO(fd_set* set);

 

select()的參數解析:

1、一個參數maxfd是加入select()的最大文件描述符值+1,最大值爲1024。可以修改FD_SETSIZE的值以使select()支持更多文件描述符監控,但必須重新編譯內核,否則結果未知。

2、中間3個參數是3個fd集合,分別是你想監聽的可讀fd、可寫fd、異常fd。

3、最後一個參數timeout是指定select()的超時時間。

      timeout的取值可以是:

             NULL - 永久等待,直到有讀/寫/異常事件發生。
             0 - 立即返回,此時select()爲非阻塞狀態。
             其他值 - 指定select()等待時間。注意,timeout指定最長等待時間,但一旦有1個或多個fd可讀/寫/異常時select()就會返回。

select()的返回值:
       0 - 超時,且沒有任何讀/寫fd。
       > 0 - 有讀/寫fd,用FD_ISSET()進一步判斷。
       -1 - select()出錯。常見的錯誤包括: 
             EINTR - 捕獲到信號。通常可忽略。
             EBADF - 有無效的文件描述符。

Socket可讀/寫的常見情況分析:

select()返回sockfd可讀:

1、Receive緩衝區的數據大於或等於low-water mark的值。low-water mark的值可通過SO_RCVLOWAT選項控制,默認是1。 (即讀緩衝區中有數據)
       2、TCP連接接收到FIN,即Read half of the connections is closed。此時對sockfd的讀操作將返回0,即EOF。

3、如果sockfd是一個監聽套接字,則表明有新連接,可調用accept()函數建立新連接。 
       4、Socket出錯,此時對sockfd的讀操作將返回-1。

select()返回sockfd可寫:
       1、Send緩衝區的數據大於或等於low-water mark的值。low-water mark的值可通過SO_SNDLOWAT選項控制,默認是2048。 (即緩衝區有數據)
       2、Write half of the connection is closed,對sockfd的寫操作將產生SIGPIPE信號。 
       3、對非阻塞的sockfd調用connect(),connect()完成或失敗。 
       4、Socket出錯,此時對sockfd的寫操作將返回-1。

分析。若讀緩衝有數據,則socket可讀;若寫緩衝有空間,則socket可寫。如果socket出錯,則它本身處於可讀寫狀態,且調用read()/write()返回-1。若是Listen Socket,則有新連接來時它可讀;若是Non-block Connect Socket,則連接成功時它可寫。這些情況都不難理解,只有以上列出情況3,需要進一步說明。

參考:http://www.berlinix.com/dev/network.php

 

epoll的接口非常簡單,一共就三個函數:
1.創建epoll句柄
int epfd = epoll_create(int size);

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

2.將被監聽的描述符添加到epoll句柄或從epool句柄中刪除或者對監聽事件進行修改

int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

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)來說的
                           (
                                    水平觸發(LT):這個是默認的工作方式,水平觸發只要滿足條件,就觸發一個事件,只要有數據沒有被獲取,內核就會不斷通知(例如:epoll_wait獲取有數據可讀,讀取時卻發生錯誤,那麼就可以等待下一次通知,再去讀數)。
                                    邊緣觸發(ET):它通知那些fd剛剛變爲就緒態,並且只會通知一次(每當狀態變化時,纔會觸發一次事件)。
                                    select因爲要維護一個大的事件數組,同時要在內核和用戶態進行大量數據拷貝,所以效率底下。
                                   epoll高性能:
                                           1、使用mmap減少複製開銷(內核和用戶空間共享同一塊內存,減少內核態到用戶態的拷貝);
                                           2、epoll使用“事件”的就緒通知方式,通過epoll_ctl註冊fd,一旦fd就緒,內核就會採用類似callback的回調機制來激活該fd,epoll_wait便可以收到通知。
                           )
EPOLLONESHOT: 只監聽一次事件,當監聽完這次事件之後,如果還需要繼續監聽這個socket的話,需要再次把這個socket加入到EPOLL隊列裏。

3. 等待事件觸發,當超過timeout還沒有事件觸發時,就超時。
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的產生,類似於select()調用。
參數:
events用來從內核得到事件的集合;
maxevents告之內核events數組的成員個數,這個maxevents的值不能大於創建epoll_create()時的size
timeout是超時時間(毫秒,0會立即返回,-1將不確定,也有說法說是永久阻塞)。
該函數返回需要處理的事件數目,返回的事件集合在events數組中。
如返回0表示已超時。

 

當產生了一個EPOLLIN事件後:
讀數據的時候需要考慮的是當recv()返回的大小如果等於要求的大小,即sizeof(buf),那麼很有可能是緩衝區還有數據未讀完,也意味着該次事件還沒有處理完,所以還需要再次讀取(如果在ET水平模式下,可以等到下次事件觸發時,再讀數據):
while(rs)                   //ET模型
{
       buflen = recv(activeevents[i].data.fd, buf, sizeof(buf), 0);
       if(buflen < 0)
       {
             // 由於是非阻塞的模式,所以當errno爲EAGAIN或EINT時,表示當前緩衝區已無數據可讀(被其它線程可能讀了)
             // 在這裏就當作是該次事件已處理處。因爲是ET模式,如果有數據,內核還會繼續觸發讀事件。

             //即當buflen<0且errno=EAGAIN||errno=EINT時,表示沒有數據了。(讀/寫都是這樣)
             if(errno == EAGAIN || errno == EINT)

                      break;
             else
                      return;          //真的失敗了。
       }
       else if(buflen == 0)
       {
                    // 這裏表示對端的socket已正常關閉,收到了FIN包。soket在read到0個數據時,表示收到對端請求的FIN包。
       }
       if(buflen == sizeof(buf)
                    rs = 1;               // 需要再次讀取(有可能是因爲數據緩衝區buf太小,所以數據沒有讀完,可以等到下次再讀,沒有buf了)
       else
                    rs = 0;                //不需要再次讀取(當buflen<sizeof(buf)時,非阻塞文件描述符的特性),
}

當產生了一個EPOLLOUT事件後:

有,假如發送端流量大於接收端的流量(意思是epoll所在的程序讀比轉發的socket要快),由於是非阻塞的socket,那麼send()函數雖然返回,但實際緩衝區的數據並未真正發給接收端,這樣不斷的讀和發,當緩衝區滿後會產生EAGAIN錯誤(參考man send),同時,不理會這次請求發送的數據。所以,需要封裝socket_send()的函數用來處理這種情況,該函數會盡量將數據寫完再返回,返回-1表示出錯。在socket_send()內部,當寫緩衝已滿(send()返回-1,且errno爲EAGAIN),那麼會等待後再重試。這種方式並不很完美,在理論上可能會長時間的阻塞在socket_send()內部,但暫沒有更好的辦法。這種方法類似於readn和writen的封裝(在《UNIX環境高級編程》中也有介紹)
ssize_t socket_send(int sockfd, const char* buffer, size_t buflen)
{
          ssize_t tmp;
          size_t total = buflen;
          const char *p = buffer;
          while(1)
          {
                   tmp = send(sockfd, p, total, 0);
                   if(tmp < 0)
                   {
                              // 當send收到信號時,可以繼續寫,但這裏返回-1.
                              if(errno == EINTR)
                                     return -1;
                              // 當socket是非阻塞時,如返回此錯誤,表示寫緩衝隊列已滿,
                              // 在這裏做延時後再重試.
                              if(errno == EAGAIN)
                              {
                                     usleep(1000);
                                     continue;
                              }
                              return -1;
                   }
                   if((size_t)tmp == total)
                         return buflen;
                   total -= tmp;
                   p += tmp;
          }
          return tmp;
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章