epoll(7) - Linux 手册页

名称

epoll - I/O 事件通知机制

概要

#include <sys/epoll.h>

描述

epoll API 执行的任务与 poll(2) 类似:监视多个文件描述符,以查看其中任何一个是否可以进行 I/O 操作。epoll API 既可以用作边缘触发(edge-triggered)接口,也可以用作水平触发(level-triggered)接口,并且能够很好地扩展到大量被监视的文件描述符。以下系统调用用于创建和管理 epoll 实例:

*

epoll_create(2) 创建一个 epoll 实例并返回指向该实例的文件描述符。(较新的 epoll_create1(2) 扩展了 epoll_create(2) 的功能。)

*

随后通过 epoll_ctl(2) 注册对特定文件描述符的兴趣。当前在 epoll 实例上注册的一组文件描述符有时被称为 epoll 集。

*

epoll_wait(2) 等待 I/O 事件,如果当前没有可用事件,则阻塞调用线程。

水平触发和边缘触发

epoll 事件分发接口既能够表现为边缘触发(ET),也能够表现为水平触发(LT)。这两种机制之间的区别可以描述如下。假设发生以下场景:
1.

代表管道读取端的文件描述符 (rfd) 被注册到 epoll 实例上。

2.

管道写入端在管道上写入了 2 kB 数据。

3.

调用 epoll_wait(2),它将返回 rfd 作为已就绪的文件描述符。

4.

管道读取端从 rfd 读取了 1 kB 数据。

5.

再次调用 epoll_wait(2)。

如果 rfd 文件描述符是通过 EPOLLET(边缘触发)标志添加到 epoll 接口中的,那么在步骤 5 中进行的 epoll_wait(2) 调用很可能会挂起,尽管文件输入缓冲区中仍然存在可用数据;与此同时,远程对端可能正在等待基于它已发送数据的响应。原因在于,边缘触发模式仅在受监视的文件描述符发生变化时才会分发事件。因此,在步骤 5 中,调用者可能会最终等待已经存在于输入缓冲区中的某些数据。在上述示例中,由于步骤 2 中的写入操作,rfd 上会产生一个事件,并且该事件在步骤 3 中被消耗。由于步骤 4 中的读取操作并没有消耗缓冲区中的全部数据,因此步骤 5 中的 epoll_wait(2) 调用可能会无限期阻塞。

使用 EPOLLET 标志的应用程序应使用非阻塞文件描述符,以避免阻塞读或写导致处理多个文件描述符的任务发生饥饿。建议将 epoll 作为边缘触发 (EPOLLET) 接口使用的方式如下:

i

使用非阻塞文件描述符;以及

ii

仅在 read(2) 或 write(2) 返回 EAGAIN 后才等待事件。

相比之下,当用作水平触发接口时(默认设置,即未指定 EPOLLET 时),epoll 只是一个更快的 poll(2),并且由于它具有相同的语义,因此可以在后者使用的任何地方使用它。

由于即使使用边缘触发的 epoll,在接收到多块数据时也可能产生多个事件,调用者可以选择指定 EPOLLONESHOT 标志,以告知 epoll 在通过 epoll_wait(2) 接收到一个事件后禁用关联的文件描述符。指定 EPOLLONESHOT 标志后,调用者有责任使用带有 EPOLL_CTL_MODepoll_ctl(2) 来重新武装(rearm)该文件描述符。

/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 作为水平触发接口使用时与 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;

/* Set up listening socket, 'listen_sock' (socket(),
   bind(), listen()) */

epollfd = epoll_create(10);
if (epollfd == -1) {
    perror("epoll_create");
    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_pwait");
        exit(EXIT_FAILURE);
    }

   for (n = 0; n < nfds; ++n) {
        if (events[n].data.fd == listen_sock) {
            conn_sock = accept(listen_sock,
                            (struct sockaddr *) &local, &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);
        }
    }
}
当作为边缘触发接口使用时,出于性能考虑,可以通过指定 (EPOLLIN|EPOLLOUT) 将文件描述符一次性添加到 epoll 接口中 (EPOLL_CTL_ADD)。这样可以避免通过调用带有 EPOLL_CTL_MODepoll_ctl(2) 在 EPOLLINEPOLLOUT 之间频繁切换。

问答

Q0

用于区分 epoll 集中注册的文件描述符的键是什么?

A0

该键是文件描述符编号和打开文件描述(open file description,也称为“打开文件句柄”,即内核内部对打开文件的表示)的组合。

Q1

如果将同一个文件描述符在同一个 epoll 实例上注册两次会发生什么?

A1

你很可能会收到 EEXIST 错误。但是,将重复的描述符(dup(2), dup2(2), fcntl(2) F_DUPFD)添加到同一个 epoll 实例是可能的。如果这些重复的文件描述符注册了不同的 events 掩码,这是一种过滤事件的有用技术。

Q2

两个 epoll 实例可以同时等待同一个文件描述符吗?如果是,事件会报告给两个 epoll 文件描述符吗?

A2

是的,事件会报告给两者。不过,要正确实现这一点可能需要仔细的编程。

Q3

epoll 文件描述符本身支持 poll/epoll/selectable 吗?

A3

是的。如果 epoll 文件描述符有待处理的事件,它会指示为可读。

Q4

如果尝试将 epoll 文件描述符放入其自身的描述符集中会发生什么?

A4

epoll_ctl(2) 调用将失败 (EINVAL)。但是,你可以将一个 epoll 文件描述符添加到另一个 epoll 文件描述符集中。

Q5

可以将 epoll 文件描述符通过 UNIX 域套接字发送到另一个进程吗?

A5

可以,但这样做没有意义,因为接收进程不会拥有 epoll 集中文件描述符的副本。

Q6

关闭文件描述符会自动将其从所有 epoll 集中移除吗?

A6

是的,但请注意以下几点。文件描述符是对打开文件描述的引用(参见 open(2))。每当通过 dup(2)、dup2(2)、fcntl(2) F_DUPFDfork(2) 复制描述符时,都会创建一个指向相同打开文件描述的新文件描述符。打开文件描述会一直存在,直到所有指向它的文件描述符都被关闭。只有在所有指向底层打开文件描述的文件描述符都被关闭(或者如果描述符通过 epoll_ctl(2) EPOLL_CTL_DEL 显式移除)后,文件描述符才会被从 epoll 集中移除。这意味着即使在 epoll 集中的一个文件描述符被关闭后,如果仍有其他指向相同底层文件描述的文件描述符打开,该文件描述符的事件可能仍会被报告。

Q7

如果在 epoll_wait(2) 调用之间发生了多个事件,它们是被合并还是单独报告?

A7

它们会被合并。

Q8

对文件描述符的操作会影响已经收集但尚未报告的事件吗?

A8

你可以对现有的文件描述符执行两种操作。移除(Remove)在这种情况下没有意义。修改(Modify)将重新读取可用的 I/O。

Q9

使用 EPOLLET 标志(边缘触发行为)时,我需要持续读/写文件描述符直到返回 EAGAIN 吗?

A9

epoll_wait(2) 接收到一个事件应该提示你该文件描述符已为请求的 I/O 操作做好准备。你必须将其视为就绪,直到下一次(非阻塞)读/写操作返回 EAGAIN。你何时以及如何使用该文件描述符完全由你决定。

对于面向数据包/令牌的文件(例如,数据报套接字、规范模式下的终端),检测读/写 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 的文件描述符,那么你的事件缓存可能仍然显示该文件描述符有待处理的事件,从而造成混淆。

一种解决方法是在处理事件 47 时,调用 epoll_ctl(EPOLL_CTL_DEL) 删除文件描述符 13 并 close(2),然后将其关联的数据结构标记为已移除并链接到清理列表。如果你在批量处理中发现了事件 13 的另一个事件,你会发现该文件描述符之前已被移除,因此不会造成混淆。

版本

epoll API 在 Linux 内核 2.5.44 中引入。glibc 对其的支持在 2.3.2 版本中添加。

符合

epoll API 是 Linux 专有的。其他一些系统提供了类似的机制,例如 FreeBSD 有 kqueue,Solaris 有 /dev/poll

参见

epoll_create(2), epoll_create1(2), epoll_ctl(2), epoll_wait(2)

引用自

capabilities(7), ev(3), eventfd(2), inotify(7), mq_overview(7), perfmonctl(2), pipe(7), proc(5), select(2), select_tut(2), signalfd(2), timerfd_create(2), udp(7)