epoll(2) 使用及源码分析的引子
epoll(2) 使用及源码分析的引子
本文代码取自内核版本 4.17
epoll(2) - I/O 事件通知设施。
epoll 是内核在2.6版本后实现的,是对 select(2)/poll(2) 更高效的改进,同时它自身也是一种文件,不恰当的比方可以看作 eventfd + poll。
多路复用也是一直在改进的,经历的几个阶段
- select(2) - 只能关注 1024 个文件描述符,并且范围固定在 0 - 1023,每次函数调用都需要把所有关注的数据复制进内核空间,再对所有的描述符集合进行遍历判断。
- poll(2) - 改进 select(2) 前面两个缺点,可以自定义关注的描述符,数量也不受限制(不超过系统的限制),每次调用同样需要复制所有的事件进内核空间,全部遍历。
- epoll(2) - 不需要每次调用时所有关注的文件描述符进行内核-用户空间的复制,而是直接将所有的文件描述符和事件常驻内核空间,同时也不需要每次遍历所有文件描述符。
提供的系统调用
#include <sys/epoll.h> typedef union epoll_data { void *ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t; struct epoll_event { uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ }; int epoll_create(int size); int epoll_create1(int flags); int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll_create() - 用来创建一个 epoll 实例,返回一个新的文件描述符。第一个参数 @size
自 2.6.8 开始无意义,但必须大于 0。
epoll_create1() - 参数 @flags
为 0 则等效于 epoll_create(),flags 可以为 EPOLL_CLOEXEC, 在exec新程序时关闭文件描述符。
epoll_ctl() - epoll 的控制接口,用户调用该系统调用来控制监听的文件描述符。参数 @epfd
为 epoll_create() 返回的新文件描述符,参数 @op
为 epoll_ctl() 提供的控制操作:
- EPOLL_CTL_ADD, 向 epoll 中注册一个新的文件描述符;
- EPOLL_CTL_MOD, 修改关联文件描述符中的事件;
- EPOLL_CTL_DEL, 移除 epoll 中的描述符,且无视
@event
参数;
参数@fd
为需要控制的文件描述符,参数@event
为相关联的 struct epoll_event 结构。
epoll_wait() - 等待epoll中监听文件描述符就绪的 I/O 事件。参数 @epfd
为epoll实例对应的文件描述符,由 epoll_create() 创建,、
参数 @events
为就绪的事件集合的地址,参数 @maxevents
为需要就绪事件集合的大小,必须大于 0,参数 @timeout
为超时时间,单位为 微秒。
水平触发模式和边缘触发模式
epoll 默认使用水平触发模式,边缘触发模式需要设置 events |= EPOLLET
。
边缘触发模式的特点是边缘触发模式只在关注的文件描述符发生改变时才产生就绪的事件,考虑高低电平的图片,边缘是有一个瞬间的概念,而水平则有一个持续的状态。
这就导致了,边缘触发有可能会丢失需要通知的事件。分析如下
现有 5 个步骤:
- 管道读端的文件描述符 rfd 被注册到 epoll 实例中。
- 管道写端写入了 2 kB 数据。
- 调用 epoll_wait(2) 返回了 rfd 作为就绪的文件描述符。
- 管道读端读取了 1 kB 数据。
- 调用 epoll_wait(2)。
如果文件描述符 rfd 使用 EPOLLET 边缘触发模式注册到 epoll 中,那么在执行上面的 5 的时候,尽管管道的读端缓冲区还有数据,epoll_wait(2) 还是可能会挂起,
同时写端可能会基于其已发送的数据期望响应。产生这个情况的原因是边缘触发模式只在关注的文件描述符发生改变时才产生就绪的事件。在上面的步骤中,2 写入的数据,
因此在 rdf 上生成一个事件,由于在 4 中的读取操作不会消耗整个缓冲区数据,故在 5 对 epoll_wait(2) 调用可能发生阻塞。
使用边缘触发模式的程序应该使用非阻塞文件描述符来避免阻塞读写造成处理多个文件描述符时产生的饥饿问题。
所以建议使用的边缘触发模式时遵从一下两点:
- 文件描述符为非阻塞方式,并且
- 只在 read(2) / write(2) 返回 EAGAIN 后进行等待。
在使用边缘触发模式时,在接收到多个数据块的时候会产生多个事