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
- 相比之下,当用作水平触发接口时(默认设置,即未指定 EPOLLET 时),epoll 只是一个更快的 poll(2),并且由于它具有相同的语义,因此可以在后者使用的任何地方使用它。
由于即使使用边缘触发的 epoll,在接收到多块数据时也可能产生多个事件,调用者可以选择指定 EPOLLONESHOT 标志,以告知 epoll 在通过 epoll_wait(2) 接收到一个事件后禁用关联的文件描述符。指定 EPOLLONESHOT 标志后,调用者有责任使用带有 EPOLL_CTL_MOD 的 epoll_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_MOD 的 epoll_ctl(2) 在 EPOLLIN 和 EPOLLOUT 之间频繁切换。
问答
- 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_DUPFD 或 fork(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)