为什么epoll比选择更快?

ama*_*jxq 53 select epoll

我看过很多比较,其中说select必须遍历fd列表,这很慢.但为什么epoll不能做到这一点?

Dav*_*rtz 96

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

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

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

相比之下,同epoll时,epoll插座本身有一个等待列表.只需要使用一个thunk,就只需要将该进程放在一个等待列表中.当进程唤醒时,需要仅从一个等待列表中删除它,并且只需要释放一个thunk.

需要明确的是,随着epoll时,epoll本身具有插座附连到每个那些200个连接.但是,对于每个连接,当它首先被接受时,这样做一次.对于每个连接,当它被移除时,这会被拆除一次.相反,对每个select块的调用必须将进程添加到每个被监视的套接字的等待队列中.

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

  • +1,但这是一个实现细节.操作系统可以缓存thunk注册,并仅根据前一次调用注册的差异更新thunk.我发现真正的杀手就是你在上一段中提到的. (2认同)

小智 20

之间的主要区别epollselect是在select()文件描述符的名单要等上只存在一个单一的时间select()调用,调用任务只停留在插座等待队列单个呼叫的持续时间.在epoll,在另一方面,创建能够从多个其他文件描述符要等待事件的单一文件描述符,所以监控FD的名单是持久的,并且任务留在跨多个系统调用插座等待队列.此外,由于epollfd可以在多个任务之间共享,因此它不再是等待队列上的单个任务,而是一个本身包含另一个等待队列的结构,其中包含当前在epollfd 上等待的所有进程.(在实现方面,这是由套接字的等待队列抽象的,它们持有一个函数指针和一个void*传递给该函数的数据指针).

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

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

显然,需要对struct eventpoll各种列表和等待队列进行大量细致的锁定,但这是一个实现细节.

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