epoll

        epoll是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率,因为它会复用文件描述符集合来传递结果而不用迫使开发者每次等待事件之前都必须重新准备要被侦听的文件描述符集合,另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核I/O事件异步唤醒而加入ready队列的描述符集合就行了。
        epoll API执行与poll类似的任务:监听多个文件描述符中的任意一个或多个描述符是否处于I/O“就绪”。epoll API既可以用作Edge-Triggered(边缘触发)接口,也可以用作Level-Triggered(水平触发)接口,并且可以很好地扩展其规模以用于大量被监听的的文件描述符。

        我们可以通过多个不同的系统调用来分别创建和管理epoll instance(epoll实例)

  1. epoll_create(2):创建一个新的epoll instance并返回一个引用此实例的文件描述符(epoll_create1(2)扩展了epoll_create(2)的功能);
  2. epoll_ctl(2):注册特定文件描述符。当前在epoll instance上注册的文件描述符集也称作epoll set(epoll集);
  3. epoll_wait(2):等待I/O事件,如果没有就绪的事件,则阻塞(可设置)当前调用线程。

触发模式

        epoll事件分发接口的行为既可以是ET模式,也可以是LT模式。

Level Triggered

        水平触发只要有数据都会触发。当EPOLLET未被指定时,默认触发。

Edge Triggered

        边缘触发只有数据到来才触发,不管缓存区中是否还有数据。这是因为ET模式仅在被监听的文件描述符发生变化时递送事件。

使用epoll作为一个边缘触发(EPOLLET)接口的建议:

  1. 与非阻塞文件描述符一同使用,并且
  2. 仅在read(2)或write(2)返回EAGAIN时等待事件。

EPOLLONESHOT标志

        由于即使使用边缘触发的epoll,在接收到多个数据块时也可以生成多个事件,因此可以选择指定EPOLLONESHOT标志,以告知epoll禁用在通过epoll_wait(2)接收到事件的关联的文件描述符。当指定了EPOLLONESHOT标志时,调用方负责使用epoll_ctl(2)和EPOLL_CTL_MOD重新配置文件描述符。

EPOLLIN和EPOLLOUT切换问题

        当用作边缘触发接口时,出于性能原因,可以通过指定EPOLLIN | EPOLLOUT在epoll接口(EPOLL_CTL_ADD)中添加一次文件描述符。这将允许你避免连续使用EPOLL_CTL_MOD调用epoll_ctl(2)在EPOLLIN和EPOLLOUT之间来回切换。

autosleep

        如果系统通过/sys/power/autosleep处于自动休眠模式,并且发生了一个将设备从睡眠中唤醒的事件,则设备驱动程序将仅在该事件排队之前保持设备处于唤醒状态。要在事件处理之前保持设备处于唤醒状态,必须使用epoll_ctl(2)和EPOLLWAKEUP标志。
当在struct epoll_events的events字段中设置EPOLLWAKEUP标志时,系统将从事件排队的那一刻起,通过epoll_wait(2)调用保持唤醒状态,该调用将返回事件,直到下一个epoll_wait(2)调用。如果该事件应使系统在该时间之后保持唤醒,则应在第二次epoll_wait(2)调用之前设置一个单独的wake_lock

/proc接口

/proc/sys/fs/epoll/max_user_watches (since Linux 2.6.28):
        此接口可用于限制epoll消耗的内核内存量:其指定了对用户可以在系统上的所有epoll实例中注册的文件描述符总数的限制。每个注册的文件描述符在32位内核上大约需要90字节,在64位内核上大约需要160字节。目前,max_user_watches的默认值是可用低内存的1/25(4%)除以以字节为单位的注册成本。

修改监听描述符上限

        可以使用cat命令查看一个进程可以打开的socket描述符上限。

cat /proc/sys/fs/file-max

        如有需要,可以通过修改配置文件的方式修改该上限值。

sudo vi /etc/security/limits.conf

        在文件尾部写入以下配置,soft软限制,hard硬限制。

* soft nofile 65536
* hard nofile 100000

Q&A

Q0. 如何区分在epoll set中已经注册过的诸多文件描述符?
A0. 打开文件描述符(也称“打开文件句柄”,这是内核对打开文件的内部表示)自身和其对应的值。

Q1. 在同一个epoll instance中,如果对一个已注册的文件描述符再注册一次会发生什么?
A1. 会得到一个EEXIST错误。但是可以先duplicate(dup(2), dup2(2), fcntl(2) F_DUPFD)该文件文件描述符,然后将duplicate得到的新文件描述符注册到同一个epoll instance中。推荐用法:将源文件描述符与duplicate后得到的文件描述符分别注册不同的事件(使用不同的events masks)。

Q2. 两个epoll instances可以wait同一个文件描述符吗?如果可以,那么在该文件描述符所注册的events触发时,这两个epoll fd都会被递送此events吗?
A2. 可以。是的,但必须谨慎操作。

Q3. epoll fd自身能否被另一个其他类型(select/poll)或同类型(epoll)的实例或set所注册呢?
A3. 可以,若在被注册的这个epoll fd中有events触发,它将会被标记为可读。

Q4. 如果一个epoll fd试图epoll自身会发生什么(自己套自己)?
A4. epoll_ctl(2)调用将返回一个EINVAL错误。

Q5. 可以把一个epoll fd通过本地套接字(UNIX domain socket)发送到另一个进程吗?
A5. 可以,但这样做是没有意义的,因为接收进程将不会拥有在该epoll set中任何文件描述符的副本。

记得写代码验证

个人疑问,Q5里说的“其他进程”应该是指没有血缘关系的进程。
在父进程fork之前create一个epoll fd,那么父子进程是否共享一个epoll instance(感觉应该可以)?

Last modification:April 2, 2022
如果觉得我的文章对你有用,请随意赞赏