linux手冊翻譯——epoll(7)

\color{#A00000}{NAME}
epoll — I/O 事件通知機(jī)制

\color{#A00000}{SYNOPSIS}

# include<sys/epoll.h>

\color{#A00000}{DESCRIPTION}
epoll API與poll具有相同功能:監(jiān)視多個(gè)文件描述符,以查看這些文件描述符中任何一個(gè)上可以進(jìn)行特定的I/O操作,如是否可讀/可寫。epoll API可以使用edge-triggered和level-triggered兩種接口,并且可以高性能的同時(shí)監(jiān)視大量的fd,這是對epoll相對魚poll的核心優(yōu)勢。

epoll的核心概念是epoll instance,這是一種內(nèi)核數(shù)據(jù)結(jié)構(gòu),從用戶空間角度看,可以視為一個(gè)包含兩種列表的容器:

  • interest列表:進(jìn)程已注冊的要監(jiān)視的fd集合。
  • ready 列表: "I/O準(zhǔn)備好”的fd集(所謂I/O準(zhǔn)備好可簡單的理解為對應(yīng)fd可讀了或可寫了),ready列表是interest列表的子集(準(zhǔn)確的說是一組引用)。ready 列表是動態(tài)變化的,由內(nèi)核動態(tài)填充。

提供以下3個(gè)系統(tǒng)調(diào)用來創(chuàng)建和管理epoll instance:

  • epoll_create(2) :創(chuàng)建一個(gè)epoll instance 并返回一個(gè)文件描述符,指向?qū)嵗?/li>
  • epoll_ctl(2):將要監(jiān)聽的fd添加到epoll instance的interest列表中
  • epoll_wait(2):等待I/O事件,如果當(dāng)前沒有可用的事件,則阻塞調(diào)用線程。(epoll_wait從epoll instance的ready列表中獲取產(chǎn)生事件的fd,若ready列表為空,那么就阻塞。)

兩種觸發(fā)模式:level_triggered (LT)和 edge_triggered(ET)
假設(shè)發(fā)生如下場景:

  1. 代表管讀取端的文件描述符(rfd)注冊在epoll實(shí)例上;
  2. 管道的寫入端寫入2KB的數(shù)據(jù);
  3. 調(diào)用epoll_wait將返回rfd的文件描述符。(因?yàn)閷懭攵藢懭霐?shù)據(jù)后,讀取端就可讀了,即發(fā)生了可讀I/O事件);
  4. 管道讀端讀取1KB數(shù)據(jù);
  5. 再次調(diào)用epoll-wait。

如果使用ET觸發(fā),那么步驟5就會阻塞掛起,這是因?yàn)閷τ贓T模式而言,只有當(dāng)緩沖區(qū)數(shù)據(jù)發(fā)生變化時(shí)才會觸發(fā)事件(對于讀,“變化”指新數(shù)據(jù)到達(dá))。而對于LT而言,只要緩沖區(qū)中存在數(shù)據(jù),就會一直觸發(fā)。

使用ET時(shí)應(yīng)使用非阻塞的fd (即無法讀寫時(shí)返回EAGIN,而非阻塞),以避免task阻塞導(dǎo)教其他fd無法監(jiān)控。
合理使用ET模式步驟:
1)修改fd為非阻塞(non-blocking)
2)在read或write操作返回EAGIN后再執(zhí)行wait等待事件。
為何ET需要非阻塞呢?因?yàn)镋T模式下要循環(huán)多次read,并通過阻塞(即是否返回EAGIN)來確定數(shù)據(jù)是否全部讀完。第一次執(zhí)行read是不可能阻塞的。

若使用LT模式(默認(rèn)情況下,使用ET模式),則可以將epoll看作是一個(gè)快速的poll,可以在任何地方使用epoll(LT)替換poll,因?yàn)樗麄兊恼Z義完全相同。
即使采用ET模式,在多線程的情況依然會導(dǎo)致產(chǎn)生多個(gè)事件(對于同一被監(jiān)控的fd),這將導(dǎo)致多個(gè)線程操作同一fd,可以使用EPOLLNESHOT標(biāo)志避免,即在一次wait返回后禁止fd再產(chǎn)生事件,并在處理完成后使用epoll_ctl的MOD操作重新開啟。
在多進(jìn)程或多線程中,epoll_fd是共享的,這將導(dǎo)致所有線程都會知道事情的發(fā)生,但是epoll僅會喚醒一個(gè)線程,以規(guī)避“群驚”現(xiàn)象。

Interaction with autosleep

If the system is in autosleep mode via /sys/power/autosleep and an event happens which wakes the device from sleep, the device driver will keep the device awake only until that event is queued. To keep the device awake until the event has been processed, it is necessary to use the epoll_ctl(2) EPOLLWAKEUP flag.

When the EPOLLWAKEUP flag is set in the events field for a struct epoll_event, the system will be kept awake from the moment the event is queued, through the epoll_wait(2) call which returns the event until the subsequent epoll_wait(2) call. If the event should keep the system awake beyond that time, then a separate wake_lock should be taken before the second epoll_wait(2) call.

/proc interfaces

以下接口可用于限制 epoll 消耗的內(nèi)核內(nèi)存用量:

  • /proc/sys/fs/epoll/max_user_watch 配置了每一個(gè)用戶ID在所有的epoll實(shí)例中所能注冊監(jiān)聽的fd最大值,注意不是每個(gè)epoll 實(shí)例,是該用戶ID的所有epoll實(shí)例。每個(gè)注冊的fd在32 位內(nèi)核上花費(fèi)大約 90 字節(jié),在 64 位內(nèi)核上花費(fèi)大約 160 字節(jié)。目前,max_user_watches 的默認(rèn)值是 available low memory 的 1/25 (4%) 除以fd的內(nèi)存占用。
Example for suggested usage

雖然 epoll 在用作級別觸發(fā)接口時(shí)具有與 poll(2) 相同的語義,但邊緣觸發(fā)的用法需要更多說明以避免應(yīng)用程序事件循環(huán)中的阻塞。
在下面例子中,listener 是一個(gè)非阻塞套接字,在它上面調(diào)用了 listen(2)。 函數(shù) do_use_fd() 使用新的就緒文件描述符,直到 read(2) 或 write(2) 返回 EAGAIN。 事件驅(qū)動的狀態(tài)機(jī)應(yīng)用程序應(yīng)該在收到 EAGAIN 后記錄其當(dāng)前狀態(tài),以便在下一次調(diào)用 do_use_fd() 時(shí),它將繼續(xù)從之前停止的位置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);
        }
    }
}

當(dāng)使用ET模式時(shí),出于性能原因,可以通過EPOLL_CTL_ADD調(diào)用 epoll_ctl(2)指定 (EPOLLIN|EPOLLOUT)添加一次文件描述符。 避免使用 EPOLL_CTL_MOD 調(diào)用 epoll_ctl(2)在 EPOLLIN 和 EPOLLOUT 之間連續(xù)切換。

Questions and answers
  1. What is the key used to distinguish the file descriptors registered in an interest list?
    The key is the combination of the file descriptor number and the open file description (also known as an "open file handle", the kernel's internal representation of an open file).
  2. 如果在 epoll 實(shí)例上注冊相同的文件描述符兩次會發(fā)生什么?
    你可能會得到 EEXIST。 但是,可以向同一個(gè) epoll 實(shí)例添加重復(fù)的(dup(2)、dup2(2)、fcntl(2) F_DUPFD)文件描述符。 如果重復(fù)的文件描述符使用不同的事件掩碼注冊,這可能是過濾事件的有用技術(shù)。
  3. 兩個(gè) epoll 實(shí)例可以等待同一個(gè)文件描述符嗎? 如果是這樣,事件是否報(bào)告給兩個(gè) epoll 文件描述符?
    是的,事件將報(bào)告給兩者。 但是,可能需要仔細(xì)編程才能正確執(zhí)行此操作。
  4. epoll 文件描述符本身是 poll/epoll/selectable 嗎?
    是的。 如果 epoll 文件描述符有事件等待,那么它將指示為可讀。
  5. 如果試圖將一個(gè) epoll 文件描述符放入它自己的文件描述符集中會發(fā)生什么?
    epoll_ctl(2) 調(diào)用失敗 (EINVAL)。 但是可以在另一個(gè) epoll 文件描述符集中添加一個(gè) epoll 文件描述符。
  6. 我可以通過 UNIX 域套接字將 epoll 文件描述符發(fā)送到另一個(gè)進(jìn)程嗎?
    是的,但這樣做沒有意義,因?yàn)榻邮者M(jìn)程不會在興趣列表中擁有文件描述符的副本。
  7. 關(guān)閉文件描述符會導(dǎo)致它從所有 epoll 興趣列表中刪除嗎?
    是的,但請注意以下幾點(diǎn)。文件描述符是對打開文件(open file description)的引用(請參閱 open(2))。每當(dāng)通過 dup(2)、dup2(2)、fcntl(2)、F_DUPFD 或 fork(2) 復(fù)制文件描述符時(shí),都會創(chuàng)建一個(gè)引用相同打開文件的新文件描述符。一個(gè)打開文件會一直存在,直到所有引用它的文件描述符都被關(guān)閉。
    只有在所有引用底層打開文件的文件描述符都已關(guān)閉后,才會從興趣列表中刪除文件描述符。這意味著即使在作為興趣列表一部分的文件描述符已經(jīng)關(guān)閉之后,如果引用相同底層文件描述的其他文件描述符保持打開,則可能針對該文件描述符報(bào)告事件。為了防止這種情況發(fā)生,文件描述符必須在復(fù)制之前從興趣列表中明確刪除(使用 epoll_ctl(2) EPOLL_CTL_DEL)?;蛘?,應(yīng)用程序必須確保關(guān)閉所有文件描述符(如果使用 dup(2) 或 fork(2) 的庫函數(shù)在幕后復(fù)制文件描述符,這可能會很困難)。
  8. 如果在 epoll_wait(2) 調(diào)用之間發(fā)生了多個(gè)事件,它們是合并還是單獨(dú)報(bào)告?
    合并
  9. 對文件描述符的操作是否會影響已收集但尚未報(bào)告的事件?
    You can do two operations on an existing file descriptor. Remove would be meaningless for this case. Modify will reread available I/O.
    10.使用ET時(shí),是否需要連續(xù)讀/寫文件描述符直到 EAGAIN?
    從 epoll_wait(2) 接收事件應(yīng)該會提示您此類文件描述符已準(zhǔn)備好用于請求的 I/O 操作。您必須認(rèn)為它已準(zhǔn)備就緒,直到下一次(非阻塞)讀/寫產(chǎn)生 EAGAIN。何時(shí)以及如何使用文件描述符完全取決于您。
    對于面向數(shù)據(jù)包/token-oriented files(例如,數(shù)據(jù)報(bào)套接字、terminal in canonical mode),檢測讀/寫 I/O 空間結(jié)束的唯一方法是繼續(xù)讀/寫直到 EAGAIN。
    對于面向流的文件(例如管道、FIFO、流套接字),還可以通過檢查從目標(biāo)文件描述符讀取/寫入的數(shù)據(jù)量來檢測讀/寫I/O空間耗盡的情況。例如,如果您通過要求讀取一定數(shù)量的數(shù)據(jù)來調(diào)用 read(2) 并且 read(2) 返回較少的字節(jié)數(shù),則可以確定已用盡文件描述符的讀取 I/O 空間。使用 write(2) 寫入時(shí)也是如此。 (如果您不能保證受監(jiān)視的文件描述符總是指向面向流的文件,請避免使用后一種技術(shù)。)
可能的陷阱和避免它們的方法
  • Starvation (edge-triggered)
    如果有大量 I/O 空間,則有可能通過嘗試將其排空,其他文件將不會得到處理,從而導(dǎo)致饑餓。 (這個(gè)問題不是 epoll 特有的。)
    解決方案是維護(hù)一個(gè)就緒列表,并在其關(guān)聯(lián)的數(shù)據(jù)結(jié)構(gòu)中將文件描述符標(biāo)記為就緒,從而允許應(yīng)用程序記住哪些文件需要處理但仍然在所有就緒文件中循環(huán)。 這也支持忽略您收到的已準(zhǔn)備好的文件描述符的后續(xù)事件。
  • If using an event cache...
    如果您使用事件緩存或存儲從 epoll_wait(2) 返回的所有文件描述符,那么請確保提供一種方法來動態(tài)標(biāo)記其關(guān)閉(即,由先前事件的處理引起)。 假設(shè)您從 epoll_wait(2) 收到 100 個(gè)事件,并且在事件 #47 中,一個(gè)條件導(dǎo)致事件 #13 關(guān)閉。 如果您移除結(jié)構(gòu)并關(guān)閉(2) 事件#13 的文件描述符,那么您的事件緩存可能仍會顯示有事件在等待該文件描述符導(dǎo)致混淆。
    對此的一種解決方案是,在處理事件 47 期間調(diào)用 epoll_ctl(EPOLL_CTL_DEL) 刪除文件描述符 13 和 close(2),然后將其關(guān)聯(lián)的數(shù)據(jù)結(jié)構(gòu)標(biāo)記為已刪除并將其鏈接到清理列表。 如果您在批處理中發(fā)現(xiàn)文件描述符 13 的另一個(gè)事件,您會發(fā)現(xiàn)文件描述符之前已被刪除,并且不會出現(xiàn)混淆。
    \color{#A00000}{VERSIONS}
    epoll API 是在 Linux 內(nèi)核 2.5.44 中引入的。 在 2.3.2 版中添加了對 glibc 的支持。

\color{#A00000}{CONFORMING TO}
The epoll API is Linux-specific. Some other systems provide similar mechanisms, for example, FreeBSD has kqueue, and Solaris has /dev/poll.

\color{#A00000}{NOTES}
通過 epoll 文件描述符監(jiān)視的文件描述符集可以通過進(jìn)程的 /proc/[pid]/fdinfo 目錄中的 epoll 文件描述符條目查看。 有關(guān)更多詳細(xì)信息,請參閱 proc(5)。

kcmp(2) KCMP_EPOLL_TFD 操作可用于測試文件描述符是否存在于 epoll 實(shí)例中。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容