为什么epoll比select更快?

我看过很多比较,说select必须通过fd列表,这是缓慢的。 但为什么不epoll必须这样做?

这里有很多错误的信息,但真正的原因是这样的:

一个典型的服务器可能正在处理200个连接。 它将服务每一个需要有数据写入或读取的连接,然后需要等待,直到有更多的工作要做。 当它正在等待时,如果在这200个连接中的任何一个上收到数据,则需要中断它。

使用select ,内核必须将进程添加到200个等待列表,每个连接一个。 要做到这一点,它需要一个“thunk”将进程附加到等待列表。 当进程最终唤醒时,需要从所有200个等待列表中删除,并且需要释放所有这些thunk。

相比之下,与epollepollsockets本身有一个等待名单。 这个过程只需要一个等待列表就只使用一个thunk。 当进程唤醒时,只需要从一个等待列表中删除,只需要释放一个thunk。

为了清楚epoll ,在epollepollsockets本身必须连接到这200个连接中的每一个。 但是,对于每个连接,当它首先被接受的时候,这是一次完成的。 这是拆除一次,每个连接,当它被删除。 相比之下, select该块的每个调用都必须将该进程添加到每个正在监视的套接字的每个等待队列中。

具有讽刺意味的是, select最大的成本来自检查没有活动的套接字是否有任何活动。 使用epoll ,没有必要检查没有活动的套接字,因为如果他们确实有活动,他们会在活动发生时通知epoll套接字。 从某种意义上说,每次调用select都要select轮询每个套接字,以查看epoll套接字是否有任何活动,以便套接字活动本身通知进程。

epollselect之间的主要区别在于,在select()中,要等待的文件描述符的列表仅存在于一个select()调用的持续时间内,并且调用任务仅停留在套接字的等待队列中一个电话。 另一方面,在epoll ,您创build了一个文件描述符,用于聚合来自多个其他文件描述符的事件,这样,所监视的fd列表将持久化,并且任务保留在跨多个系统的套接字等待队列中调用。 此外,由于epoll fd可以在多个任务之间共享,所以它不再是等待队列上的单个任务,而是一个本身包含另一个等待队列的结构,包含当前正在等待epoll fd的所有进程。 (就实现而言,这是通过套接字的等待队列抽象出来的,持有一个函数指针和一个void*数据指针传递给该函数)。

所以,再解释一下这个机制:

  1. 一个epoll文件描述符有一个私有的struct eventpoll来跟踪哪个fd被连接到这个fd。 struct eventpoll还有一个等待队列,用于跟踪当前在此fd上等待的所有进程。 struct epoll还有一个当前可用于读取或写入的所有文件描述符的列表。
  2. 当使用epoll_ctl()epoll fd添加文件描述符时, epoll会将struct eventpoll添加到该fd的等待队列中。 它还检查fd是否准备好处理,并将其添加到就绪列表中,如果是的话。
  3. 当您使用epoll_wait等待epoll fd时,内核首先检查就绪列表,如果有任何文件描述符已经准备好,则立即返回。 如果没有,它将自己添加到struct eventpoll的单个等待队列中,并进入睡眠状态。
  4. 当一个事件发生在epoll()的套接字上时,它会调用epollcallbackstruct eventpoll ,将文件描述符添加到就绪列表中,并唤醒当前正在等待该struct eventpoll任何等待struct eventpoll

显然, struct eventpoll和各种列表和等待队列需要很多小心的locking,但这是一个实现细节。

重要的是要注意的是,我上面没有描述过一个遍历所有感兴趣的文件描述符的步骤。 通过完全基于事件,通过使用一套持久的fd和一个准备好的列表, epoll可以避免一个操作花费O(n)时间,其中n是被监视的文件描述符的数量。