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