Skip to main content
  1. Docs/

浅谈 Epoll

·5 mins· ·
Owl Dawn
Author
Owl Dawn
Table of Contents

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 结构的权限。

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_createepoll_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 模式,可以将 EPOLLETEPOLLIN 进行按位或(OR)操作来实现。若希望某注册事件仅触发一次通知后停止监控,可将 EPOLLONESHOTevents 进行按位或操作。相关 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 上又有新的事件发生。

参考引用
#

Related

摄影调色
·2 mins
Golang Channel
·6 mins
Golang
Golang sync.Map
·4 mins
Golang
containerd
·12 mins
Kubernetes Scheduler
·8 mins
性能优化
·7 mins