Epoll 即 event poll,是 linux 提供的一个特殊结构,通过 epoll 可以实现进程对多个文件描述符的监听,并在 I/O 就绪时收到通知。epoll 提供了 ET(边缘触发,edge-triggered)和 LT(水平触发,level-triggered)两种工作模式。
调用方法 #
与 poll 不同,epoll 本身不是系统调用,而是一个内核数据结构,允许进程通过多路复用管理多个文件描述符的 I/O。此数据结构通过三个系统调用来实现创建、修改和删除:
epoll_create #
#include <sys/epoll.h>
int epoll_create(int size);
size
:内核早期用它决定 epoll 实例的大小(表示进程希望监控的文件描述符数量),但从 Linux 2.6.8 起被忽略,epoll 自动动态调整大小。- 返回值:一个文件描述符,指向新创建的 epoll 实例。调用进程可以使用该文件描述符来添加、删除或修改它想要监视的其他文件描述符(I/O)。
这个函数还有一个变体
int epoll_create1(int flags);
flags
: 可以是 0 或EPOLL_CLOEXEC
- 0: 与
epoll_create
相同 EPOLL_CLOEXEC
: 表示当调用fork
时,子进程会在execs
之前自动关闭该文件描述符,这样子进程就没有访问这个 epoll 结构的权限。
- 0: 与
epoll_ctl #
进程通过 epoll_ctl
向 epoll 实例注册需要监控的文件描述符。注册的文件描述符列表即 epoll set(interest list)
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd
: 由epoll_create
或epoll_create1
返回的文件描述符。fd
: 要加入 interest list 的文件描述符。op
: 操作类型。EPOLL_CTL_ADD
:注册 fd 并监控事件。EPOLL_CTL_DEL
:从列表中移除 fd,关闭文件描述符会自动从所有关联的 epoll 实例中移除。EPOLL_CTL_MOD
:修改 fd 监控的事件。
event
:指向epoll_event
结构的指针,里面存放了我们关注的文件描述符的事件。
struct epoll_event {
uint32_t events; /* 事件类型 */
epoll_data_t data;
};
epoll_event.events
是 位掩码 的形式,表示需要哪一种监听事件。如果 fd 是 socket 类型,我们希望监听 socket 缓冲区是否有新数据(EPOLLIN),如果需要采用 ET 模式,可以将 EPOLLET
与 EPOLLIN
进行按位或(OR)操作来实现。若希望某注册事件仅触发一次通知后停止监控,可将 EPOLLONESHOT
与 events
进行按位或操作。相关 flag 可以在这里查阅
epoll_wait #
进程调用 epoll_wait
可以接受 interest list 中事件的通知,
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event *evlist,
int maxevents, int timeout);
epfd
:epoll 实例描述符evlist
:epoll_event 数组,由调用进程遇险创建,当函数返回后,其中的内容被内核修改,存放就绪事件的文件描述符。maxevents
:数组容量上限timeout
:超时设置(毫秒):0
:非阻塞模式,立即返回当前就绪事件-1
:完全阻塞,直到事件发生>0
:阻塞指定毫秒数
- 返回值:
- 成功:返回就绪事件数量(填充至 evlist)
- 超时:返回 0
- 错误:返回 -1
内部实现 #
epoll 内部维护了三个核心的数据结构:interest list (红黑树)、wait queue (链表) 和 ready list (链表)。interest list 和 ready list 由每个 epoll 实例来维护;对于 wait queue,每个 epoll 实例会维护一个全局的 wait queue,内核还会为 interest list 中的每一个 fd 维护一个本地的 wait queue,用于注册 fd 的等待事件和回调函数。
当调用 epoll_ctl()
时,内核将将待监听的 fd 打包成一个 epitem
(epoll item) 之后插入到 interest list(红黑树),然后再将 epitem
打包进 ep_pqueue
,最后将这个等待事件加入 fd 本地的 wait queue,并为其注册一个回调函数 ep_poll_callback()
。
调用 epoll_wait()
时,首先扫描 ready list (链表),如果其中存在事件,就直接返回给调用进程。否则注册一个等待事件到 epoll 实例的 wait queue (链表),主动把自己从当前的 CPU 调度走,保存现场并进入阻塞状态,等待就绪事件的到来。
当网卡收到监听 socket 的网络包之后,DMA 就会将数据拷贝到内存并发出一个硬件中断通知内核,内核收到硬中断之后会调用硬中断处理程序,然后再触发一个软件中断,启动软中断处理程序,先定位到 DMA 拷贝的数据在内存中的地址,然后将其中的数据过一遍网络协议栈,按照 OSI 中每一层的特定协议逐层解包,最后得到原始的应用数据,与此同时软中断程序会找到 socket 的 wait queue 中对应的事件回调函数 ep_poll_callback()
并调用之,将 epitem 插入到 epoll 的 ready list 中并唤醒被阻塞在 epoll_wait
系统调用的线程,内核的进程调度器会重新调度这个进程,当进程被调度器选中并放到 CPU 上执行的时候,恢复现场并继续执行 epoll_wait
扫描 ready list 链表取出事件返回,用户进程可以开始处理就绪事件。
LT
LT 是 epoll 的默认模式,只要文件描述符处于 readable 或者 writable 状态,epoll 就会一直通知用户程序去处理,直到状态变成 unreadable 或者 unwritable。即只要有数据可读或者可写,epoll 就会一直通知用户程序去处理。
ET
当关注的文件描述符状态发上变化,就会触发通知。一般情况下 ET 模式下需要设置 non-blocking
,因为文件描述符状态未发生改变,就不会触发通知,意味着事件处理需要彻底,如果读事件缓存没有读完,就不会触发再次通知。一般用户进程会一直读写 socket fd 直至返回 EAGAIN
错误确保事件处理完全。
在 epoll_wait
中,如果 socket 有就绪事件需要处理,内核就会将其对应的 epitem (此时具体的就绪事件类型已经设置完成) 拷贝到用户空间,之后区分 LT 和 ET 模式最关键的一步就是决定是否把已经拷贝到用户空间的 epitem 重新加回到 epoll 实例的全局 ready list。LT 模式下就会将 epitem 加回去,ET 模式则不会。
当下次用户再次调用 epoll_wait
时,LT 模式下还是会再次从 ready list 中遍历到上次那个 epitem,然后再检查该 epitem 上是否还有就绪事件,如果有则继续返回给用户程序,如果没有则忽略;而 ET 模式则再也遍历不到这个 epitem,除非对应 socket 上又有新的事件发生。