EPOLL的工作原理及流程
時間:2018-05-14作者:華清遠見
一.Epoll是什么? epoll是個什么東東呢?按照man手冊的說法:是為處理大批量句柄而作了改進的poll。當然,這不是2.6內核才有的,它是在2.5.44內核中被引進的(epoll(4) is a new API introduced in Linux kernel 2.5.44),它幾乎具備了之前所說的一切優點,被公認為Linux2.6下性能最好的多路I/O就緒通知方法。 二.epoll與poll和select對比 [1]select 的缺點: 單個進程能夠監視的文件描述符的數量存在最大限制,通常是1024,當然可以更改數量,但由于select采用輪詢的方式掃描文件描述符,文件描述符數量越多,性能越差;(在linux內核頭文件中,有這樣的定義:#define __FD_SETSIZE 1024) 內核 / 用戶空間內存拷貝問題,select需要復制大量的句柄數據結構,產生巨大的開銷; select返回的是含有整個句柄的數組,應用程序需要遍歷整個數組才能發現哪些句柄發生了事件; select中應用程序如果沒有完成對一個已經就緒的文件描述符進行IO操作,那么之后每次select調用還是會將這些文件描述符通知進程。 相對于我們的select模型,我們的poll是使用鏈表保持文件描述符,因此沒有了監視文件數量的限制,但是2,3,4等缺點依舊存在。 拿select模型為例,假設我們的服務器需要支持100萬的并發連接,則在__FD_SETSIZE 為1024的情況下,則我們至少需要開辟1k個進程才能實現100萬的并發連接。除了進程間上下文切換的時間消耗外,從內核/用戶空間大量的無腦內存拷貝、數組輪詢等,是系統難以承受的。因此,基于select模型的服務器程序,要達到10萬級別的并發訪問,是一個很難完成的任務。 因此,該epoll上場了。 三.Epoll的工作原理 設想一下如下場景:有100萬個客戶端同時與一個服務器進程保持著TCP連接。而每一時刻,通常只有幾百上千個TCP連接是活躍的(事實上大部分場景都是這種情況)。如何實現這樣的高并發? 在select/poll時代,服務器進程每次都把這100萬個連接告訴操作系統(從用戶態復制句柄數據結構到內核態),讓操作系統內核去查詢這些套接字上是否有事件發生,輪詢完后,再將句柄數據復制到用戶態,讓服務器應用程序輪詢處理已發生的網絡事件,這一過程資源消耗較大,因此,select/poll一般只能處理幾千的并發連接。 epoll的設計和實現與select完全不同。epoll通過在Linux內核中申請一個簡易的文件系統(文件系統一般用什么數據結構實現?二叉樹樹)。然后epoll的調用分成了3個部分: 1)調用epoll_create()建立一個epoll對象(在epoll文件系統中為這個句柄對象分配資源) 2)調用epoll_ctl向epoll對象中添加這100萬個連接的套接字 3)調用epoll_wait收集發生的事件的連接 如此一來,要實現上面說是的場景,只需要在進程啟動時建立一個epoll對象,然后在需要的時候向這個epoll對象中添加或者刪除連接。同時,epoll_wait的效率也非常高,因為調用epoll_wait時,并沒有一股腦的向操作系統復制這100萬個連接的句柄數據,內核也不需要去遍歷全部的連接。 具體流程: [1]當我們某個進程調用epoll_create()函數的時候,linux內核會默認創建一個eventpoll結構體,這個結構體中有兩個成員與epoll的使用方式相關。
每一個epoll對象都有一個獨立的eventpoll結構體,用于存放通過epoll_ctl方法向epoll對象中添加進來的事件。這些事件都會掛載在紅黑樹中,如此,重復添加的事件就可以通過紅黑樹而高效的識別出來. 而所有添加到epoll中的事件都會與設備(網卡)驅動程序建立回調關系,也就是說,當相應的事件發生時會調用這個回調方法。這個回調方法在內核中叫ep_poll_callback,它會將發生的事件epitem添加到rdlist雙鏈表中。 在epoll中,對于每一個事件,都會建立一個epitem結構體,如下所示:
當調用epoll_wait檢查是否有事件發生時,只需要檢查eventpoll對象中的rdlist雙鏈表中是否有epitem元素即可。如果rdlist不為空,則把發生的事件復制到用戶態,同時將事件數量返回給用戶。
總結: (1)我們我們調用epoll_wait()函數的時候,系統創建一個epoll對象,每個對象都有一個 叫做eventpoll類型的結構體與之對應,該結構體中主要有兩個主要的成員,一個是 rbn,代表將要通過epoll_ctl向epll對象中添加的事件。這些事情都是掛載在紅黑樹中。 一個是rdlist,里面存放的是將要發生的事件 (2)當我們使用epoll_ctrl()函數的時候,就是向epoll對象中添加,刪除,修改感興趣的事件 (3) epoll_wait()系統。通過此調用收集在epoll監控中已經發生的事件。當監控的事件狀態發生改變的時候,我們會調用會調用函數把epitem加入到rdlist中去。 一. Epoll的API函數接口 3.1 事件的創建---epoll_create(); int epoll_create(int size); int epoll_create1(int flags); 功能:poll_create()創建一個epoll的事例,通知內核需要監聽size個fd。size指的并不是最大的后備存儲設備,而是衡量內核內部結構大小的一個提示。當創建成功后,會占用一個fd,所以記得在使用完之后調用close(),否則fd可能會被耗盡。 Note:自從Linux2.6.8版本以后,size值其實是沒什么用的,不過要大于0,因為內核可以動態的分配大小,所以不需要size這個提示了。 其次:epoll_create1()函數,其實它和epoll_create差不多,不同的是epoll_create1函參數flag: · 當flag是0時,表示和epoll_create函數完全一樣,不需要size的提示了; · 當flag = EPOLL_CLOEXEC,創建的epfd會設置FD_CLOEXEC; · 當flag = EPOLL_NONBLOCK,創建的epfd會設置為非阻塞。 一般用法都是使用EPOLL_CLOEXEC。 Note:關于FD_CLOEXEC,它是fd的一個標識說明,用來設置文件close-on-exec狀態的。當close-on-exec狀態為0時,調用exec時,fd不會被關閉;狀態非零時則會被關閉,這樣做可以防止fd泄露給執行exec后的進程。 返回值:成功返回一個非負的文件描述符。 例如: int epfd = epoll_create(20); //注:20為隨機寫的一個值,大于0即可。 或 int epfd = epoll_create1(0); 3.1 事件的注冊---epoll_ctl(); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); 功能:epoll的事件注冊函數,epoll的事件注冊函數,它不同于select()是在監聽事件時告訴內核要監聽什么類型的事件,而是在這里先注冊要監聽的事件類型。 參數: @epfd epoll_create()函數的返回值 @op 表示參數的動作,常用以下宏: EPOLL_CTL_ADD:注冊新的fd到epfd中; EPOLL_CTL_MOD:修改已經注冊的fd的監聽事件; EPOLL_CTL_DEL:從epfd中刪除一個fd; @fd 表示我們需要監聽的文件描述符 @event 表示告訴內核,我們需要監聽什么事件。 結構體如下: typedef union epoll_data { void *ptr; int fd; //保存我們使用的sockfd 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隊列里 返回值:成功返回0,失敗返回-1. 3.2等待事件---epoll_wait(); int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout); 功能:收集在epoll監控的事件中已經發送的事件。 參數: @epfd epoll_create()函數的返回值 @events 已經分配好的epoll_event結構體數組,epoll會把將發生的事情存放到events中。 @maxevents 告訴內核events有多大。必須大于0 @timeout 超時時間 -1 表示epoll將無限制的等待下去 0 立即返回 >0 指定超時時間 返回值: 成功返回已經就緒的文件描述符個數。若是設置了超時時間,在超時時間內返回0. 失敗返回-1. 五.Epoll的工作模式。 LT(level triggered)是缺省的工作方式,并且同時支 持block和no-block socket.在這種做法中,。當epoll_wait檢測到描述符事件發生并將此事件通知應用程序,應用程序可以不立即處理該事件。下次調用epoll_wait時,會再次響應應用程序并通知此事件。 ET (edge-triggered)是高速工作方式,常工作在no-block socket。在這種模式下,當epoll_wait檢測到描述符事件發生并將此事件通知應用程序,應用程序必須立即處理該事件。如果不處理,下次調用epoll_wait時,不會再次響應應用程序并通知此事件。 EPOLLIN事件: EPOLLIN事件則只有當對端有數據寫入時才會觸發,所以觸發一次后需要不斷讀取所有數據直到讀完EAGAIN為止。否則剩下的數據只有在下次對端有寫入時才能一起取出來了。設想這樣一個場景:接收端接收完整的數據后會向對端發送應答報文, ,對端才會繼續向接收端發送數據,從而觸發下一次的EPOLLIN,而這時沒有讀完socket緩沖區中的所有數據,導致接收端無法向對端發送應答報文,而對端沒有收到應答報文,也就不會再發送數據觸發下一次的EPOLLIN,而沒有下一次的EPOLLIN事件,接收端也就永遠不知道此socket緩沖區中還有未讀出的數據。一個完美的死循環) 示例代碼: 實現多個客戶端和服務端的回射代碼。 Server.c
Client.c
運行結果:
相關資訊
發表評論
|