Linux 多路复用之epoll

epoll 简析

epoll作为一种基于事件通知的I/O处理模型,广泛用于需要I/O多路处理的场景。epoll API所执行的任务与poll类似:监测多路文件描述符的I/O可用性。

epoll的特性:

  • 触发方式:epoll API能被用于边沿触发或者电平触发方式,这两种触发方式的机制存在较大的不同,下文会做详细讲解。
  • 性能:epoll能够很好地监测大量的文件描述符,对于大规模的I/O多路复用场景,epoll是很好的选择。

epoll实例基本管理

通过epoll_create、epoll_wait、epoll_ctl系统调用完成epoll的创建、I/O事件等待、文件描述符的增加|删除|修改等功能。

  • epoll_create:其创建一个epoll的实例,并且返回一个指向该实例的文件描述符。(epoll_create1扩展了epoll_create的一些功能)。
  • epoll_ctl:通过epoll_ctl将感兴趣的文件描述符添加到epoll实例。当前注册到epoll实例上的文件描述符集合,一般称为epoll set。
  • epoll_wait:等待I/O事件,如果没有I/O事件可用,那么epoll_wait会阻塞调用线程。

触发方式

epoll事件分发界面可以表现为edge-triggered(ET)和level-triggered(LT)触发方式。关于两种机制的区别下面会详细的介绍。现在模拟这样一种场景:

  1. 打开一个管道,并将管道的读端文件描述符(rfd)注册到epoll实例。
  2. 在管道的另一端,向管道写入2kB的数据。
  3. epoll_wait会返回,并且返回可读状态的rfd。
  4. 通过rfd读取1kB的数据。
  5. 调用epoll_wait。

如果rfd在加入到epoll实例时配置了EPOLLET(edge-triggered)属性,那么上面第5步的epoll_wait调用会挂起,尽管在rfd输入缓冲区中仍然存在可用的数据;同时,管道的另一端可能在等待基于这些数据的响应消息。
发生这种情况的原因是,edge-triggered模式只会在文件描述符发生变化时才会递送事件通知。所以,第5步的epoll_wait可能会一直一些数据的到来,但是这些数据已经存在于输入缓冲区。上面例子中,步骤2,向管道写入
2kB数据,步骤3中,epoll_wait以为rfd可读而返回,步骤4,读取了管道中的一部分数据。因为步骤4中没有将全部的数据读取出来,所以,步骤5中的epoll_wait调用可能会一直阻塞下去。

为了避免应用程序在处理多个使用了EPOLLET属性的文件描述时,由于阻塞与读或者写,而导致某个任务出现"饥饿"的现象,我们应该使用非阻塞文件描述符。建议使用EPOLLET模式的epoll方式如下:

  • 使用非阻塞的文件描述符;并且
  • 只有在read或write返回EAGAIN错误后,再调用epoll_wait等待I/O事件。

相反,当使用电平触发方式时(默认是该模式),epoll仅仅是高速版的poll,其语义与poll相同。

即使某些文件描述符使用了边缘触发的epoll,也可以在接收到多个数据块时生成多个事件,因此调用者可以选择指定EPOLLONESHOT标志,告诉epoll在接收到epoll_wait(2)事件后禁用相关的文件描述符。当EPOLLONESHOT属性启用时,调用
者负责使用epoll_ctrl的EPOLL_CTL_MOD再次解禁epoll的文件描述符。

epoll与内存

/proc/sys/fs/epoll/max_user_watches(Linux 2.6.28),负责限制epoll所使用的内核态下的内存总量。该文件定义了单个用户下所有epoll实例所拥有的文件描述符的总量。每个已注册的文件描述符在32-bit内核下占用90字节,64-bit
内核下占用160字节。目前,max_user_watches的默认值是1/25(4%)的可用物理内存除以每个文件描述符所用的内存大小。

常见陷阱其避免方式

  • 进程饥饿(边沿触发)
    如果有大量的I/O空间,那么通过尝试耗尽它,其他文件可能不会得到处理,从而导致饥饿。(这个问题不是epoll特有的。)
    解决方案是维护一个就绪列表,并在其关联的数据结构中将文件描述符标记为ready,从而允许应用程序记住需要处理哪些文件,但仍然在所有就绪文件之间进行循环。这还支持忽略已经准备好的文件描述符接收到的后续事件。

  • 如果使用了event cache

如果您使用事件缓存从epoll_wait返回的所有文件描述符,那么请确保提供一种动态标记其闭包的方法(即,由先前事件的处理引起)。假设你从epoll_wait收到100个事件,而在事件#47中,有一个条件会导致事件#13关闭。 如果删除结构并关闭事件#13的文件描述符,那么事件缓存可能仍然认为等待该文件描述符的事件从而导致混淆,这是常见的缓存状态不一直问题。

对此的一个解决方案是在事件#47的处理期间调用epoll_ctl(EPOLL_CTL_DEL)来删除文件描述符,并将其close,然后将其关联的数据结构标记为已移除并将其链接到清理列表。 如果在批处理中找到文件描述符#13的另一个事件,您将发现之前已删除的文件描述符,并且不会产生混淆。

Q&A

  • Q0:用于区分已经注册到epoll中的各个文件描述符的关键是什么?

  • A0:关键是文件描述符号和打开的文件描述的组合(也称为“打开文件句柄”,它是打开文件的内核内部表示)。

  • Q1:同一个文件描述符注册两次会出现什么问题?

  • A1:可能会返回EEXIST错误码。但是,可以向同一epoll实例注册一个使用(dup、dup2、fcntl)复制过的文件描述符。如果重复的文件描述符用不同的事件掩码注册,这对于过滤事件是一种有用的技术。

  • Q2:不同的epoll实例可以监测同一文件描述符吗?如果可以的话,两个epoll都会收到事件通知吗?

  • A2:是的,每个epoll都会收到事件通知。然而,小心编程完全可以避免该问题。

  • Q3:epoll本身的文件描述符是否可以poll/epoll/selectable?

  • A3:是的。如果一个epoll文件描述符可读时,其同样会产生相应的事件通知。

  • Q4:如果将epoll文件描述符添加到epoll自己的文件描述符集会发生什么问题?

  • A4:epoll_ctrl会返回EINVAL错误码。然而,你可以将epoll文件描述符添加到另一个epoll实例的文件描述符集中。

  • Q5:可以将一个epoll文件描述符通过UNIX域socket发送给另一个进程吗?

  • 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阻塞期间,产生了多个事件,那么,它们是一块上报还是逐个上报呢?

  • A7:一块上报。

  • Q8:对于一个收集到但尚未上报的事件所关联的文件描述操作,是否会产生影响?

  • A8:对于已经存在的文件描述符,你可以执行两项操作:没有太大意义的删除操作;修改会重新获得可读的IO。

  • Q9:当使用EPOLLET标志(边缘触发行为)时,我是否需要不断地读取/写入文件描述符,直到返回EAGAIN?

  • A9:从epoll_wait(2)接收一个事件应该会向您提示,该文件描述符已经为请求的I/O操作做好了准备。您必须认为它已经准备好了,直到下一次(非阻塞)读/写再次产生EAGAIN。何时以及如何使用文件描述符完全取决于您。
    对于面向包/令牌的文件(例如,数据报套接字、规范模式下的终端),检测读/写I/O空间结束的惟一方法是继续读/写,直到返回EAGAIN。
    对于面向流的文件(例如管道、FIFO、流套接字),也可以通过检查从读/写到目标文件描述符的数据量来检测读/写I/O空间耗尽的情况。例如,如果您通过请求读取一定数量的数据来调用read(2),而read(2)返回的字节数更少,那么可以肯定已经耗尽了文件描述符的read I/O空间。当使用write(2)进行写作时也是如此。(如果不能保证所监视的文件描述符始终引用面向流的文件,则避免使用后一种技术。)

发布了119 篇原创文章 · 获赞 125 · 访问量 45万+

猜你喜欢

转载自blog.csdn.net/linux_embedded/article/details/90080253