1. 背景
I/O多路复用的实现方案select、poll和epoll的区别是常见的面试题,在搜索引擎搜索,通常会给出如下的答案:
epoll相对于select、poll拥有如下的优势
1、没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口)。
2、效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。
3、内存开销减少,即epoll减少内存复制的开销。
这样的答案大体没错,但是我们需要深究一下,为什么会有这样的优势?为什么epoll的效率有提升?
本文讲重点剖析select、poll、epoll的机制并解释三者的异同。
2. 历史
I/O多路复用允许应用程序同时检查多个文件描述符(以下简称fd),查看任意一个是否可执行I/O操作。
首先出现的系统调用是select,它首次出现在BSD系统的套接字API中;其次出现是系统调用poll,它出现在System V中。 select和poll都是SUSv3中规定的接口。
这里顺便解释一下BSD,System V和SUSv3:
BSD(伯克利软件发布:Berkeley Software Distribution),UNIX之父Ken Thompson于1975/1976学年与他的研究生们为UNIX开发的新版本;
System V,1982年AT&T由于反托拉斯法案拆分后,获准销售UNIX,在1981年发布System III,1983年发布System V;
单一UNIX规范(Single UNIX Specification,缩写为SUS),它是一套UNIX系统的统一规格书。扩充了POSIX标准,定义了标准UNIX操作系统。由IEEE与The Open Group所提出,目前由Austin Group负责维持,目前最新版本为SUSv4 2018 Edition。
3. 三种系统调用详解
3.1 select
select系统调用的的用途是:在一段指定的时间内,监听用户感兴趣的文件描述符上可读、可写和异常等事件。
#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
参数说明:
maxfdp:被监听的文件描述符的总数,它比所有文件描述符集合中的文件描述符的最大值大1,因为文件描述符是从0开始计数的。
readfds、writefds、exceptset:分别指向可读、可写和异常等事件对应的描述符集合。
timeout:用于设置select函数的超时时间,即告诉内核select等待多长时间之后就放弃等待。timeout == NULL 表示等待无限长的时间。
术语“异常情况”常常被误解为在文件描述符上出现了一些错误,这并不正确。在Linux上,一个异常情况只会在下面两种情况下发生:
- 连接到处于信包模式下的伪终端主设备上的从设备状态发生了改变;
- 流式套接字上接收到了带外数据。
文件描述符集合有一个最大容量限制,由常量FD_SETSIZE来决定。在Linux上,该常量的值为1024。(其他UNIX实现对于该限制也有类似的常量值来限定。)
至此我们可以看出select系统调用具有如下缺点:
- fd set保存了调用结果,因此fd set无法复用,应用程序每次调用select时,都需要重新构建fd set数据结构;
- 每次调用都需要拷贝fd set到内核空间,并且遍历整个fd set,处理时长随fd长度增加线性增长;
- 应用程序每次调用结束后,当返回值大于0时,都需要遍历并处理整个fd set结构;
- fd set长度首先,必须小于FD_SETSIZE常量。
3.2 poll
系统调用poll()执行的任务同select()很相似。两者间主要的区别在于我们要如何指定待检查的文件描述符。在select()中,我们提供三个集合,在每个集合中标明我们感兴趣的文件描述符。而在poll()中我们提供一列文件描述符,并在每个文件描述符上标明我们感兴趣的事件。
#include <poll.h>
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
参数定义:
- fds:需要poll()来检查的文件描述符;
- nfds:数组fds中元素的个数;
- timeout:超时时间。
pollfd结构体定义如下:
struct pollfd {
int fd; /* 文件描述符 */
short events; /* 请求的事件类型,监视驱动文件的事件掩码 */
short revents; /* 驱动文件实际返回的事件 */
};
综上可以看出,poll作为后推出的一个系统调用,解决了select的一些问题,但是仍然存在一些致命缺点。
poll系统调用具有如下优缺点:
- fds结构采用链表结构,不再受FD_SETSIZE常量限制;
- 调用结果保存在revents掩码中,fds可以复用;
- 每次调用都需要拷贝fds到内核空间,并且遍历整个fds,处理时长随fds长度增加线性增长;
- 应用程序每次调用结束后,当返回值大于0时,都需要遍历并处理整个fds;
3.3 epoll
从3.1和3.2,我们找到一些select和poll共同存在的一些致命缺点:
- 每次调用select或poll,内核都必须检查所有被指定的文件描述符,看它们是否处于就绪态。当检查大量处于密集范围内的文件描述符时,该操作耗费的时间将大大超过接下来的操作;
- 每次调用select或poll时,程序都必须传递一个表示所有需要被检查的文件描述符的数据结构到内核,内核检查过描述符后,修改这个数据结构并返回给程序;
- select或poll调用完成后,程序必须检查返回的数据结构中的每个元素,以此查明哪个文件描述符处于就绪态了。
#include <sys/epoll.h>
int epoll_create ( int size );
int epoll_ctl ( int epfd, int op, int fd, struct epoll_event *event );
int epoll_wait ( int epfd, struct epoll_event* events, int maxevents, int timeout );
3.3.1 epoll_create
系统调用epoll_create()创建了一个新的epoll实例,其对应的兴趣列表初始化为空。
参数定义:
- size:fd个数的初始大小,自2.6.8后,此入参忽略不用。
返回值定义:
epoll对应的fd,当不需要监听时,关闭此fd。
3.3.2 epoll_ctl
系统调用epoll_ctl()能够修改由文件描述符epfd所代表的epoll实例中的兴趣列表。
参数定义:
- epfd:epoll对应的fd;
- fd:受检查的fd;
- op:操作类型,添加、删除、修改(EPOLL_CTL_ADD、EPOLL_CTL_DEL、EPOLL_CTL_MOD);
- event:监听的事件掩码和回调信息。
3.3.3 epoll_wait
系统调用epoll_wait()返回epoll实例中处于就绪态的文件描述符信息。单个epoll_wait()调用能返回多个就绪态文件描述符的信息。
参数定义:
epfd:epoll对应的fd;
events:时间数组,空间由应用程序分配;
maxevents:events最大数量;
timeout:超时时间。
3.3.4 epoll的优势
- 检查大量fd时,epoll具有明显的性能优势;
- epoll支持水平触发和边缘触发,select和poll仅支持水平触发。
3.4 异步I/O aio
Linux Kernel AIO 已经推出很长时间了,但是由于自身的原因,以及JAVA主要的IO框架如netty不支持,在JAVA圈应用较少。
主要的系统调用如下:
// 创建一个异步IO上下文(io_context_t是一个句柄)
int io_setup(int maxevents, io_context_t *ctxp);
// 销毁一个异步IO上下文(如果有正在进行的异步IO,取消并等待它们完成)
int io_destroy(io_context_t ctx);
// 提交异步IO请求
long io_submit(aio_context_t ctx_id, long nr, struct iocb **iocbpp);
// 取消一个异步IO请求
long io_cancel(aio_context_t ctx_id, struct iocb *iocb, struct io_event *result);
// 等待并获取异步IO请求的事件
long io_getevents(aio_context_t ctx_id, long min_nr, long nr, structio_event *events, struct timespec *timeout);
4. 性能对比
从网上找到一个:poll()、select()以及epoll在2.6.25内核下进行100000次监视操作所花费的时间对比表格。
fd number | poll 秒 | select 秒 | epoll 秒 |
---|---|---|---|
10 | 0.61 | 0.73 | 0.41 |
100 | 2.9 | 3.0 | 0.42 |
1000 | 35 | 35 | 0.53 |
10000 | 990 | 930 | 0.66 |
粗略来看,我们可以认为当N(被监视的文件描述符数量)取值很大时,select()和poll()的性能会随着N的增大而线性下降。
与之相反的是,epoll的性能会根据发生I/O事件的数量而扩展(呈线性)。因此常见的能够高效使用epollAPI的应用场景就是需要同时处理许多客户端的服务器:需要监视大量的文件描述符,但大部分处于空闲状态,只有少数文件描述符处于就绪态。这一应用场景也是互联网行业常见的场景。