簡介
- 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的高效。