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