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)

如果讀取方添加 rfdepoll 接口時使用了 EPOLLET (邊緣觸發)標誌位,那麼縱使此刻文件輸入緩衝區中仍有可用的數據(剩餘的1 KB 數據),步驟5中的epoll_wait(2) 調用仍可能會掛起;與此同時,寫入方可能在等待讀取方對它發送的數據的響應。造成這種互相等待的情形的原因是邊緣觸發模式只有在被監控的文件描述符發生變化時纔會遞送事件。因此,在步驟5中,讀取方最終可能會爲一些已經存在於自己輸入緩衝區內的數據一直等下去。在上面的例子中,由於寫入方在第2步中進行了寫操作, rfd 上產生了一個事件,這個事件在第3步中被讀取方消耗了。但讀取方在第4步中進行的讀操作卻沒有消耗完整個緩衝區的數據,因此在第5步中對epoll_wait(2) 的調用可能會無限期地阻塞。

使用 EPOLLET 標誌位的應用程序應當使用非阻塞的文件描述符,以避免(因事件被消耗而)使正在處理多個文件描述符的任務因阻塞的讀或寫而出現飢餓。將 epoll用作邊緣觸發(EPOLLET)的接口,建議的使用方法如下:

a)
使用非阻塞的文件描述符;
b)
只在 read(2)write(2) 返回 EAGAIN 後再等待新的事件。

相較而言,當作爲水平觸發的接口使用時(默認情況,沒有指定 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 消耗的內核內存的量。
/proc/sys/fs/epoll/max_user_watches (從 Linux 2.6.28 開始)
此接口指定了單個用戶在系統內所有 epoll 實例中可以註冊的文件描述符的總數限制。這個限制是針對每個真實用戶ID的。每個註冊的文件描述符在32位內核上大約需要90個字節,在64位內核上大約需要160個字節。目前, max_user_watches 的默認值是可用低內存的1/25(4%)除以註冊的空間成本(以字節計)。

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_MODEPOLLINEPOLLOUT 之間來回切換。

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) 寫入時同理。(但如果你不能保證被監視的文件描述符總是指向一個面向流的文件,那麼就應當避免使用這一技巧)

o 邊緣觸發下的飢餓

如果某個就緒的文件可用的 I/O 空間很大,試圖窮盡它可能會導致其他文件得不到處理,造成飢餓。(但這個問題並不是 epoll 特有的)。

解決方案是維護一個就緒列表,並在其關聯的數據結構中將此文件描述符標記爲就緒,從而使應用程序在記住哪些文件需要被處理的同時仍能循環遍歷所有就緒的文件。這也使你可以忽略收到的已經就緒的文件描述符的後續事件。

o 如果使用了事件緩存...

如果你使用了事件緩存或暫存了所有從 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