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),這是核心的一個內部資料結構,從使用者空間的角度看,它可以被看作一個內含兩個列表的容器:
- 興趣列表(interest list,有時也稱為 epoll 集(epoll set)):程序註冊了“監控興趣”的檔案描述符的集合。
- 就緒列表(ready list):“準備好”進行 I/O 的檔案描述符的集合。就緒列表是興趣列表中的檔案描述符的子集(或者更準確地說,是其引用的集合)。核心會根據這些檔案描述符上的 I/O 活動動態地填充就緒列表。
下列系統呼叫可用於建立和管理 epoll 例項:
- epoll_create(2) 會建立一個新的 epoll 例項,並返回一個指向該例項的檔案描述符。(最新的 epoll_create1(2) 擴充套件了 epoll_create(2) 的功能。)
- epoll_ctl(2) 能向 epoll 例項的興趣列表中新增專案,註冊對特定檔案描述符的興趣。
- epoll_wait(2) 會等待 I/O 事件,如果當前沒有事件可用,則阻塞呼叫它的執行緒。(此係統呼叫可被看作從 epoll 例項的就緒列表中獲取專案。)
水平觸發與邊緣觸發¶
epoll 事件的分發介面既可以表現為邊緣觸發(ET),也可以表現為水平觸發(LT)。這兩種機制的區別描述如下。假設發生下列情況:
- 1.
- 讀取方在 epoll 例項中註冊代表管道讀取端(rfd)的檔案描述符。
- 2.
- 寫入方在管道的寫入端寫入 2 kB 的資料。
- 3.
- 讀取方呼叫 epoll_wait(2), rfd 作為一個就緒的檔案描述符被返回。
- 4.
- 讀取方只從 rfd 中讀取 1 kB 的資料。
- 5.
- 讀取方再次呼叫 epoll_wait(2)。
如果讀取方新增 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。
/proc 介面¶
以下介面可以用來限制 epoll 消耗的核心記憶體的量。
- /proc/sys/fs/epoll/max_user_watches (從 Linux 2.6.28 開始)
- 此介面指定了單個使用者在系統內所有 epoll 例項中可以註冊的檔案描述符的總數限制。這個限制是針對每個真實使用者ID的。每個註冊的檔案描述符在32位核心上大約需要90個位元組,在64位核心上大約需要160個位元組。目前, max_user_watches 的預設值是可用低記憶體的1/25(4%)除以註冊的空間成本(以位元組計)。
示例:建議的使用 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 之間來回切換。
epoll 十問¶
- 0.
- 用什麼區分興趣列表中註冊的檔案描述符?
- 檔案描述符的數值和開啟檔案描述(open file description,又稱“open file handle”,核心對開啟的檔案的內部表示)的組合。
- 1.
- 如果在同一個 epoll 例項上多次註冊相同的檔案描述符會怎樣?
- 你可能會得到 EEXIST。然而,在同一個epoll例項上新增重複的(dup(2),dup2(2), fcntl(2) F_DUPFD)檔案描述符是可能的。如果重複的檔案描述符是用不同的事件掩碼(events mask)註冊的,那麼這會成為過濾事件的一個實用技巧。
- 2.
- 多個 epoll 例項能等待同一個檔案描述符嗎?如果可以,事件會被報告給所有的這些epoll 檔案描述符嗎?
- 能,而且事件會被報告給所有的例項。但你可能需要小心仔細地程式設計才能正確地實現這一點。
- 3.
- epoll 檔案描述符本身 poll/epoll/selectable 嗎?
- 是的,如果一個 epoll 檔案描述符有事件在等待,那麼它將顯示為可讀。
- 4.
- 如果試圖把 epoll 檔案描述符放到它自己的檔案描述符集合中會發生什麼?
- epoll_ctl(2) 呼叫會失敗(EINVAL)。但你可以將一個 epoll 檔案描述符新增到另一個 epoll 檔案描述符集合中。
- 5.
- 我可以透過 UNIX 域套接字傳送一個 epoll 檔案描述符到另一個程序嗎?
- 可以,但這樣做是沒有意義的,因為接收程序不會得到興趣列表中檔案描述符的副本。
- 6.
- 關閉一個檔案描述符會將它從所有 epoll 興趣列表中移除嗎?
- 會,但要注意幾點。檔案描述符是對開啟檔案描述(open file description)的引用(見 open(2))。每當透過 dup(2), dup2(2), fcntl(2) F_DUPFD,或 fork(2) 複製某個檔案描述符時,都會建立一個新的檔案描述符,引用同一個開啟檔案描述。一個開啟檔案描述會在所有引用它的檔案描述符被關閉之前一直存在。
- 一個檔案描述符只有在所有指向其依賴的開啟檔案描述的檔案描述符都被關閉後才會從興趣列表中移除。這意味著,即使興趣列表內的某個檔案描述符被關閉了,如果引用同一檔案描述的其他檔案描述符仍然開著,則該檔案描述符的事件仍可能會通知。為了防止這種情況發生,在複製檔案描述符前,必須顯式地將其從興趣列表中移除(使用epoll_ctl(2) EPOLL_CTL_DEL)。或者應用程式必須能確保所有的檔案描述符都被關閉(如果檔案描述符是被使用 dup(2) 或 fork(2) 的庫函式隱式複製的,這一點可能會很難保證)。
- 7.
- 如果在兩次 epoll_wait(2) 呼叫之間發生了不止一個事件,它們是會一起報告還是會分開報告?
- 它們會一起報告。
- 8.
- 對檔案描述符的操作會影響已經收集到但尚未報告的事件嗎?
- 你可以對某個現有的檔案描述符做刪除和修改兩種操作:刪除,對這種情況沒有意義;修改,將重新讀取可用的 I/O。
- 9.
- 當使用 EPOLLET 標誌位(邊緣觸發行為)時,我需要持續讀/寫檔案描述符,直到EAGAIN 嗎?
- 從 epoll_wait(2) 收到的事件會提示你,對應的檔案描述符已經準備好進行所要求的I/O 操作。直到下一次(非阻塞的)讀/寫產生 EAGAIN 之前,此檔案描述符都應被認為是就緒的。何時及如何使用該檔案描述符完全取決於你。
- 對於面向資料包/令牌的檔案(如資料報套接字、典型模式(canonical mode)下的終端),感知讀/寫 I/O 空間盡頭的唯一方法是持續讀/寫直到 EAGAIN。
- 對於面向流的檔案(如管道、FIFO、流套接字),也可透過檢查從目標檔案描述符讀/寫的資料量來檢測讀/寫 I/O 空間消費完的情況。例如,如果你在呼叫 read(2) 時指定了期望讀取的位元組數,但 read(2) 返回的實際讀取位元組數較少,你就可以確定檔案描述符的讀 I/O 空間已經消費完了。在使用 write(2) 寫入時同理。(但如果你不能保證被監視的檔案描述符總是指向一個面向流的檔案,那麼就應當避免使用這一技巧)
可能的陷阱和避免的方法¶
如果某個就緒的檔案可用的 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 |