【发布时间】:2011-03-21 21:28:14
【问题描述】:
简短的问题,但对我来说很难理解。
为什么 ePoll 的扩展性比 Poll 更好?
【问题讨论】:
标签: epoll
简短的问题,但对我来说很难理解。
为什么 ePoll 的扩展性比 Poll 更好?
【问题讨论】:
标签: epoll
虽然 Damon 的原因对于您从不阻塞套接字的异常情况是正确的,但在典型的实际程序中,原因完全不同。一个典型的程序如下所示:
1) 做我们现在能做的所有工作。
2) 检查是否有网络连接需要服务,无事则阻塞。
3) 为发现的任何网络连接提供服务。
4) 转到步骤 1。
通常,因为您只是完成了所有可以做的工作,所以当您回到第 2 步时,您就没有工作要做了。所以你需要稍等片刻。现在,假设有 800 个您感兴趣的套接字。内核必须为这 800 个套接字中的每一个放入等待队列。而且,一瞬间,当数据到达这 800 个套接字中的一个时,内核必须将您从这 800 个等待队列中删除。将任务放在等待队列上需要创建一个“thunk”以将该任务链接到该等待队列。没有好的优化是不可能的,因为内核不知道你将等待哪个 800 个套接字。
使用epoll,epoll 套接字本身有一个等待队列,进程只被放在一个等待队列中。需要一个 thunk 将 800 个连接中的每一个连接到 epoll 等待队列,但该 thunk 是持久的。您可以通过将套接字添加到 epoll 集合来创建它,并且它会一直保留在那里,直到您从集合中移除该套接字。
当套接字上有活动时,内核会在检测到活动的任务中处理它。当您等待时,内核已经知道是否检测到事件并且内核只需要将您放在那个等待队列中。当你醒来时,它只需要将你从那个队列中移除。
因此,select 或 poll 的杀手锏与其说是复制,不如说是内核必须在每次阻塞操作时操纵大量等待队列。
【讨论】:
poll 系统调用每次都需要将文件描述符列表复制到内核。 epoll_ctl 只会发生一次,但不是每次调用 epoll_wait 时都会发生这种情况。
另外,epoll_wait 是 O(1) 观察描述符的数量1,这意味着无论您等待一个描述符还是等待 5,000 或 50,000 个描述符都没有关系。 poll,虽然比 select 更有效,但每次仍然需要遍历列表(即,就描述符数量而言,它是 O(N))。
最后,除了“正常”模式之外,epoll 还可以在“边缘触发”模式下工作,这意味着内核不需要在收到准备就绪信号后跟踪您读取了多少数据。这种模式更难掌握,但效率更高。
epoll_wait 对于发生的事件当然仍然是 O(N)。几乎没有任何方法可以与任何界面有所不同。如果 N 个事件发生在被监视的描述符上,那么应用程序需要获得 N 个通知,并且需要在为了对正在发生的事情做出反应。M <= N 获得M 事件。在边缘触发模式下,当同一事件(例如,POLLIN)发生多次时,您可能会收到较少的通知,可能只有一个。但是,这对于 big-O 符号本身并没有太大变化。
但是,epoll_wait 与观看的描述符数量无关。假设它以预期的“正常”方式使用(即,许多描述符,很少的事件),这才是真正重要的,这里确实是O(1)。
作为一个类比,您可以想到一个哈希表。哈希表在O(1) 中访问其内容,但有人可能会争辩说计算哈希 实际上是O(N) 的密钥长度。这在技术上是绝对正确的,并且可能存在这样的问题,但是,对于大多数人来说,这无关紧要。
【讨论】:
epoll_wait 是 O(N),而不是 O(1)。 epoll_wait 发现的套接字数量为 O(N),就像 poll 一样。每个发现的套接字都必须复制到应用程序的epoll_event 结构中。如果将轮询的套接字数量增加一倍,那么预期有活动的套接字数量就会增加一倍。
epoll_wait 同时是O(N) 和O(1)。但重要的是它是O(1)为它设计的“正常”场景。假设您观察到许多(数十、数百、数千)描述符,而实际上一次发生的事件很少(通常少于 5 个)。 epoll 不能做“魔术”,就发生的事件而言,它不是(也不能是)O(1) - 如果发生 N 个事件,那么 something 必须为 N 个事件完成。但是,如果 N ≈ 1,则实际上与 O(1) 一样好。更重要的是...
O(1)。因为如果你有 N ≈ 10000,那么 O(1) 或 O(N) 就不是一回事了(很明显)。在这里,真的,真的很重要。您的反对意见是正确的,如果您以一种与预期用途相反的方式使用 epoll 接口并且期望“魔术”发生,那么您可能会感到失望。在这种情况下,普通的 poll 可能既容易又快。我在 this lengthy answer 中提到了这一点,其中谈到了 epoll 等(大约在中间)。