epoll
epoll
是Linux核心的可延伸I/O事件通知機制[1]。於Linux 2.5.44首度登場,它設計目的旨在取代既有POSIX select(2)
與poll(2)
系統函數,讓需要大量操作檔案描述子的程式得以發揮更優異的效能(舉例來說:舊有的系統函數所花費的時間複雜度為O(n),epoll
的時間複雜度O(log n))。epoll 實現的功能與 poll 類似,都是監聽多個檔案描述子上的事件。
epoll
與FreeBSD的kqueue
類似,底層都是由可組態的作業系統核心物件建構而成,並以檔案描述子(file descriptor)的形式呈現於用戶空間。epoll
通過使用紅黑樹(RB-tree)搜尋被監視的檔案描述子(file descriptor)。
在 epoll 實例上註冊事件時,epoll 會將該事件添加到 epoll 實例的紅黑樹上並註冊一個回呼函數,當事件發生時會將事件添加到就緒鏈結串列中。
程式介面
編輯int epoll_create(int size);
在內核中創建epoll
實例並返回一個epoll
檔案描述子。
在最初的實現中,呼叫者通過 size
參數告知核心需要監聽的檔案描述子數量。如果監聽的檔案描述子數量超過 size, 則核心會自動擴容。而現在 size 已經沒有這種語意了,但是呼叫者呼叫時 size 依然必須大於 0,以保證後向相容性。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
向 epfd 對應的內核epoll
實例添加、修改或刪除對 fd 上事件 event 的監聽。op 可以為 EPOLL_CTL_ADD
, EPOLL_CTL_MOD
, EPOLL_CTL_DEL
分別對應的是添加新的事件,修改檔案描述子上監聽的事件類型,從實例上刪除一個事件。如果 event 的 events 屬性設置了 EPOLLET
flag,那麼監聽該事件的方式是邊緣觸發。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
當 timeout 為 0 時,epoll_wait 永遠會立即返回。而 timeout 為 -1 時,epoll_wait 會一直阻塞直到任一已註冊的事件變為就緒。當 timeout 為一正整數時,epoll 會阻塞直到計時 timeout 毫秒終了或已註冊的事件變為就緒。因為核心排程延遲,阻塞的時間可能會略微超過 timeout 毫秒。
觸發模式
編輯epoll
提供邊沿觸發及狀態觸發模式。在邊沿觸發模式中,epoll_wait
僅會在新的事件首次被加入epoll
佇列時返回;於level-triggered模式下,epoll_wait
在事件狀態未變更前將不斷被觸發。狀態觸發模式是默認的模式。
狀態觸發模式與邊沿觸發模式有讀和寫兩種情況,我們先來考慮讀的情況。假設我們註冊了一個讀事件到epoll
實例上,epoll
實例會通過epoll_wait
返回值的形式通知我們哪些讀事件已經就緒。簡單地來說,在狀態觸發模式下,如果讀事件未被處理,該事件對應的內核讀緩衝區非空,則每次調用 epoll_wait
時返回的事件列表都會包含該事件。直到該事件對應的內核讀緩衝區為空為止。而在邊沿觸發模式下,讀事件就緒後只會通知一次,不會反復通知。
然後我們再考慮寫的情況。水平觸發模式下,只要檔案描述子對應的內核寫緩衝區未滿,就會一直通知可寫事件。而在邊沿觸發模式下,內核寫緩衝區由滿變為未滿後,只會通知一次可寫事件。
舉例來說,倘若有一個已經於epoll
註冊之管線化接獲資料,epoll_wait
將返回,並發出資料讀取的訊號。現假設緩衝區的資料僅有部份被讀取並處理,在level-triggered模式下,任何對epoll_wait
之呼叫都將即刻返回,直到緩衝區中的資料全部被讀取;然而,在edge-triggered的情境下,epoll_wait
僅會於再次接收到新資料(亦即,新資料被寫入管線化)時返回。
邊沿觸發模式
編輯邊沿觸發模式使得程式有可能在用戶態快取 IO 狀態。nginx 使用的是邊沿觸發模式。
檔案描述子有兩種情況是推薦使用邊沿觸發模式的。
- read 或者 write 系統呼叫返回了 EAGAIN。
- 非阻塞的檔案描述子。
可能的缺陷:
- 如果 IO 空間很大,你要花很多時間才能把它一次讀完,這可能會導致飢餓。舉個例子,假設你在監聽一個檔案描述子列表,而某個檔案描述子上有大量的輸入(不間斷的輸入流),那麼你在讀完它的過程中就沒空處理其他就緒的檔案描述子。(因為邊沿觸發模式只會通知一次可讀事件,所以你往往會想一次把它讀完。)一種解決方案是,程式維護一個就緒佇列,當
epoll
實例通知某檔案描述子就緒時將它在就緒佇列數據結構中標記為就緒,這樣程式就會記得哪些檔案描述子等待處理。Round-Robin 迴圈處理就緒佇列中就緒的檔案描述子即可。 - 如果你快取了所有事件,那麼一種可能的情況是 A 事件的發生讓程式關閉了另一個檔案描述子 B。但是核心的
epoll
實例並不知道這件事,需要你從epoll
刪除掉。