【问题标题】:Why is epoll faster than select?为什么 epoll 比 select 快?
【发布时间】:2013-06-25 16:32:12
【问题描述】:

我看到很多比较说 select 必须遍历 fd 列表,这很慢。但是为什么 epoll 不必这样做呢?

【问题讨论】:

    标签: select epoll


    【解决方案1】:

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

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

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

    相比之下,epollepoll 套接字本身有一个等待列表。该过程只需要使用一个 thunk 就可以放在一个等待列表中。当进程唤醒时,只需将其从一个等待列表中移除,并且只需要释放一个 thunk。

    需要明确的是,对于epollepoll 套接字本身必须附加到这 200 个连接中的每一个。但是,对于每个连接,当它首先被接受时,它就完成一次。对于每个连接,当它被删除时,它会被拆除一次。相比之下,每次调用 select 阻塞都必须将进程添加到每个受监视套接字的等待队列中。

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

    【讨论】:

    • +1,但这是一个实现细节。操作系统可以缓存 thunk 注册,并且仅根据与先前调用的注册的差异来更新 thunk。我发现真正的杀手是你在上一段中提到的。
    • @jxh 是的。您可以实现 select 来实现这一点,在后台调用 epoll。您仍然需要缓存先前调用的注册、比较它们等等。 (你必须小心,因为同一个文件描述符可能引用不同的底层套接字。但内核可以很容易地分辨出来。)
    • @jxh:除了确定两组之间的差异是O(n),因此您实际上不会通过在操作系统级别缓存注册来保存任何内容。您可以将其缓存在应用程序级别,这可能有办法检测集合中的插入和删除,然后您只需要告诉操作系统差异。这正是 epoll 与 select/poll 的不同之处。
    • @LieRyan 不同之处在于在调用select 之间有一些东西留在等待队列中,因此不必轮询连续调用的所有公共套接字。
    • @DavidSchwartz 是对的。但这并不意味着epoll 总是比select/poll 快。使用selectpoll,所有fds 都被添加到用户空间,整个集合被复制到内核空间并返回。但是对于epoll,整个集合都维护在内核空间中,因此需要进行系统调用以将新文件描述符添加到此列表中(epoll_ctl)。系统调用很昂贵,并且在有许多短期活动连接的情况下,由于系统调用开销,epoll 会比 select 慢。参考:kernel.org/doc/ols/2004/ols2004v1-pages-215-226.pdf
    【解决方案2】:

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

    所以,再解释一下机制:

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

    显然,struct eventpoll 以及各种列表和等待队列需要大量小心锁定,但这是一个实现细节。

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

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2018-12-28
      • 1970-01-01
      • 1970-01-01
      • 2012-08-24
      • 2013-08-03
      • 2011-11-20
      • 2019-11-02
      相关资源
      最近更新 更多