epoll简介
名称
epoll - I/O 事件通知机制
概要
#include <sys/epoll.h>
描述
epoll是poll的升级版,支持边缘触发和水平触发的就绪通知方式,并且拥有良好的扩展性,可以监听大量文件描述符。一个epoll实例由下列系统调用创建和管理:
* epoll实例由 epoll_create创建, 返回新建epoll实例的文件描述符 (最新的epoll_create1扩展了epoll_create的功能.)
* 使用epoll_ctl在epoll实例中注册文件描述符。一个epoll实例上的文件描述符集合被称为“epoll集合”。
* 最后调用epoll_wait等待事件的发生.
水平触发和边缘触发
epoll 事件发布机制有边缘触发(ET) 和水平触发(LT)两种. 为了说明它们的区别,请看下面的情形:
1. 把一个用来从管道中读取数据的文件描述符句柄(rdf)添加到一个epoll实例中
2. 这个时候从管道的另一端被写入了2KB的数据
3. 调用epoll_wait,并且它会返回rdf,说明它已经就绪
4. 读取了1KB的数据
5. 调用epoll_wait......
如果上面情形使用边缘触发方式,尽管文件输入缓冲区中还有可用的数据,第5步中的epoll_wait仍然很可能会引起进程挂起。产生这个现象的原因是边缘触发模式只有在被监听的文件描述符的状态发生变化才发出事件通知。由于第4步的读操作没有读完缓冲区的所有数据,因此第5步的 epoll_wait 调用后,进程很有可能会一直等待下去。
另外,如果epoll应用边缘触发方式,应用程序要使用非阻塞文件描述符,避免读写过程中的阻塞使得处理多个文件描述符的任务产生饥饿现象。总之,使用边缘触发的epoll时要注意以下两点:
i 使用非阻塞式文件描述符;
ii 当 read或write(返回EAGAIN后再进行下一次事件等待。
相对而言,当eppoll使用水平触发方式时,其就像是一个更高效的poll, 它们的语义基本相同。
还有一种模式,调用者可以指定标志位EPOLLONESHOT,告诉epoll在监听完这个描述符的一次事件后禁用相关文件描述符。如果使用了标志位EPOLLONESHOT ,调用者需要每次都重新使用epoll_ctl(2) 的 EPOLL_CTL_MOD操作重新设置这个文件描述符。
/proc 接口
下列接口用于限制内核中epoll消耗的内存:
/proc/sys/fs/epoll/max_user_watches (since Linux 2.6.28)
指定系统里所有注册在epoll实例中的文件描述符的限制。每个注册的文件描述符 在32位内核中大概占用90字节,在64位内核中占用160字节。目前 max_user_watches的默认值是可用内存的4%。
范例
水平触发方式的epoll在语义上和poll(2)相似,而边缘触发的epoll需要开发者更加复 杂的操作以防止应用程序的停滞。在这个例子中,应用程序监听一个非阻塞socket,函数do_use_fd() 对新就绪的文件描述符上调用read 或 write直 至返回错误EAGAIN。
#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); } } }
Questions and Answers
Q0 在一个epoll集合中用来区分文件描述符的键值是什么?
A0 键值是文件描述符编号以及打开文件描述块(open file description 或 open file handle)。
Q1 如果在一个epoll实例中重复注册同一个文件描述符会发生什么?
A1 你可能会得到EEXIST。不过向一个epoll实例中注册描述符副本(dup)是允许的。如果文件描述符和其副本使用不同的事件选项,那么这么做可以用来过滤事件。
Q2 可以使用两个epoll实例监听同一个文件描述符吗?如果可以的话,事件会都报告给两个epoll文件描述符吗?
A2 是的,都会收到事件通知,但是这么做时一定要小心。
Q3 epoll文件描述符也可以被poll/epoll监听吗?
A3 可以。如果一个epoll文件描述符正在等待事件,那么它就被认为是可读的。
Q4 如果将一个epoll文件描述符加入自身的描述符集合中会发生什么?
A4 epoll_ctl(2) 调用会失败 (EINVAL). 不过你可以将一个epoll文件描述符加入另一个epoll描述符集合。
Q5 我可以将一个epoll文件描述符通过一个UNIX域套接字发送给其它进程吗?
A5 可以,但是这么做没有意义,因为接受进程没有epoll集合中文件描述符的拷贝。
Q6 如果关闭一个文件描述符,那么它会从epoll集合中自动删除吗?
A6 是的,但是要注意以下几点。一个文件描述符对应一个打开文件描述块,当使用dup, dup2, fcntl F_DUPFD创建副本或使用fork时,新创建的文件描述符也会对应同一个打开文件描述块。 只要关联的文件描述符没有全部关闭,打开文件描述块还存在于内核中。因此在epoll集合中,文件描述符自动删除的条件是其对应的打开文件描述块的所有文件描述符都被关闭了。这意味着即使epoll集合中的一个文件描述符关闭了,只要还有对应同一个文件描述块的文件描述符打开着,事件的通知就依然会继续。
Q7 如果在在 epoll_wait 调用之间发生了多个事件, 那它们会被一起报告还是单独报告给进城?
A7 会一起报告.
Q8 对一个文件描述符的操作是否会影响到已经收集好但还没有发出通知的事件?
A8 对文件描述符的操作有两种:删除不会产生影响,修改可能造成可用IO被重读。
Q9 对于边缘触发模式的epoll,我们需要不断的读/写可用的文件描述符直至产生 EAGAIN吗?
A9 何时以及如何使用这些文件描述符完全取决于你。
对于packet/token-oriented 文件(datagram socket,terminal in canonical mode), 探测读写I/O空间是否完成的唯一途径就是等待产生EAGAIN.
对于 stream-oriented 文件 (e.g., pipe, FIFO, stream socket), 判断的方法是查看读写数据的数量。例如如果调用read读特定大小的数据,如果返回值比设定值小,那么说明可读数据已经耗尽了。