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
刪除掉。