1. 程式人生 > >I/O多路轉接——select、poll 和 epoll

I/O多路轉接——select、poll 和 epoll

一、select

1. select() 函式

  • select系統呼叫是用來讓我們的程式監視多個檔案描述符的狀態變化的;
  • 程式會停在select這裡等待,直到被監視的檔案描述符有一個或多個發生了狀態改變。

select函式原型:

#include <sys/select.h>

int select( int nfds, 
            fd_set *readfds, fd_set *writefds, fd_set *exceptfds,
            struct timeval *timeout);
  • 引數nfds:指定被監聽的檔案描述符的總數,通常被設定為select監聽的所有檔案描述符中最大的那個+1。

  • readfds, writefds 和 exceptfds 引數分別指向可讀、可寫和異常等事件對應的檔案描述符。程式調 select 是通過這三個引數傳入自己感興趣的檔案描述符,select 呼叫返回時,核心會修改他們,以便通知程式哪些檔案描述符已經就緒。這是典型的輸入輸出型引數,他們都是 fd_set 結構體型別。

  • 引數 timeout 為結構體 timeval,用來設定 select() 的等待時間

(1)引數timeout

  • NULL:則表示 select() 沒有 timeout,select 將一直被阻塞,直到某個檔案描述符上發生了事件;
  • 0:僅檢測描述符集合的狀態,然後立即返回,並不等待外部事件的發生。
  • 特定的時間值:如果在指定的時間段裡沒有事件發⽣生,select() 將超時返回。

timeval結構體定義如下:

struct timeval
{
    long tv_sec;  // 秒
    long tv_usec; // 微妙
}

(2)fd_set結構體

  第二個引數、第三個引數、第四個引數都是指向fd_set型別的指標,fd_set結構體內實質上就是一個位圖。點陣圖中每個元素的下標代表一個檔案描述符。每個元素的取值只有0和1。

  fd_set 的大小由 FD_SETSIZE 指定,這就限制了select 能同時處理的檔案描述符的總數。

  由於對點陣圖的操作比較繁瑣,所以系統已經封裝好了一套函式供我們使用。

#include <sys/select.h>

FD_CLR(int fd, fd_set* fdset);  //清除fdset的位fd
FD_SET(int fd, fd_set* fdset);  //設定fdset的位fd
FD_ZERO(fd_set* fdset;          //清除fdset所有的位
FD_ISSET(int fd, fd_set* fdset) //測試fdset的位fd是否被設定

(3)函式返回值

  select 函式成功時返回就緒(可讀、可寫和異常)檔案描述符的總數。如果在超時時間內沒有任何檔案描述符就緒,select 返回0.select失敗時返回-1並設定error。

  如果在select等待期間,程式收到訊號,則select立即返回-1,並設定error為EINTR。

2. socket就緒條件

(1)讀就緒

  • socket核心中, 接收緩衝區中的位元組數, 大於等於低水位標記SO_RCVLOWAT. 此時可以無阻塞的讀該檔案描述符, 並且返回值大於0;

  • socket TCP通訊中, 對端關閉連線(收到了FIN的TCP連線), 此時對該socket讀, 將不會阻塞,而是直接返回0(也就是EOF);

  • 監聽的socket上有新的連線請求;該套接字是一個listen的監聽套接字,並且目前已經完成的連線數不為0。對這樣的套接字進行accept操作通常不會阻塞。

  • socket上有未處理的錯誤;

(2)寫就緒

  • socket核心中, 傳送緩衝區中的可用位元組數(傳送緩衝區的空閒位置大小), 大於等於低水位標記SO_SNDLOWAT, 此時可以無阻塞的寫, 並且返回值大於0;

  • socket的寫操作被關閉(close或者shutdown或主動傳送了FIN的TCP連線).對一個寫操作被關閉的socket進行寫操作, 會觸發SIGPIPE訊號;

  • socket使用非阻塞connect連線成功或失敗之後;

  • socket上有未讀取的錯誤。

(3)異常就緒

  • socket上收到帶外資料. 關於帶外資料, 和TCP緊急模式相關(TCP協議頭中, 有一個緊急指標的欄位)。

3. select的特點

  • 可監控的檔案描述符個數取決與sizeof(fdset)的值。如果sizeof(fdset)=512,那麼支援的最大檔案描述符是 512*8=4096.

  • 將fd加入select監控集的同時,還要再使用一個數據結構array儲存放到select監控集中的fd,

    • 一是用於再select返回後,array作為源資料和fdset進行FDISSET判斷。
    • 二是select返回後會把以前加入的但並無事件發生的fd清空,則每次開始select前都要重新從array取得fd逐一加入(FD_ZERO最先),掃描array的同時取得fd最大值maxfd,用於select的第一個引數。

4. select的缺點

  • 每次呼叫select, 都需要手動設定fd集合,從介面使用角度來說非常不便.

  • 每次呼叫select,都需要把fd集合從使用者態拷貝到核心態,這個開銷在fd很多時會很大

  • 同時每次呼叫select都需要在核心遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大

  • select支援的檔案描述符數量太小.

二、poll

1. poll()函式

#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);

// pollfd結構
struct pollfd {
    int fd;         /* file descriptor  */
    short events;   /* requested events */
    short revents;  /* returned events  */
};

(1)引數

  • fds是一個poll函式監聽的結構列表. 每一個元素中, 包含了三部分內容:檔案描述符, 監聽的事件集合, 返回的事件集合.

  • nfds表示fds陣列的長度.

  • timeout表示poll函式的超時時間, 單位是毫秒(ms).

(2)events和revents

這裡寫圖片描述

(3)返回值

  • 返回值小於0,表示出錯;
  • 返回值等於0,表示poll函式等待超時;
  • 返回值大於0,表示poll由於監聽的檔案描述符就緒而返回。

2. poll的缺點

poll中監聽的檔案描述符數目增多時:

  • 和select函式一樣,poll返回後,需要輪詢pollfd來獲取就緒的描述符.
  • 每次呼叫poll都需要把大量的pollfd結構從使用者態拷貝到核心中.
  • 同時連線的大量客戶端在一時刻可能只有很少的處於就緒狀態, 因此隨著監視的描述符數量的增長,其效率也會線性下降.

三、epoll

1. epoll_create()函式

#include <sys/epoll.h>
int epoll_create(int size);
/*
 * 功能:在核心中建立一個事件表
 * 引數:告訴核心事件表需要多大
 * 返回值:建立的事件表的檔案描述符  
 */

2. epoll_ctl()函式

#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
/*
 * 功能:操作epoll的核心事件表
 * 引數:
 *   1. epfd:epoll_create()函式的返回值,epoll核心事件表的檔案描述符

 *   2. op:要對事件表進行的操作,有以下三個選項:
       ①EPOLL_CTL_ADD 給fd檔案描述符新增event事件 
       ②EPOLL_CTL_MOD 將fd檔案描述符上的事件修改成event事件
       ③EPOLL_CTL_DEL 刪除fd檔案描述符上的事件,event引數寫NULL 

 *   3. fd:要操作的檔案描述符 

 *   4. event:告訴核心需要監聽什麼事件,epoll_event結構體如下:
        struct epoll_event {
            uint32_t     events; //可以取值EPOLLIN、EPOLLOUT、EPOLLET等    
            epoll_data_t data;      
        };
        而其中的epoll_data_t 是一個聯合體,其結構如下:
        typedef union epoll_data {
            void        *ptr; // 指定與fd相關的使用者資料(因為聯合體,每次只能用其中一個)
            int          fd;  // 事件所屬的檔案描述符
            uint32_t     u32;
            uint64_t     u64;
        } epoll_data_t;

 * 返回值:成功返回0,失敗返回-1
 */

3. epoll_wait()

#include <sys/epoll.h>

int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
/*
 * 功能:在超時時間內等待一組檔案描述符上的事件
 * 引數:
 *   1. epfd:epoll_create()函式的返回值,epoll核心事件表的檔案描述符

 *   2. events:分配好的epoll_event結構體陣列,用於儲存事件表中所有已發生的事件,即輸出型引數,它使得epoll大大提高了效率

 *   3. maxevents:最多監聽多少個事件

 *   4. timeout:和select的timeout一樣。 
 * 
 * 返回值:和select返回值一樣
 */

關於struct epoll_event結構體:
這裡寫圖片描述

  • 第一個成員 events:代表使用者關心的事件,值可以設成EPOLLIN、EPOLLOUT等
  • 第二個成員 data:是一個union epoll_data 結構體,所以裡面的 ptr 和 fd 不能同時使用。

4. epoll高效背後的祕密

  在呼叫 epoll_create 時,Linux核心會建立一個 eventpoll 結構體,這個結構體中有兩個成員就是epoll高效的祕密。

struct eventpoll{
    ...
    /*紅黑樹的根節點,這顆樹中儲存著所有新增到epoll中的需要監控的事件*/
    struct rb_root  rbr;

    /*雙鏈表中則存放著將要通過epoll_wait返回給使用者的滿足條件的事件*/
    struct list_head rdlist;
    ...
};

  每一個epoll物件都有一個獨立的eventpoll結構體,用於存放通過epoll_ctl方法向epoll物件中新增進來的事件。這些事件都會掛載在紅黑樹中,如此,重複新增的事件就可以通過紅黑樹而高效的識別出來(紅黑樹的插入時間效率是lgn,其中n為樹的高度)。

  而所有新增到epoll中的事件都會與裝置(網絡卡)驅動程式建立回撥關係,也就是說,當相應的事件發生時會呼叫這個回撥方法。這個回撥方法在核心中叫ep_poll_callback,它會將發生的事件新增到rdlist雙鏈表中。

  在epoll中,對於每一個事件,都會建立一個epitem結構體,如下所示:

struct epitem{
    struct rb_node  rbn;//紅黑樹節點
    struct list_head    rdllink;//雙向連結串列節點
    struct epoll_filefd  ffd;  //事件控制代碼資訊
    struct eventpoll *ep;    //指向其所屬的eventpoll物件
    struct epoll_event event; //期待發生的事件型別
}

  當呼叫epoll_wait檢查是否有事件發生時,只需要檢查eventpoll物件中的rdlist雙鏈表中是否有epitem元素即可。如果rdlist不為空,則把發生的事件複製到使用者態,同時將事件數量返回給使用者。
這裡寫圖片描述

  1. 當呼叫epoll_wait時就相當於以往呼叫select/poll,但是這時卻不用傳遞socket控制代碼給核心,因為核心已經在epoll_ctl中拿到了要監控的控制代碼列表。

  2. 在核心裡,一切皆檔案。所以,epoll 向核心註冊了一個檔案系統,用於儲存上述的被監控socket。當你呼叫epoll_create時,就會在這個虛擬的epoll檔案系統裡建立一個file結點。當然這個file不是普通檔案,它只服務於epoll。

  3. epoll在被核心初始化時(作業系統啟動),同時會開闢出epoll自己的核心高速cache區,用於安置每一個我們想監控的socket,這些socket會以紅黑樹的形式儲存在核心cache裡,以支援快速的查詢、插入、刪除。這個核心高速cache區,就是建立連續的實體記憶體頁,然後在之上建立slab層,簡單的說,就是物理上分配好你想要的size的記憶體物件,每次使用時都是使用空閒的已分配好的物件。

  4. 極其高效的原因:
      我們在呼叫epoll_create時,核心除了幫我們在epoll檔案系統裡建了個file結點,在核心cache裡建了個紅黑樹用於儲存以後epoll_ctl傳來的socket外,還會再建立一個list連結串列,用於儲存準備就緒的事件,當epoll_wait呼叫時,僅僅觀察這個list連結串列裡有沒有資料即可。有資料就返回,沒有資料就sleep,等到timeout時間到後即使連結串列沒資料也返回。所以,epoll_wait非常高效。
      這個準備就緒list連結串列是怎麼維護的呢?當我們執行epoll_ctl時,除了把socket放到epoll檔案系統裡file物件對應的紅黑樹上之外,還會給核心中斷處理程式註冊一個回撥函式,告訴核心,如果這個控制代碼的中斷到了,就把它放到準備就緒list連結串列裡。所以,當一個socket上有資料到了,核心在把網絡卡上的資料copy到核心中,然後就把socket插入到準備就緒連結串列裡了。
      如此,一顆紅黑樹,一張準備就緒控制代碼連結串列,少量的核心cache,就幫我們解決了大併發下的socket處理問題。執行epoll_create時,建立了紅黑樹和就緒連結串列,執行epoll_ctl時,如果增加socket控制代碼,則檢查在紅黑樹中是否存在,存在立即返回,不存在則新增到樹幹上,然後向核心註冊回撥函式,用於當中斷事件來臨時向準備就緒連結串列中插入資料。執行epoll_wait時立刻返回準備就緒連結串列裡的資料即可。

5. LT 和 ET

  EPOLL事件有兩種模型 LT(Level Triggered 水平觸發事件) 和 ET(Edge Triggered 邊沿觸發事件)

  LT(level triggered,水平觸發模式)是預設的工作方式,並且同時支援 block 和 non-block socket。在這種做法中,核心告訴你一個檔案描述符是否就緒了,然後你可以對這個就緒的fd進行IO操作。如果你不作任何操作,核心還是會繼續通知你的,所以,這種模式程式設計出錯誤可能性要小一點。

  ET(edge-triggered,邊緣觸發模式)是高速工作方式,只支援no-block socket。在這種模式下,當描述符從未就緒變為就緒時,核心通過epoll告訴你。然後它會假設你知道檔案描述符已經就緒,並且不會再為那個檔案描述符傳送更多的就緒通知,等到下次有新的資料進來的時候才會再次出發就緒事件。

  從作業系統角度看,當一個socket控制代碼上有事件時,核心會把該控制代碼插入上面所說的準備就緒list連結串列,這時我們呼叫epoll_wait,會把準備就緒的 socket 拷貝到使用者態記憶體,然後清空準備就緒list連結串列,最後,epoll_wait幹了件事,就是檢查這些socket,如果是LT模式,並且這些socket上確實有未處理的事件時,又把該控制代碼放回到剛剛清空的準備就緒連結串列了。所以,LT的控制代碼,只要它上面還有事件,epoll_wait每次都會返回這個控制代碼。

四、select、poll 和 epoll 的優缺點對比

  1. select優點:
    1. 一次可以等待多個檔案描述符,減少了平均等待時間
    2. 客戶越來越多時,減輕了程序排程的壓力(相較於多程序多執行緒伺服器)
  2. select缺點:

    1. 能監聽的檔案描述符有上限,這個上限是由fd_set決定的。
    2. 它返回的只是就緒事件的個數,要判斷是那個事件滿足,需要遍歷檔案描述符。
    3. select監聽的集合是輸入輸出引數,每次監聽都需要重新初始化。
    4. 每次呼叫select,都需要把fd集合從使用者態拷貝到核心態,這個開銷在fd很多時會很大
    5. 核心採用輪詢(遍歷fd集合)的方式來檢測就緒事件,這個開銷在fd很多時也很大
    6. select和poll都只能工作在低效的LT(水平觸發)模式
  3. poll優點:

    1. poll監聽的檔案描述符沒有最大數量的限制(65535)
    2. poll對於select來說包含了一個pollfd結構,pollfd結構包含了要監視的event和發生的revent,而不像select那樣使用輸入輸出的傳遞方式。所以不需要每次監聽都初始化
  4. poll缺點:

    1. 數量過大以後其效率也會線性下降。
    2. poll和select一樣也是返回就緒事件的個數,需要遍歷檔案描述符來判斷是那個事件已經就緒,當數量很大時,開銷也就很大。
    3. select和poll都只能工作在低效的LT(水平觸發)模式
    4. 每次呼叫poll,都需要把pollfd陣列從使用者態拷貝到核心態,這個開銷在fd很多時會很大
    5. 核心採用輪詢(遍歷pollfd陣列)的方式來檢測就緒事件,這個開銷在fd很多時也很大
  5. epoll 的優點

    1. 檔案描述符數目無上限: 通過epoll_ctl()來註冊一個檔案描述符, 核心中使用紅黑樹的資料結構來管理所有需要監控的檔案描述符.
    2. 基於事件的就緒通知方式: 一旦被監聽的某個檔案描述符就緒, 核心會採用類似於callback的回撥機制, 迅速啟用這個檔案描述符. 這樣隨著檔案描述符數量的增加, 也不會影響判定就緒的效能;
    3. 維護就緒佇列: 當檔案描述符就緒, 就會被放到核心中的一個就緒佇列中. 這樣呼叫epoll_wait獲取就緒檔案描述符時,只需要去佇列中的元素即可,操作時間複雜度是O(1)
      這裡寫圖片描述