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),这是内核的一个内部数据结构,从用户空间的角度看,它可以被看作一个内含两个列表的容器:
下列系统调用可用于创建和管理 epoll 实例:
epoll 事件的分发接口既可以表现为边缘触发(ET),也可以表现为水平触发(LT)。这两种机制的区别描述如下。假设发生下列情况:
如果读取方添加 rfd 到 epoll 接口时使用了 EPOLLET (边缘触发)标志位,那么纵使此刻文件输入缓冲区中仍有可用的数据(剩余的1 KB 数据),步骤5中的epoll_wait(2) 调用仍可能会挂起;与此同时,写入方可能在等待读取方对它发送的数据的响应。造成这种互相等待的情形的原因是边缘触发模式只有在被监控的文件描述符发生变化时才会递送事件。因此,在步骤5中,读取方最终可能会为一些已经存在于自己输入缓冲区内的数据一直等下去。在上面的例子中,由于写入方在第2步中进行了写操作, rfd 上产生了一个事件,这个事件在第3步中被读取方消耗了。但读取方在第4步中进行的读操作却没有消耗完整个缓冲区的数据,因此在第5步中对epoll_wait(2) 的调用可能会无限期地阻塞。
使用 EPOLLET 标志位的应用程序应当使用非阻塞的文件描述符,以避免(因事件被消耗而)使正在处理多个文件描述符的任务因阻塞的读或写而出现饥饿。将 epoll用作边缘触发(EPOLLET)的接口,建议的使用方法如下:
相较而言,当作为水平触发的接口使用时(默认情况,没有指定 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 消耗的内核内存的量。
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_MOD 在 EPOLLIN 和 EPOLLOUT 之间来回切换。
如果某个就绪的文件可用的 I/O 空间很大,试图穷尽它可能会导致其他文件得不到处理,造成饥饿。(但这个问题并不是 epoll 特有的)。
解决方案是维护一个就绪列表,并在其关联的数据结构中将此文件描述符标记为就绪,从而使应用程序在记住哪些文件需要被处理的同时仍能循环遍历所有就绪的文件。这也使你可以忽略收到的已经就绪的文件描述符的后续事件。
如果你使用了事件缓存或暂存了所有从 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 |