epoll 的核心思想是避免无效的轮询。它不像 select 或 poll
那样,每次都需要应用把所有要监视的FD列表告诉内核,而是让内核来“记住”这个列表,只有当FD真正就绪时,才通知应用程序。
整个过程可以分解为以下几个步骤:
epoll_create() - 创建实例
应用程序调用 epoll_create(),请求内核创建一个 epoll
实例。内核会在内部创建一个专属的数据结构,可以想象成一个“事件中心”,并返回一个指向这个实例的文件描述符(epollfd)。这个步骤只需要执行一次。
epoll_ctl() - 注册事件
应用程序通过 epoll_ctl() 将它关心的所有文件描述符(例如,成千上万个网络连接的 socket FD)注册到第一步创建的 epoll
实例中。在内核中,这些FD会被添加到一个高效的数据结构中,通常是红黑树(Red-Black
Tree)。这使得即使在有大量FD的情况下,添加、删除和查找的效率也很高。
关键点:这个FD列表现在由内核维护,应用程序不需要在每次查询时都重复提交。
epoll_wait() - 等待事件
应用程序调用
epoll_wait(),然后进入阻塞(睡眠)状态,等待内核的通知。它告诉内核:“如果我注册的任何一个FD有事件发生,请唤醒我。”
当一个外部事件发生时,例如,一个网络数据包到达网卡,并经过内核网络协议栈的处理,最终数据被放入了某个 socket(比如图中的 fd3)的接收缓冲区。这个 socket
此时变为“可读”状态。
由于在步骤 ② 中,fd3 已经被注册到了 epoll
实例中,内核知道有进程在关心它。内核会触发一个回调机制,将这个就绪的 fd3 添加到 epoll
实例内部的另一个列表——就绪队列(Ready List)中。
因为就绪队列现在不再为空,内核会唤醒在步骤 ③ 中阻塞的应用程序。epoll_wait() 函数返回,并将就绪队列中的所有FD(这里是
fd3)拷贝给应用程序。应用程序得到一个只包含真正就绪的FD的列表,然后就可以只对这些FD进行读写操作,极大地提高了处理效率。
epoll_wait() 的返回只包含就绪的FD,其处理时间复杂度是
O(就绪FD数量),而不是 O(全部监视的FD数量)。epoll 能够轻松管理数十万甚至上百万的并发连接,是构建高性能网络服务的基石。