文章目录
epoll编程接口
linux的epoll
(event poll
)API可以检查多个文件描述符上的I/O就绪状态。
epoll API
主要优点如下:
- 当检查大量的文件描述符时,
epoll
的性能延展性比select()
和poll()
高很多。 epoll API
既支持水平触发也支持边缘触发。与之相反,select()
和poll()
只支持水平触发,而信号驱动I/O只支持边缘触发。
性能表现上,epoll
同信号驱动I/O相似。但是,epoll
有一些胜过信号驱动I/O的优点。
- 可以避免复杂的信号处理流程(比如信号队列溢出时的处理)。
- 灵活性高,可以指定我们希望检查的事件类型(例如,检查套接字文件描述符的读就绪,写就绪或者两者同时指定)。
epoll API
是Linux
系统专有的。
epoll API
的核心数据结构称作epoll
实例,它和一个打开的文件描述符相关联。这个文件描述符不是用来做I/O操作的,相反,它是内核数据的句柄,这些内核数据结构实现了两个目的。
- 记录了在进程中声明过的感兴趣的文件描述符列表——
interest list
(兴趣列表)。 - 维护了处于I/O就绪态的文件描述符列表——
ready list
(就绪列表)。
ready list
中的成员是interest list
的子集。
epoll API
由以下3个系统调用组成。
- 系统调用
epoll_create
创建一个epoll
实例,返回代表该实例的文件描述符。 - 系统调用
epoll_ctl
操作同epoll
实例相关联的兴趣列表。通过epoll_ctl()
,我们可以增加新的描述符到列表中,将已有的文件描述符从该列表中移除,以及修改代表文件描述符上事件类型的位掩码。 - 系统调用
epoll_wait
返回与epoll
实例相关联的就序列表中的成员。
创建epoll实例:epoll_create()
系统调用epoll_create()
创建了一个新的epoll
实例,其对应的兴趣列表初始化为空。
#include <sys/epoll.h>
int epoll_create(int size);
//returns file descriptor on success,or -1 error
参数size
制定了我们想要通过epoll
实例来检查的文件描述符个数。该参数并不是一个上限,而是告诉内核应该如何为内部数据结构划分初始大小。
作为函数返回值,epoll_create()
返回了代表新创建的epoll
实例的文件描述符。这个文件描述符在其他几个epoll
系统调用中用来表示epoll
实例。当这个文件描述符不再需要时,应该通过close()
关闭。当所有与epoll
实例相关的文件描述符都被关闭时,实例被销毁,相关的资源都返回给系统。(多个文件描述符可能引用到相同的epoll
实例,这是由于调用了fork()
或者dup()
这样类似的函数所致)。
修改epoll的兴趣列表:epoll_ctl()
系统调用epoll_ctl
能够修改由文件描述符epfd
所代表的epoll
实例中的兴趣列表。
#include <sys/epoll.h>
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* ev);
//returns 0 on success,or -1 on error
参数fd
指明了要修改兴趣列表中的哪一个文件描述符的设定。该参数可以是代表管道,FIFO,套接字,POSIX消息队列,inotify实例,终端,设备,甚至是另一个epoll
实例的文件描述符。但是,这里fd
不能作为普通文件或目录的文件描述符。
参数op
用来指定需要执行的操作,它可以是如下几种值。
EPOLL_CTL_ADD
将描述符fd
添加到epoll
实例epfd
中的兴趣列表中去。对于fd
上我们感兴趣的事件,都指定在ev
所指向的结构体中。如果我们试图向兴趣列表中添加一个已存在的文件描述符,epoll_ctl
将出现EEXIST
错误。
EPOLL_CTL_MOD
修改描述符fd
上设定的事件,需要用到由ev
所指向的结构体中的信息。如果我们试图修改不在兴趣列表中的文件描述符,epoll_ctl()
将出现ENOENT
错误。
EPOLL_CTL_DEL
将文件描述符fd
从epfd
的兴趣列表中移除。该操作忽略参数ev
。如果我们试图移除一个不在epfd
的兴趣列表中的文件描述符,epoll_ctl()
将出现ENOENT
错误。关闭一个文件描述符会自动将其从所有的epoll
实例的兴趣列表中移除。
参数ev
是指向结构体epoll_event
的指针,结构体的定义如下:
struct epoll_event{
uint32_t event; //epoll events
epoll_data_t data; //user data
};
结构体epoll_event
中的data
字段的类型为
typedef union epoll_data{
void *ptr; //pointer to user-defined data
int fd; //file descriptor
uint32_t u32; //32-bit integer
uint64_t u64; //64-bit integer
}epoll_data_t;
参数ev
为文件描述符fd
所做的设置如下:
- 结构体
epoll_event
中的events
字段是一个位掩码,它指定了我们为待检查的描述符fd
上所感兴趣的事件集合。 data
字段是一个联合体,当描述符fd
稍后成为就绪态时,联合体的成员可用来指定传回给调用进程的信息。
int epfd;
struct epoll_event ev;
epfd = epoll_create(5);
if(epfd == -1)
errExit("epoll_create");
ev.data.fd = fd;
ev.events = EPOLLIN;
if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, ev) == -1)
errExit("epoll_ctl");
事件等待:epoll_wait()
系统调用epoll_wait()
返回epoll
实例中处于就绪态的文件描述符信息。
单个epoll_wait()
调用能返回多个就绪态文件描述符的信息。
#include <sys/epoll.h>
int epoll_wait(int epfd, struct epoll_event* evlist, int maxevents, int timeout);
//return number of ready file descriptor,0 on timeout,or -1 on error
参数evlist
所指向的结构体数组中返回的是有关就绪态文件描述符的信息。数组evlist
的空间由调用者负责申请,所包含的元素个数在参数maxevents
中指定。
在数组evlist
中,每个元素返回的是都是单个就绪态文件描述符的信息。events
字段返回了在该描述符上已经发生的事件掩码。data
字段返回的是我们在描述符上使用epoll_ctl
注册感兴趣的事件时在ev.data
中所指定的值。注意,data
字段是唯一可获知同这个事件相关的文件描述符号的途径。因此,当我们调用epoll_ctl()
将文件描述符添加到兴趣列表中时,应该要么将ev.data.fd
设置为文件描述符号,要么将ev.data.ptr
设置为指向包含文件描述符号的结构体。
参数timeout
用来确定epoll_wait()
的阻塞行为,由以下几种。
- 如果
timeout
等于-1,调用将一直阻塞,直到兴趣列表中的文件描述符上有事件产生,或者直到捕获到一个信号为止。 - 如果
timeout
等于0,执行一次非阻塞式的检查,看兴趣列表中的文件描述符上产生了哪个事件。 - 如果
timeout
大于0,调用将阻塞最多timeout
毫秒,直到文件描述符上有事件发生,或者直到捕获到一个信号为止。
调用成功后,epoll_wait
返回数组evlist
中的元素个数。如果在timeout
时间间隔内没有任何文件描述符处于就绪态的话,返回0。出错时返回-1,并在errno
中设定错误码以表示错误原因。
在多线程程序中,可以在一个线程中使用epoll_ctl()
将文件描述符添加到另一个线程中由epoll_wait()
所监听的epoll
实例的兴趣列表中去。这些对兴趣列表的修改将立刻得到处理,而epoll_wait
调用将返回有关新添加的文件描述符的就绪信息。
epoll事件
当我们调用epoll_ctl()
时可以在ev.events
中指定的位掩码以及由epoll_wait()
返回的evlist[].events
中的值在下表中给出。
位掩码 | 作为epoll_ctl的输入? | 由epoll_wait返回? | 描述 |
---|---|---|---|
EPOLLIN | 是 | 是 | 可读取非高优先级的数据 |
EPOLLPRI | 是 | 是 | 可读取高优先级数据 |
EPOLLRDHUP | 是 | 是 | 套接字对端关闭 |
EPOLLOUT | 是 | 是 | 普通数据可写 |
EPOLLET | 是 | 采取边缘触发事件通知 | |
EPOLLONESHOT | 是 | 在完成事件通知之后禁用检查 | |
EPOLLERR | 是 | 有错误发生 | |
EPOLLHUP | 是 | 出现挂断 |
EPOLLONESHOT标志
默认情况下,一旦通过epoll_ctl()
的EPOLL_CTL_ADD
操作将文件描述符添加到epoll
实例的兴趣列表中后,它会保持激活状态(即,之后对epoll_wait()
的调用会在描述符处于就绪态时通知我们)直到我们显式的通过epoll_ctl()
的EPOLL_CTL_DEL
操作将其从列表中移除。如果我们希望在某个特定的文件描述符上只得到一次通知,那么可以在传给epoll_ctl()
的ev.events
中指定EPOLLONESHOT
标志。如果指定了这个标志,那么在下一个epoll_wait()
调用通知我们对应的文件描述符处于就绪态之后,这个描述符就会在兴趣列表中被标记为非激活状态,之后的epoll_wait()
调用都不会再通知我们有关这个描述符的状态了。如果需要,我们可以稍后通过epoll_ctl()
的EPOLL_CTL_MOD
操作重新激活对这个文件描述符的检查。(这种情况下不能使用EPOLL_CTL_ADD
操作,因为非激活状态的文件描述符仍然还在epoll
实例的兴趣列表中)。
深入探究epoll的语义
现在我们来看看打开的文件同文件描述符以及epoll
之间交互的一些细微之处。
当我们通过epoll_create
创建一个epoll
实例时,内核在内存中创建了一个新的i-node
并打开文件描述,随后在调用进程中为打开的这个文件描述分配一个新的文件描述符。同epoll
实例的兴趣列表相关联的是打开的文件描述,而不是epoll
文件描述符。这将产生下列结果。
- 如果我们使用
dup()
(或类似的函数)复制一个epoll
文件描述符,那么被复制的描述符所指代的epoll
兴趣列表和就绪列表同原始的epoll
文件描述符相同。若要修改兴趣列表,在epoll_ctl()
的参数epfd
上设定文件描述符可以是原始的也可以复制的。 - 上一条观点同样也适用于
fork()
调用之后的情况。此时子进程通过继承复制了父进程的epoll
文件描述符,而这个复制的文件描述符所指向的epoll
数据结构同原始的描述符相同。
当我们执行epoll_ctl()
的EPOLL_CTL_ADD
操作时,内核在epoll
兴趣列表中添加了一个元素,这个元素同时记录了需要检查的文件描述符数量以及对应的打开文件描述的引用。epoll_wait()
调用的目的就是让内核负责监视打开的文件描述。这表示我们必须对之前的观点做改进:如果一个文件描述符是epoll
兴趣列表中的成员,当关闭它后会自动从列表中移除。改进版应该是这样的:一旦所有指向打开的文件描述的文件描述符都被关闭后,这个打开的文件描述符将从epoll
的兴趣列表中移除。这表示如果我们通过dup()
(或类似的函数)或者fork()
为打开的文件创建了描述符副本,那么这个打开的文件只会在原始的描述符以及所有其他的副本都被关闭时才会移除。
这些语义可导致出现某些令人惊讶的行为。假设我们执行下面的代码。即使文件描述符fd1
已经被关闭,这段代码中的epoll_wait()
调用也会告诉我们fd1
已经就绪。这是因为还有一个打开的文件描述符fd2
存在,它所指向的文件描述信息仍包含在epoll
的兴趣列表中。当两个进程持有对同一个打开文件的文件描述符副本时,也会出现相似的场景。执行epoll_wait()
操作的进程已经关闭了文件描述符,但是另一个进程仍然持有打开的文件描述符副本。
int epfd, fd1, fd2;
struct epoll_event ev;
struct epoll_event evlist[MAX_EVENTS];
ev.data.fd = fd1;
ev.events = EPOLLIN;
if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd1, ev) == -1)
errExit("epoll_ctl");
fd2 = dup(fd1);
close(fd1);
ready = epoll_wait(epfd, evlist, MAX_EVENTS, -1);
if(ready == -1)
errExit("epoll_wait");
文件描述
文件描述表示的是一个打开文件的上下文信息(大小,内容,编码等与文件有关的信息),可以比喻为一个抽屉,这部分内容实际上由内核来管理。而用户空间的应用程序如果要操作文件则么办。就是通过open()
这样的系统调用向内核请求,然后内核分配给用户空间一个文件描述符。这个文件描述符可以比喻为抽屉的把手,有了这个把手(文件描述符),用户就可以操作抽屉(文件描述)里的内容。但是,一个抽屉可以有多个把手(即文件描述可以对应多个文件描述符),只有当所有的把手(文件描述符)都关闭了,内核就知道此时没有用户空间的程序要用这个抽屉了(文件描述),那么就把它回收。
文件描述实际上是内核中的一个数据结构,而用户空间中的文件描述符只不过是一个整数,epoll
的兴趣列表实际关注的是内核中的数据结构。
epoll实现机制
当某个进程调用epoll_create()
方法时,Linux内核会创建一个eventpoll
结构体,这个结构体中有两个成员与epoll
的使用方式密切相关。
eventpoll
的结构体如下所示:
struct eventpoll{
/* 红黑树的根节点,这个红黑树存放所有添加到epoll中的需要监控的事件 */
struct rb_root rbr;
/* 双链表中存放将要通过epoll_wait返回给用户的满足条件的事件 */
struct list_head rdlist;
};
每个epoll
对象都有一个独立的eventpoll
结构体,用于存放epoll_ctl()
方法向epoll
对象中添加进来的需要监控的事件,这些事件都会存放到红黑树中,因此,添加重复的事件就可以通过红黑树识别出来。
所有添加到epoll
中的事件都与设备驱动程序建立回调关系,当相应的事件发生时会调用这个回调方法,这个回调函数的名字是ep_poll_callback()
,它会将发生的事件添加到rdlist
双向链表中。
在epoll
中,对于每一个事件,都会建立一个epitem
结构体,如下所示:
struct epitem
{
struct rb_node rbn; //红黑树节点
struct list_head rdllink; //双向链表节点
struct epoll_filefd ffd; //事件句柄信息
struct eventpoll *ep; //指向其所属的eventpoll对象
struct epoll_event event; //期待发生的事件类型
};
epoll为什么高效
- 不用重复传递。
select
和poll
每次调用时都需要将要要监控的socket
传递给内核,这需要将用户态的socket
列表复制到内核态。epoll
调用epoll_wait()
就相当于调用select
和poll
,但是这时却不用传递socket
句柄给内核,因为内核已经在epoll_ctl()
中拿到了要监控的句柄列表。 - 在内核中,一切皆文件,所以,
epoll
向内核注册了一个文件,用于存储上述被监控的socket
。当你调用epoll_create()
时,就会在这个虚拟的epoll
文件系统中创建一个file节点,这个file只服务于epoll
。 epoll
在被内核初始化时,同时会开辟出epoll
自己的内核高速cache区,用于安置每一个需要监控的socket
,这些socket
会以红黑树的形式保存在内核cache中,用来支持快速查找,插入,删除,这个内核高速cache区,就是建立连续的物理内存页,然后在其上建立slab层,slab层就是在物理上分配好你想要的size的内存对象,每次使用时都是使用空闲的已分配好的对象。- 在调用
epoll_create()
时,内核除了帮我们在epoll
文件系统里创建了file节点,在内核cache里创建红黑树用于存储之后epoll_ctl
传来的socket外,还会建立一个list
链表,用于存储准备就绪的事件,当epoll_wait
调用时,仅仅观察这个list
链表有没有数据即可,有数据就返回,没有数据就sleep
,等到timeout
时间到后即使链表没有数据也返回。 - 这个就绪
list
链表是怎么维护的?当执行epoll_ctl
时,除了把socket
放到epoll
文件系统中的file对象对应的红黑树上之外,还会给内核中断处理程序注册一个回调函数,告诉内核,如果这个句柄的中断到了,就把它放到准备就绪的list
链表中。所以,当一个socket
上有数据到了,内核就把网卡上的数据复制到内核中然后就把socket插入到list
链表中。 - epoll的基础就是回调。
对于水平触发和边缘触发
当一个socket句柄上有事件发生时,内核会把该句柄插入到准备就绪list链表中,这时调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,然后清空准备就绪list链表中,最后,epoll_wait()干了件事,就是检查这些socket,如果不是边缘模式,并且这些socket上确实有未处理的事件时,又把该句柄放到刚刚清空的准备就绪链表了。所以,非边缘触发的句柄,只要它上面还有事件,epoll_wait()每次都会返回这个句柄。可以看出,水平出发还有个回放的过程,降低了效率。
边缘触发与EPOLL_ONESHOT
处于边缘触发时需要循环读取,但是在读取的过程中,如果有新的事件到达,很可能出发了其他线程来处理这个socket
,这样就乱了。
EPOLL_ONESHOT就是用来避免这种情况的,在一个线程处理完一个socket的数据后,就是触发EAGAIN的时候,需要重置EPOLL_ONESHOT,这时候新到来的事件就可以重新进入触发流程了。
EPOLL_ONESHOT的原理就是在每次触发事件之后,就将事件注册从epollfd上清除了,不会再监听这个描述符,下次需要用到的时候,需要使用epoll_ctl的EPOLL_CTL_MOD来添加。
epoll同I/O多路复用的性能对比
为什么epoll
性能表现更好
- 每次调用
select()
和poll()
时,内核必须检查所有在调用中指定的文件描述符。与之相反,当通过调用epoll_ctl()
指定了需要监视的文件描述符时,内核会在与打开的文件描述上下文相关联的链表中记录该描述符,之后每当执行I/O操作使得文件描述符成为就绪态时,内核就在epoll
描述符的就绪列表中添加一个元素。(单个打开的文件描述符上下文中的一次I/O时间可能导致与之相关的多个文件描述符成为就绪态)。之后的epoll_wait()
调用从就绪列表中简单的取出这些元素。 - 每次调用
select()
或poll()
时,我们传递了一个标记了所有待监视的文件描述符的数据结构给内核,调用返回时,内核将所有标记为就绪态的文件描述符的数据结构再传回给我们。与之相反,在epoll
中我们使用epoll_ctl()
在内核空间中建立一个数据结构,该数据结构会将待监视的文件描述符都记录下来。一旦这个数据结构建立完成,稍后每次调用epoll_wait()
时就不需要再传递任何与文件描述符有关的信息给内核了,而调用返回的信息中只包含那些已经处于就绪态的描述符。
除了以上几点外,对于select()
来说,我们必须在每次调用之前先初始化输入数据。而无论是select()
还是poll()
,我们必须对返回的数据结构做检查,以此找出N个文件描述符中有哪些是处于就绪态的。
粗略来看,我们可以认为当N(被监视的文件描述符数量)取值很大时,select()
和poll()
的性能会随着N的增大而线性下降。
与之相反,epoll
的性能会根据发生I/O事件的数量而扩展(呈线性)。因此常见的能够高效使用epoll API
的应用场景就是需要同时处理许多客户端的服务器:需要监视大量的文件描述符,但大部分处于空闲状态,只有少数文件描述符处于就绪态。
边缘触发通知
默认情况下epoll
提供的是水平触发通知。这表示epoll
会告诉我们何时能在文件描述符上以非阻塞的方式执行I/O操作。
epoll API
还能以边缘触发方式进行通知——也就是说,会告诉我们自从上一次调用epoll_wait()
以来文件描述符上是否已经有I/O活动了(或者由于描述符被打开了,如果之前没有调用的话)。使用epoll
的边缘触发通知在语义上类似于信号驱动I/O,只是如果有多个I/O事件发生的话,epoll
会将它们会合并成一次单独的通知,通过epoll_wait()
返回,而在信号驱动I/O中则可能会产生多个信号。
要使用边缘触发通知,我们在调用epoll_ctl()
时在ev.events
字段中指定EPOLLET
标志。
struct epoll_event ev;
ev.data.fd = fd;
ev.events = EPOLLIN | EPOLLET;
if(epoll_ctl(epfd, EPOLL_CTL_ADD, fd, ev) == -1)
errExit("epoll_ctl");
我们通过一个例子来说明epoll
的水平触发和边缘触发通知之间的区别。假设我们使用epoll
来监视一个套接字上的输入(EPOLLIN),接下来会发生如下的事件。
- 套接字上有输入到来。
- 我们调用一次
epoll_wait()
。无论我们采用的是水平触发还是边缘触发通知,该调用都会告诉我们套接字已经处于就绪态了。 - 再次调用
epoll_wait()
。
如果我们采用的是水平触发通知,那么第二个epoll_wait()
调用将告诉我们套接字处于就绪态。而如果我们采用边缘触发通知,那么第二个epoll_wait()
调用将阻塞,因为自从上一次调用epoll_wait()
以来并没有新的输入到来。
边缘触发通知通常和非阻塞的文件描述符结合使用。
因而,采用epoll
的边缘触发通知机制的程序基本框架如下。
- 让所有待监视的文件描述符都成为非阻塞的。
- 通过
epoll_ctl()
构建epoll
的兴趣列表。 - 通过如下的循环处理I/O事件。
- 通过
epoll_wait()
取得处于就绪态的描述符列表。 - 针对每一个处于就绪态的文件描述符,不断进行i/o处理直到相关的系统调用(例如
read,write,recv,send,accept
)返回EAGAIN
或EWOULDBLOCK
错误。
- 通过
当采用边缘触发通知时避免出现文件描述符饥饿现象
假设我们采用边缘触发通知监视多个文件描述符,其中一个处于就绪态的文件描述符上有着大量的输入存在(可能是不间断的输入流)。如果在检测到该文件描述符处于就绪态后,我们将尝试通过非阻塞式的读操作将所有的输入都读取,那么此时就会有使其他的文件描述符处于饥饿状态的风险存在(即,在我们再次检查这些文件描述符是否处于就绪态并执行I/O操作前会有很长的一段处理时间)。该问题的一种解决方案是让应用程序维护一个列表,列表中中存放着已经被通知为就绪态的文件描述符。通过一个循环按照如下的方式不断处理。
- 调用
epoll_wait()
监视文件描述符,并将处于就绪态的描述符添加到应用程序维护的列表中。如果这个文件描述符已经注册到应用程序维护的列表中,那么这次监视操作的超时时间应该设置为较小的值或者是0。这样如果没有新的文件描述符成为就绪态,应用程序就可以迅速进行到下一步,去处理那些已经处于就绪态的文件描述符了。 - 在应用程序维护的列表中,只在那些已经注册为就绪态的文件描述符上进行一定限度的I/O操作(可能是以轮转调度方式循环处理,而不是每次
epoll_wait()
调用后都从列表头开始处理)。当相关的非阻塞I/O系统调用出现了EAGAIN
或EWOULDBLOCK
错误时,文件描述符就可以从应用程序维护的列表中移除了。
尽管采用这种方法需要做些额外的编程工作,但是除了能避免出现文件描述符饥饿现象外,我们还能获得其他益处。比如,我们可以在上述循环中加入其他的步骤,比如处理定时器以及用sigwaitinfo()
来接收信号。
因为信号驱动I/O也是采用的边缘触发通知机制,因此也需要考虑文件描述符饥饿的情况。与之相反,在采用水平触发通知机制的应用程序中,考虑文件描述符饥饿的情况并不是必须的。这是因为我们可以采用水平触发通知在非阻塞式的文件描述符上通过循环连续地检查描述符的就绪状态,然后在下一次检查文件描述符的状态前在处于就绪态的描述符上做一些I/O处理就可以了。
三种I/O复用函数的比较
这3组函数都通过某种结构体变量来告诉内核监听哪些文件描述符上的哪些事件,并使用该结构体类型的参数来获取内核处理的结果。select
的参数需要提供3个这种类型的参数来分别传入和输出可读,可写以及异常事件。这一方面使得select
不能处理更多类型的事件,另一方面由于内核对fd_set
集合的在线修改,应用程序下次调用select
前不得不重置这3个fd_set
集合。poll
的参数类型pollfd
,它把文件描述符和事件都定义其中,任何事件都被统一处理,从而使得编程接口简洁得多。并且内核每次修改的是pollfd
结构体的revents
成员,而events
成员保持不变,因此下次调用poll
时应用程序无须重置pollfd
类型的事件集参数。由于每次select
和poll
调用都返回整个用户注册的事件集合(其中包括就绪的和未就绪的),所以应用程序索引就绪文件描述符的时间复杂度为
O
(
n
)
O(n)
O(n)。epoll
则采用与select
和poll
完全不同的方式来管理用户注册的事件。它在内核中维护一个事件表,并提供了一个独立的系统调用epoll_ctl
来控制往其中添加,删除,修改事件。这样,每次epoll_wait
调用都直接从该内核事件表中取得用户注册的事件,而无须反复从用户空间读入这些事件。epoll_wait
系统调用的events
参数仅用来返回就绪的事件,这使得应用程序索引就绪文件描述符的事件复杂度达到了
O
(
1
)
O(1)
O(1)。
poll
和epoll_wait
分别用nfds
和maxevents
参数指定最多监听多少个文件描述符和事件。这两个数值都能达到系统允许打开的最大文件描述符数目,即65535。而select
允许监听的最大文件描述符数量通常有限制。虽然用户可以修改这个限制,但这可能导致不可预期的后果。
select
和poll
都只能工作在相对低效的LT模式,而epoll
则可以工作在ET高效模式。并且epoll
还支持EPOLLONESHOT
事件。该事件能进一步减少可读,可写和异常等事件被触发的次数。
从实现原理上来说,select
和poll
采用的都是轮询的方式,即每次调用都要扫描整个注册文件描述符集合,并将其中就绪的文件描述符返回给用户程序,因此它们检测就绪事件的算法的时间复杂度是
O
(
n
)
O(n)
O(n)。epoll_wait
则不同,它采用的是回调的方式。内核检测到就绪的文件描述符时,将触发回调函数,回调函数就将该文件描述符上对应的事件插入内核就绪事件队列。内核最后在适当的时间将该就绪队列中的内容拷贝到用户空间。因此epoll_wait
无须轮询整个文件描述符集合来检测哪些事件已经就绪,其算法时间复杂度是
O
(
1
)
O(1)
O(1)。但是,当活动连接比较多的时候,epoll_wait
的效率未必比select
和poll
高,因为此时回调函数被触发得过于频繁。所以epoll_wait
适用于连接数量多,但活动连接较少的情况。
系统调用 | select | poll | epoll |
---|---|---|---|
事件集合 | 用户通过3个参数分别传入感兴趣的可读,可写以及异常等事件,内核通过对这些参数的在线修改来反馈其中的就绪事件。这使得用户每次调用select 都要重置这3个参数 | 统一处理所有事件类型,因此只需一个事件集参数。用户通过pollfd.events 传入感兴趣的事件,内核通过修改pollfd.revents 反馈其中就绪的事件 | 内核通过一个事件表直接管理用户感兴趣的所有事件,因此每次调用epoll_wait 时,无需反复传入用户感兴趣的事件。epoll_wait 系统调用的参数events 仅用来反馈就绪的事件 |
应用程序索引就绪文件描述符的时间复杂度 | O ( n ) O(n) O(n) | O ( n ) O(n) O(n) | O ( 1 ) O(1) O(1) |
最大支持文件描述符数 | 一般有最大值限制 | 65535 | 65535 |
工作模式 | LT | LT | LT和ET |
内核实现和工作效率 | 采用轮询方式来检测就绪事件,算法时间复杂度为 O ( n ) O(n) O(n) | 采用轮询方式来检测就绪事件,算法时间复杂度为 O ( n ) O(n) O(n) | 采用回调方式来检测就绪事件,算法时间复杂度为 O ( 1 ) O(1) O(1) |