Linux下Socket編程(四)——epoll的使用

簡介

  • epoll與select
  • epoll_create
  • epoll_ctl
  • epoll_wait
  • ET、LT模式
#include <sys/epoll.h>

epoll與select

  • Epoll 沒有最大并發連接的限制,上限是最大可以打開文件的數目
  • 效率提升,epoll對于句柄事件的選擇不是遍歷的,是事件響應的,就是句柄上事件來就馬上選擇出來,不需要遍歷整個句柄鏈表,因此效率非常高,內核將句柄用紅黑樹保存的,IO效率不隨FD數目增加而線性下降。
  • 內存拷貝, select讓內核把 FD 消息通知給用戶空間的時候使用了內存拷貝的方式,開銷較大,但是Epoll 在這點上使用了共享內存的方式,這個內存拷貝也省略了。

相比于select,epoll最大的好處在于它不會隨著監聽fd數目的增長而降低效率。因為在內核中的select實現中,它是采用輪詢來處理的,輪詢的fd數目越多,自然耗時越多。
并且,在linux/posix_types.h頭文件有這樣的聲明:
#define __FD_SETSIZE 1024
表示select最多同時監聽1024個fd,當然,可以通過修改頭文件再重編譯內核來擴大這個數目,但這似乎并不治本。

epoll_create

int epoll_create(int size);

創建一個epoll的句柄,

  • size用來告訴內核這個監聽的數目一共有多大。

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

epoll_ctl

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

epoll的事件注冊函數,它不同與select()是在監聽事件時告訴內核要監聽什么類型的事件,而是在這里先注冊要監聽的事件類型

  • EPOLL_CTL_ADD 注冊新的fd到epfd中;
  • EPOLL_CTL_MOD 修改已經注冊的fd的監聽事件;
  • EPOLL_CTL_DEL 從epfd中刪除一個fd;

fd 是要監聽的fd
event 是要監聽什么樣的事件

typedef union epoll_data {
   void    *ptr;
   int      fd;
   uint32_t u32;
   uint64_t u64;
} epoll_data_t;

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

epoll_wait

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

ET、LT兩種工作模式:

  • EPOLLLT:完全靠Linux-kernel-epoll驅動,應用程序只需要處理從epoll_wait返回的fds, 這些fds我們認為它們處于就緒狀態。此時epoll可以認為是更快速的poll。
  • EPOLLET:此模式下,系統僅僅通知應用程序哪些fds變成了就緒狀態,一旦fd變成就緒狀態,epoll將不再關注這個fd的任何狀態信息(從epoll隊列移除), 直到應用程序通過讀寫操作(非阻塞)觸發EAGAIN狀態,epoll認為這個fd又變為空閑狀態,那么epoll又重新關注這個fd的狀態變化(重新加入epoll隊列)。 隨著epoll_wait的返回,隊列中的fds是在減少的,所以在大并發的系統中,EPOLLET更有優勢,但是對程序員的要求也更高。
舉例

假設現在對方發送了2k的數據,而我們先讀取了1k,然后這時調用了epoll_wait,如果是邊沿觸發ET,那么這個fd變成就緒狀態就會從epoll 隊列移除,則epoll_wait 會一直阻塞,忽略尚未讀取的1k數據; 而如果是水平觸發LT,那么epoll_wait 還會檢測到可讀事件而返回,我們可以繼續讀取剩下的1k 數據。
總結: LT模式可能觸發的次數更多, 一旦觸發的次數多, 也就意味著效率會下降; 但這樣也不能就說LT模式就比ET模式效率更低, 因為ET的使用對編程人員提出了更高更精細的要求,一旦使用者編程水平不夠, 那ET模式還不如LT模式。

ET模式僅當狀態發生變化的時候才獲得通知,這里所謂的狀態的變化并不包括緩沖區中還有未處理的數據,也就是說,如果要采用ET模式,需要一直read/write直到出錯為止,很多人反映為什么采用ET模式只接收了一部分數據就再也得不到通知了,大多因為這樣;而LT模式是只要有數據沒有處理就會一直通知下去的.

epoll IO多路復用模型實現機制

設想一下如下場景:有100萬個客戶端同時與一個服務器進程保持著TCP連接。而每一時刻,通常只有幾百上千個TCP連接是活躍的(事實上大部分場景都是這種情況)。如何實現這樣的高并發?
在select/poll時代,服務器進程每次都把這100萬個連接告訴操作系統(從用戶態復制句柄數據結構到內核態),讓操作系統內核去查詢這些套接字上是否有事件發生,輪詢完后,再將句柄數據復制到用戶態,讓服務器應用程序輪詢處理已發生的網絡事件,這一過程資源消耗較大,因此,select/poll一般只能處理幾千的并發連接。
epoll的設計和實現與select完全不同。epoll通過在Linux內核中申請一個簡易的文件系統,把原先的select/poll調用分成了3個部分:

  • 調用epoll_create()建立一個epoll對象(在epoll文件系統中為這個句柄對象分配資源)
  • 調用epoll_ctl向epoll對象中添加這100萬個連接的套接字
  • 調用epoll_wait收集發生的事件的連接

只需要在進程啟動時建立一個epoll對象,然后在需要的時候向這個epoll對象中添加或者刪除連接。同時,epoll_wait的效率也非常高,因為調用epoll_wait時,并沒有一股腦的向操作系統復制這100萬個連接的句柄數據,內核也不需要去遍歷全部的連接。

Linux內核具體的epoll機制實現思路。

當某一進程調用epoll_create方法時,Linux內核會創建一個eventpoll結構體,這個結構體中有兩個成員與epoll的使用方式密切相關

/*
 * This structure is stored inside the "private_data" member of the file
 * structure and rapresent the main data sructure for the eventpoll
 * interface.
 */
struct eventpoll {
    /* Protect the this structure access */
    spinlock_t lock;

    /*
     * This mutex is used to ensure that files are not removed
     * while epoll is using them. This is held during the event
     * collection loop, the file cleanup path, the epoll file exit
     * code and the ctl operations.
     */
    struct mutex mtx;

    /* Wait queue used by sys_epoll_wait() */
    wait_queue_head_t wq;

    /* Wait queue used by file->poll() */
    wait_queue_head_t poll_wait;

    /* List of ready file descriptors */
/*雙鏈表中則存放著將要通過epoll_wait返回給用戶的滿足條件的事件*/  
    struct list_head rdllist;
/*紅黑樹的根節點,這顆樹中存儲著所有添加到epoll中的需要監控的事件*/
    /* RB tree root used to store monitored fd structs */
    struct rb_root rbr;

    /*
     * This is a single linked list that chains all the "struct epitem" that
     * happened while transfering ready events to userspace w/out
     * holding ->lock.
     */
    struct epitem *ovflist;

    /* The user that created the eventpoll descriptor */
    struct user_struct *user;
};

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

而所有添加到epoll中的事件都會與設備(網卡)驅動程序建立回調關系,也就是說,當相應的事件發生時會調用這個回調方法。這個回調方法在內核中叫ep_poll_callback,它會將發生的事件添加到rdlist雙鏈表中。
在epoll中,對于每一個事件,都會建立一個epitem結構體,如下所示:

/*
 * Each file descriptor added to the eventpoll interface will
 * have an entry of this type linked to the "rbr" RB tree.
 */
struct epitem {
    /* RB tree node used to link this structure to the eventpoll RB tree */
//紅黑樹節點  
    struct rb_node rbn;

    /* List header used to link this structure to the eventpoll ready list */
//雙向鏈表節點  
    struct list_head rdllink;

    /*
     * Works together "struct eventpoll"->ovflist in keeping the
     * single linked chain of items.
     */
    struct epitem *next;

    /* The file descriptor information this item refers to */
//事件句柄信息
    struct epoll_filefd ffd;

    /* Number of active wait queue attached to poll operations */
    int nwait;

    /* List containing poll wait queues */
    struct list_head pwqlist;

    /* The "container" of this item */
//指向其所屬的eventpoll對象 
    struct 
![Uploading EPOLL_663944.jpg . . .]
eventpoll *ep;

    /* List header used to link this item to the "struct file" items list */
    struct list_head fllink;

    /* The structure that describe the interested events and the source fd */
 //期待發生的事件類型  
    struct epoll_event event;
};

當調用epoll_wait檢查是否有事件發生時,只需要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素即可。如果rdlist不為空,則把發生的事件復制到用戶態,同時將事件數量返回給用戶。


EPOLL.jpg

通過紅黑樹和雙鏈表數據結構,并結合回調機制,造就了epoll的高效。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容