我看过很多比较说 select 必须遍历 fd 列表,这很慢。但是为什么 epoll 不必这样做呢?
2 回答
有很多关于此的错误信息,但真正的原因是:
一个典型的服务器可能要处理 200 个连接。它将为需要写入或读取数据的每个连接提供服务,然后需要等到有更多工作要做。在等待时,如果在这 200 个连接中的任何一个上接收到数据,则需要中断它。
使用select
,内核必须将进程添加到 200 个等待列表中,每个连接一个。为此,它需要一个“thunk”来将进程附加到等待列表。当进程最终唤醒时,需要从所有 200 个等待列表中删除它,并且需要释放所有这些 thunk。
相比之下,epoll
套接字epoll
本身有一个等待列表。该过程只需要使用一个 thunk 就可以放在一个等待列表中。当进程唤醒时,它只需要从一个等待列表中删除,并且只需要释放一个 thunk。
需要明确的是,使用epoll
,epoll
套接字本身必须附加到这 200 个连接中的每一个。但是,对于每个连接,当它首先被接受时,它就完成一次。对于每个连接,当它被删除时,它会被拆除一次。相比之下,select
对该块的每次调用都必须将进程添加到每个被监视的套接字的等待队列中。
具有讽刺意味的是,select
最大的成本来自检查没有活动的套接字是否有任何活动。有了epoll
,就不需要检查没有活动的套接字,因为如果他们确实有活动,他们会epoll
在活动发生时通知套接字。从某种意义上说,select
每次调用时都会轮询每个套接字select
,以查看在绑定它时是否有任何活动,epoll
以便套接字活动本身通知进程。
epoll
和之间的主要区别在于select
,select()
要等待的文件描述符列表仅在单个select()
调用期间存在,并且调用任务仅在单个调用期间停留在套接字的等待队列中。epoll
另一方面,您创建了一个文件描述符,它聚合了您要等待的多个其他文件描述符中的事件,因此受监视的 fd 列表是持久的,并且任务跨多个系统调用保留在套接字等待队列中. 此外,由于一个epoll
fd 可以跨多个任务共享,它不再是等待队列上的单个任务,而是一个本身包含另一个等待队列的结构,包含当前在等待队列上等待的所有进程epoll
fd。(在实现方面,这是由套接字的等待队列抽象出来的,该队列持有一个函数指针和一个void*
传递给该函数的数据指针)。
因此,为了进一步解释机制:
- 一个
epoll
文件描述符有一个私有struct eventpoll
的跟踪哪些 fd 附加到这个 fd。struct eventpoll
还有一个等待队列,用于跟踪当前正在epoll_wait
此 fd 上的所有进程。struct epoll
还具有当前可用于读取或写入的所有文件描述符的列表。 - 当您使用 将文件描述符添加到
epoll
fdepoll_ctl()
时,epoll
将 添加struct eventpoll
到该 fd 的等待队列中。它还检查 fd 当前是否已准备好进行处理,如果是,则将其添加到就绪列表中。 - 当您使用 等待
epoll
fdepoll_wait
时,内核首先检查就绪列表,如果任何文件描述符已经就绪,则立即返回。如果没有,它会将自己添加到内部的单个等待队列中struct eventpoll
,然后进入睡眠状态。 - 当正在
epoll()
编辑的套接字上发生事件时,它会调用epoll
回调,将文件描述符添加到就绪列表中,并唤醒当前正在等待的任何服务员struct eventpoll
。
显然,需要对struct eventpoll
各种列表和等待队列进行大量仔细的锁定,但这是一个实现细节。
需要注意的重要一点是,我在上面没有描述循环遍历所有感兴趣的文件描述符的步骤。通过完全基于事件并使用一组持久的 fd 和一个就绪列表,epoll 可以避免一次操作花费 O(n) 时间,其中 n 是被监视的文件描述符的数量。