EPOLL(7) | Linux Programmer's Manual | EPOLL(7) |
epoll - I/O 事件通知設施
#include <sys/epoll.h>
epoll API 的任務與 poll(2) 類似:監控多個檔案描述符,找出其中可以進行I/O 的檔案描述符。 epoll API 既可以作為邊緣觸發(edge-triggered)的介面使用,也可以作為水平觸發(level-triggered)的介面使用,並能很好地擴充套件,監視大量檔案描述符。
epoll API 的核心概念是 epoll 例項(epoll instance),這是核心的一個內部資料結構,從使用者空間的角度看,它可以被看作一個內含兩個列表的容器:
下列系統呼叫可用於建立和管理 epoll 例項:
epoll 事件的分發介面既可以表現為邊緣觸發(ET),也可以表現為水平觸發(LT)。這兩種機制的區別描述如下。假設發生下列情況:
如果讀取方新增 rfd 到 epoll 介面時使用了 EPOLLET (邊緣觸發)標誌位,那麼縱使此刻檔案輸入緩衝區中仍有可用的資料(剩餘的1 KB 資料),步驟5中的epoll_wait(2) 呼叫仍可能會掛起;與此同時,寫入方可能在等待讀取方對它傳送的資料的響應。造成這種互相等待的情形的原因是邊緣觸發模式只有在被監控的檔案描述符發生變化時才會遞送事件。因此,在步驟5中,讀取方最終可能會為一些已經存在於自己輸入緩衝區內的資料一直等下去。在上面的例子中,由於寫入方在第2步中進行了寫操作, rfd 上產生了一個事件,這個事件在第3步中被讀取方消耗了。但讀取方在第4步中進行的讀操作卻沒有消耗完整個緩衝區的資料,因此在第5步中對epoll_wait(2) 的呼叫可能會無限期地阻塞。
使用 EPOLLET 標誌位的應用程式應當使用非阻塞的檔案描述符,以避免(因事件被消耗而)使正在處理多個檔案描述符的任務因阻塞的讀或寫而出現飢餓。將 epoll用作邊緣觸發(EPOLLET)的介面,建議的使用方法如下:
相較而言,當作為水平觸發的介面使用時(預設情況,沒有指定 EPOLLET), epoll只是一個更快的 poll(2),可以用在任何能使用 poll(2) 的地方,因為此時兩者的語義相同。
即使是邊緣觸發的 epoll,在收到多個數據塊時也可能產生多個事件,因此呼叫者可以指定 EPOLLONESHOT 標誌位,告訴 epoll 在自己用 epoll_wait(2)收到事件後禁用相關的檔案描述符。當指定了 EPOLLONESHOT 標誌位時,呼叫者可使用epoll_ctl(2) 與 EPOLL_CTL_MOD 標誌位重灌(rearm)一個被禁用的檔案描述符,這是呼叫者而不是 epoll 的責任。
如果多個執行緒(或程序,如果子程序透過 fork(2) 繼承了 epoll 檔案描述符)等待同一個 epoll 檔案描述符,且同時在 epoll_wait(2) 中被阻塞,那麼當興趣列表中某個標記為邊緣觸發 (EPOLLET) 通知的檔案描述符準備就緒,這些執行緒(或程序)中只會有一個執行緒(或程序)從 epoll_wait(2) 中被喚醒。這為避免某些場景下的“驚群”(thundering herd)喚醒提供了有用的最佳化。
如果系統透過 /sys/power/autosleep 處於 autosleep 模式,那麼當某個事件的發生將裝置從睡眠中喚醒時,裝置驅動程式僅會保持裝置喚醒直到該事件入隊為止。若想保持裝置喚醒直到事件被處理完畢,則需使用 epoll_ctl(2) 的 EPOLLWAKEUP標誌位。
當在 struct epoll_event 結構體的 events 段中設定 EPOLLWAKEUP標誌位時,從事件入隊的那一刻起,到 epoll_wait(2) 呼叫返回事件,再一直到下一次 epoll_wait(2) 呼叫之前,系統會一直保持喚醒。若要讓事件保持系統喚醒的時間超過這個時間,那麼在第二次 epoll_wait(2) 呼叫之前,應當設定一個單獨的wake_lock。
以下介面可以用來限制 epoll 消耗的核心記憶體的量。
epoll 作為水平觸發介面的用法與 poll(2) 具有相同的語義,但邊緣觸發的用法需要更多的說明,以避免應用程式事件迴圈的停滯。在下面的例子中,呼叫了 listen(2)來監聽 listener,一個非阻塞的套接字。函式 do_use_fd() 使用新就緒的檔案描述符,直到 read(2) 或 write(2) 返回 EAGAIN。一個事件驅動的狀態機應用程式在接收到 EAGAIN 後,應該記錄它的當前狀態,這樣在下一次呼叫do_use_fd() 時,它就能從之前停下的地方繼續 read(2) 或 write(2)。
#define MAX_EVENTS 10 struct epoll_event ev, events[MAX_EVENTS]; int listen_sock, conn_sock, nfds, epollfd; /* Code to set up listening socket, 'listen_sock', (socket(), bind(), listen()) omitted. */ epollfd = epoll_create1(0); if (epollfd == -1) { perror("epoll_create1"); exit(EXIT_FAILURE); } ev.events = EPOLLIN; ev.data.fd = listen_sock; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, listen_sock, &ev) == -1) { perror("epoll_ctl: listen_sock"); exit(EXIT_FAILURE); } for (;;) { nfds = epoll_wait(epollfd, events, MAX_EVENTS, -1); if (nfds == -1) { perror("epoll_wait"); exit(EXIT_FAILURE); } for (n = 0; n < nfds; ++n) { if (events[n].data.fd == listen_sock) { conn_sock = accept(listen_sock, (struct sockaddr *) &addr, &addrlen); if (conn_sock == -1) { perror("accept"); exit(EXIT_FAILURE); } setnonblocking(conn_sock); ev.events = EPOLLIN | EPOLLET; ev.data.fd = conn_sock; if (epoll_ctl(epollfd, EPOLL_CTL_ADD, conn_sock, &ev) == -1) { perror("epoll_ctl: conn_sock"); exit(EXIT_FAILURE); } } else { do_use_fd(events[n].data.fd); } } }
當作為邊緣觸發的介面使用時,出於效能考慮,可在新增檔案描述符(EPOLL_CTL_ADD)時指定 (EPOLLIN|EPOLLOUT)。這樣可以避免反覆呼叫 epoll_ctl(2) 與EPOLL_CTL_MOD 在 EPOLLIN 和 EPOLLOUT 之間來回切換。
如果某個就緒的檔案可用的 I/O 空間很大,試圖窮盡它可能會導致其他檔案得不到處理,造成飢餓。(但這個問題並不是 epoll 特有的)。
解決方案是維護一個就緒列表,並在其關聯的資料結構中將此檔案描述符標記為就緒,從而使應用程式在記住哪些檔案需要被處理的同時仍能迴圈遍歷所有就緒的檔案。這也使你可以忽略收到的已經就緒的檔案描述符的後續事件。
如果你使用了事件快取或暫存了所有從 epoll_wait(2) 返回的檔案描述符,那麼一定要有某種方法來動態地標記這些檔案描述符的關閉(例如因先前的事件處理引起的檔案描述符關閉)。假設你從 epoll_wait(2) 收到了100個事件,在事件#47中,某個條件導致事件#13被關閉。如果你刪除資料結構並關閉(close(2))事件#13的檔案描述符,那麼你的事件快取可能仍然會說事件#13的檔案描述符有事件在等待而造成迷惑。
對應的一個解決方案是,在處理事件47的過程中,呼叫 epoll_ctl(EPOLL_CTL_DEL)來刪除並關閉(close(2))檔案描述符13,然後將其相關的資料結構標記為已刪除,並將其連結到一個清理列表。如果你在批處理中發現了檔案描述符13的另一個事件,你會發現檔案描述符13先前已被刪除,這樣就不會有任何混淆。
epoll API 在 Linux 核心2.5.44中引入。2.3.2版本的 glibc 加入了對其的支援。
epoll API 是 Linux 特有的。其他的一些系統也提供類似的機制,例如 FreeBSD有 kqueue, Solaris 有 /dev/poll。
可以透過程序對應的 /proc/[pid]/fdinfo 目錄下的 epoll 檔案描述符條目檢視epoll 檔案描述符所監視的檔案描述符的集合。詳情見 proc(5)。
kcmp(2) 的 KCMP_EPOLL_TFD 操作可以用來檢查一個 epoll 例項中是否存在某個檔案描述符。
epoll_create(2), epoll_create1(2), epoll_ctl(2), epoll_wait(2), poll(2), select(2)
本頁面中文版由中文
man 手冊頁計劃提供。
中文 man
手冊頁計劃:https://github.com/man-pages-zh/manpages-zh
2021-03-22 | Linux |