linux手册翻译——epoll(7)

\color{#A00000}{NAME}
epoll — I/O 事件通知机制

\color{#A00000}{SYNOPSIS}

# include<sys/epoll.h>

\color{#A00000}{DESCRIPTION}
epoll API与poll具有相同功能:监视多个文件描述符,以查看这些文件描述符中任何一个上可以进行特定的I/O操作,如是否可读/可写。epoll API可以使用edge-triggered和level-triggered两种接口,并且可以高性能的同时监视大量的fd,这是对epoll相对鱼poll的核心优势。

epoll的核心概念是epoll instance,这是一种内核数据结构,从用户空间角度看,可以视为一个包含两种列表的容器:

  • interest列表:进程已注册的要监视的fd集合。
  • ready 列表: "I/O准备好”的fd集(所谓I/O准备好可简单的理解为对应fd可读了或可写了),ready列表是interest列表的子集(准确的说是一组引用)。ready 列表是动态变化的,由内核动态填充。

提供以下3个系统调用来创建和管理epoll instance:

  • epoll_create(2) :创建一个epoll instance 并返回一个文件描述符,指向实例
  • epoll_ctl(2):将要监听的fd添加到epoll instance的interest列表中
  • epoll_wait(2):等待I/O事件,如果当前没有可用的事件,则阻塞调用线程。(epoll_wait从epoll instance的ready列表中获取产生事件的fd,若ready列表为空,那么就阻塞。)

两种触发模式:level_triggered (LT)和 edge_triggered(ET)
假设发生如下场景:

  1. 代表管读取端的文件描述符(rfd)注册在epoll实例上;
  2. 管道的写入端写入2KB的数据;
  3. 调用epoll_wait将返回rfd的文件描述符。(因为写入端写入数据后,读取端就可读了,即发生了可读I/O事件);
  4. 管道读端读取1KB数据;
  5. 再次调用epoll-wait。

如果使用ET触发,那么步骤5就会阻塞挂起,这是因为对于ET模式而言,只有当缓冲区数据发生变化时才会触发事件(对于读,“变化”指新数据到达)。而对于LT而言,只要缓冲区中存在数据,就会一直触发。

使用ET时应使用非阻塞的fd (即无法读写时返回EAGIN,而非阻塞),以避免task阻塞导教其他fd无法监控。
合理使用ET模式步骤:
1)修改fd为非阻塞(non-blocking)
2)在read或write操作返回EAGIN后再执行wait等待事件。
为何ET需要非阻塞呢?因为ET模式下要循环多次read,并通过阻塞(即是否返回EAGIN)来确定数据是否全部读完。第一次执行read是不可能阻塞的。

若使用LT模式(默认情况下,使用ET模式),则可以将epoll看作是一个快速的poll,可以在任何地方使用epoll(LT)替换poll,因为他们的语义完全相同。
即使采用ET模式,在多线程的情况依然会导致产生多个事件(对于同一被监控的fd),这将导致多个线程操作同一fd,可以使用EPOLLNESHOT标志避免,即在一次wait返回后禁止fd再产生事件,并在处理完成后使用epoll_ctl的MOD操作重新开启。
在多进程或多线程中,epoll_fd是共享的,这将导致所有线程都会知道事情的发生,但是epoll仅会唤醒一个线程,以规避“群惊”现象。

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 消耗的内核内存用量:

  • /proc/sys/fs/epoll/max_user_watch 配置了每一个用户ID在所有的epoll实例中所能注册监听的fd最大值,注意不是每个epoll 实例,是该用户ID的所有epoll实例。每个注册的fd在32 位内核上花费大约 90 字节,在 64 位内核上花费大约 160 字节。目前,max_user_watches 的默认值是 available low memory 的 1/25 (4%) 除以fd的内存占用。
Example for suggested usage

虽然 epoll 在用作级别触发接口时具有与 poll(2) 相同的语义,但边缘触发的用法需要更多说明以避免应用程序事件循环中的阻塞。
在下面例子中,listener 是一个非阻塞套接字,在它上面调用了 listen(2)。 函数 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);
        }
    }
}

当使用ET模式时,出于性能原因,可以通过EPOLL_CTL_ADD调用 epoll_ctl(2)指定 (EPOLLIN|EPOLLOUT)添加一次文件描述符。 避免使用 EPOLL_CTL_MOD 调用 epoll_ctl(2)在 EPOLLIN 和 EPOLLOUT 之间连续切换。

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 实例上注册相同的文件描述符两次会发生什么?
    你可能会得到 EEXIST。 但是,可以向同一个 epoll 实例添加重复的(dup(2)、dup2(2)、fcntl(2) F_DUPFD)文件描述符。 如果重复的文件描述符使用不同的事件掩码注册,这可能是过滤事件的有用技术。
  3. 两个 epoll 实例可以等待同一个文件描述符吗? 如果是这样,事件是否报告给两个 epoll 文件描述符?
    是的,事件将报告给两者。 但是,可能需要仔细编程才能正确执行此操作。
  4. epoll 文件描述符本身是 poll/epoll/selectable 吗?
    是的。 如果 epoll 文件描述符有事件等待,那么它将指示为可读。
  5. 如果试图将一个 epoll 文件描述符放入它自己的文件描述符集中会发生什么?
    epoll_ctl(2) 调用失败 (EINVAL)。 但是可以在另一个 epoll 文件描述符集中添加一个 epoll 文件描述符。
  6. 我可以通过 UNIX 域套接字将 epoll 文件描述符发送到另一个进程吗?
    是的,但这样做没有意义,因为接收进程不会在兴趣列表中拥有文件描述符的副本。
  7. 关闭文件描述符会导致它从所有 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) 的库函数在幕后复制文件描述符,这可能会很困难)。
  8. 如果在 epoll_wait(2) 调用之间发生了多个事件,它们是合并还是单独报告?
    合并
  9. 对文件描述符的操作是否会影响已收集但尚未报告的事件?
    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时,是否需要连续读/写文件描述符直到 EAGAIN?
    从 epoll_wait(2) 接收事件应该会提示您此类文件描述符已准备好用于请求的 I/O 操作。您必须认为它已准备就绪,直到下一次(非阻塞)读/写产生 EAGAIN。何时以及如何使用文件描述符完全取决于您。
    对于面向数据包/token-oriented files(例如,数据报套接字、terminal in canonical mode),检测读/写 I/O 空间结束的唯一方法是继续读/写直到 EAGAIN。
    对于面向流的文件(例如管道、FIFO、流套接字),还可以通过检查从目标文件描述符读取/写入的数据量来检测读/写I/O空间耗尽的情况。例如,如果您通过要求读取一定数量的数据来调用 read(2) 并且 read(2) 返回较少的字节数,则可以确定已用尽文件描述符的读取 I/O 空间。使用 write(2) 写入时也是如此。 (如果您不能保证受监视的文件描述符总是指向面向流的文件,请避免使用后一种技术。)
可能的陷阱和避免它们的方法
  • Starvation (edge-triggered)
    如果有大量 I/O 空间,则有可能通过尝试将其排空,其他文件将不会得到处理,从而导致饥饿。 (这个问题不是 epoll 特有的。)
    解决方案是维护一个就绪列表,并在其关联的数据结构中将文件描述符标记为就绪,从而允许应用程序记住哪些文件需要处理但仍然在所有就绪文件中循环。 这也支持忽略您收到的已准备好的文件描述符的后续事件。
  • If using an event cache...
    如果您使用事件缓存或存储从 epoll_wait(2) 返回的所有文件描述符,那么请确保提供一种方法来动态标记其关闭(即,由先前事件的处理引起)。 假设您从 epoll_wait(2) 收到 100 个事件,并且在事件 #47 中,一个条件导致事件 #13 关闭。 如果您移除结构并关闭(2) 事件#13 的文件描述符,那么您的事件缓存可能仍会显示有事件在等待该文件描述符导致混淆。
    对此的一种解决方案是,在处理事件 47 期间调用 epoll_ctl(EPOLL_CTL_DEL) 删除文件描述符 13 和 close(2),然后将其关联的数据结构标记为已删除并将其链接到清理列表。 如果您在批处理中发现文件描述符 13 的另一个事件,您会发现文件描述符之前已被删除,并且不会出现混淆。
    \color{#A00000}{VERSIONS}
    epoll API 是在 Linux 内核 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 文件描述符监视的文件描述符集可以通过进程的 /proc/[pid]/fdinfo 目录中的 epoll 文件描述符条目查看。 有关更多详细信息,请参阅 proc(5)。

kcmp(2) KCMP_EPOLL_TFD 操作可用于测试文件描述符是否存在于 epoll 实例中。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 218,122评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 93,070评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,491评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,636评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,676评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,541评论 1 305
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,292评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,211评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,655评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,846评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,965评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,684评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,295评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,894评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,012评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,126评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,914评论 2 355

推荐阅读更多精彩内容