Linux NIO 系列(04-4) select、poll、epoll 对比

Linux NIO 系列(04-4) select、poll、epoll 对比

Netty 系列目录(https://www.cnblogs.com/binarylei/p/10117436.html)

既然 select/poll/epoll 都是 I/O 多路复用的具体的实现,之所以现在同时存在,其实他们也是不同历史时期的产物

  • select 出现是 1984 年在 BSD 里面实现的
  • 14 年之后也就是 1997 年才实现了 poll,其实拖那么久也不是效率问题, 而是那个时代的硬件实在太弱,一台服务器处理1千多个链接简直就是神一样的存在了,select 很长段时间已经满足需求
  • 2002, 大神 Davide Libenzi 实现了 epoll

一、API 对比

1.1 select API

int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);
int FD_ZERO(int fd, fd_set *fdset);     // 一个 fd_set 类型变量的所有位都设为 0
int FD_CLR(int fd, fd_set *fdset);      // 清除某个位时可以使用
int FD_SET(int fd, fd_set *fd_set);     // 设置变量的某个位置位
int FD_ISSET(int fd, fd_set *fdset);    // 测试某个位是否被置位

select() 的机制中提供一种 fd_set 的数据结构,实际上是一个 long 类型的数组,每一个数组元素都能与一打开的文件句柄建立联系(这种联系需要自己完成),当调用 select() 时,由内核根据IO 状态修改 fd_set 的内容,由此来通知执行了 select() 的进程哪一 Socket 或文件可读。

select 机制的问题

  1. 每次调用 select,都需要把 fd_set 集合从用户态拷贝到内核态,如果 fd_set 集合很大时,那这个开销也很大
  2. 同时每次调用 select 都需要在内核遍历传递进来的所有 fd_set,如果 fd_set 集合很大时,那这个开销也很大
  3. 为了减少数据拷贝带来的性能损坏,内核对被监控的 fd_set 集合大小做了限制(默认为 1024)

1.2 poll API

int poll(struct pollfd *fds, nfds_t nfds, int timeout);
struct pollfd {
    int fd;         // 文件描述符
    short events;   // 感兴趣的事件
    short revents;  // 实际发生的事件
};

poll 的机制与 select 类似,与 select 在本质上没有多大差别,管理多个描述符也是进行轮询,根据描述符的状态进行处理,但是 poll 没有最大文件描述符数量的限制。也就是说,poll 只解决了上面的问题 3,并没有解决问题 1,2 的性能开销问题。

1.3 epoll API

// 函数创建一个 epoll 句柄,实际上是一棵红黑树
int epoll_create(int size);
// 函数注册要监听的事件类型,op 表示红黑树进行增删改
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 函数等待事件的就绪,成功时返回就绪的事件数目,调用失败时返回 -1,等待超时返回 0
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);

epoll 在 Linux2.6 内核正式提出,是基于事件驱动的 I/O 方式,相对于 select 来说,epoll 没有描述符个数限制,使用一个文件描述符管理多个描述符,将用户关心的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。

二、总结

I/O 多路复用技术在 I/O 编程过程中,当需要同时处理多个客户端接入请求时,可以利用多线程或者 I/O 多路复用技术进行处理。I/O 多路复用技术通过把多个 I/O 的阻塞复用到同一个 select 的阻塞上,从而使得系统在单线程的情况下可以同时处理多个客户端请求。与传统的多线程/多进程模型比,I/O 多路复用的最大优势是系统开销小,系统不需要创建新的额外进程或者线程,也不需要维护这些进程和线程的运行,降低了系统的维护工作量,节省了系统资源,I/O多路复用的主要应用场景如下:

  • 服务器需要同时处理多个处于监听状态或者多个连接状态的套接字
  • 服务器需要同时处理多种网络协议的套接字。

目前支持 I/O 多路复用的系统调用有 select、pselect、poll、epoll,在 Linux 网络编程过程中,很长一段时间都使用 select 做轮询和网络事件通知,然而 select 的一些固有缺陷导致了它的应用受到了很大的限制,最终 Linux 不得不在新的内核版本中寻找 select 的替代方案,最终选择了 epoll。epoll 与 select 的原理比较类似,为了克服 select 的缺点, epoll 作了很多重大改进,现总结如下。

2.1 支持一个进程打开的 socket 描述符(FD)不受限制(仅受限于操作系统的最大文件句柄数)

select、poll 和 epoll 底层数据各不相同。select 使用数组;poll 采用链表,解决了 fd 数量的限制;epoll 底层使用的是红黑树,能够有效的提升效率。

select 最大的缺陷就是单个进程所打开的 FD 是有一定限制的,它由 FD_SETSIZE 设置,默认值是 1024。对于那些需要支持上万个 TCP 连接的大型服务器来说显然太少了。可以选择修改这个宏然后重新编译内核,不过这会带来网络效率的下降。我们也可以通过选择多进程的方案(传统的 Apache 方案)解决这个问题,不过虽然在 Linux 上创建进程的代价比较小,但仍旧是不可忽视的。另外,进程间的数据交换非常麻烦,对于 Java 来说,由于没有共享内存,需要通过 Socket 通信或者其他方式进行数据同步,这带来了额外的性能损耗,増加了程序复杂度,所以也不是一种完美的解决方案。值得庆幸的是, epoll 并没有这个限制,它所支持的 FD 上限是操作系统的最大文件句柄数,这个数字远远大于 1024。例如,在 1GB 内存的机器上大约是 10 万个句柄左右,具体的值可以通过 cat proc/sys/fs/file-max 查看,通常情况下这个值跟系统的内存关系比较大。

# (所有进程)当前计算机所能打开的最大文件个数。受硬件影响,这个值可以改(通过limits.conf)
cat /proc/sys/fs/file-max

# (单个进程)查看一个进程可以打开的socket描述符上限。缺省为1024
ulimit -a 
# 修改为默认的最大文件个数。【注销用户,使其生效】
ulimit -n 2000

# soft软限制 hard硬限制。所谓软限制是可以用命令的方式修改该上限值,但不能大于硬限制
vi /etc/security/limits.conf
* soft nofile 3000      # 设置默认值。可直接使用命令修改
* hard nofile 20000     # 最大上限值

2.2 I/O 效率不会随着 FD 数目的増加而线性下降

传统 select/poll 的另一个致命弱点,就是当你拥有一个很大的 socket 集合时,由于网络延时或者链路空闲,任一时刻只有少部分的 socket 是“活跃”的,但是 select/poll 每次调用都会线性扫描全部的集合,导致效率呈现线性下降。 epoll 不存在这个问题,它只会对“活跃”的 socket 进行操作一一这是因为在内核实现中, epoll 是根据每个 fd 上面的 callback 函数实现的。那么,只有“活跃”的 socket オ会去主动调用 callback 函数,其他 idle 状态的 socket 则不会。在这点上, epoll 实现了一个伪 AIO。针对 epoll 和 select 性能对比的 benchmark 测试表明:如果所有的 socket 都处于活跃态 - 例如一个高速 LAN 环境, epoll 并不比 select/poll 效率高太多;相反,如果过多使用 epoll_ctl,效率相比还有稍微地降低但是一旦使用 idle connections 模拟 WAN 环境, epoll 的效率就远在 select/poll 之上了。

2.3 使用 mmap 加速内核与用户空间的消息传递

无论是 select、poll 还是 epoll 都需要内核把 FD 消息通知给用户空间,如何避免不必要的内存复制就显得非常重要,epoll 是通过内核和用户空间 mmap 同一块内存来实现的。

2.4 epoll API 更加简单

包括创建一个 epoll 描述符、添加监听事件、阻塞等待所监听的事件发生、关闭 epoll 描述符等。

值得说明的是,用来克服 select/poll 缺点的方法不只有 epoll, epoll 只是一种 Linux 的实现方案。在 freeBSD 下有 kqueue,而 dev/poll 是最古老的 Solaris 的方案,使用难度依次递增。 kqueue 是 freeBSD 宠儿,它实际上是一个功能相当丰富的 kernel 事件队列,它不仅仅是 select/poll 的升级,而且可以处理 signal、目录结构变化、进程等多种事件。 kqueue 是边缘触发的。 /dev/poll 是 Solaris 的产物,是这一系列高性能 API 中最早出现的。 Kernel 提供了一个特殊的设备文件 /dev/poll,应用程序打开这个文件得到操作 fd_set 的句柄,通过写入 polled 来修改它,一个特殊的 ioctl 调用用来替换 select。不过由于出现的年代比较早,所以 /dev/poll 的接口实现比较原始。

附表1: select/poll/epoll 区别

比较 select poll epoll
操作方式 遍历 遍历 回调
底层实现 数组 链表 红黑树
IO效率 每次调用都进行线性遍历,
时间复杂度为O(n)
每次调用都进行线性遍历,
时间复杂度为O(n)
事件通知方式,每当fd就绪,
系统注册的回调函数就会被调用,
将就绪fd放到readyList里面,
时间复杂度O(1)
最大连接数 1024 无上限 无上限
fd拷贝 每次调用select,
都需要把fd集合从用户态拷贝到内核态
每次调用poll,
都需要把fd集合从用户态拷贝到内核态
调用epoll_ctl时拷贝进内核并保存,
之后每次epoll_wait不拷贝

总结:epoll 是 Linux 目前大规模网络并发程序开发的首选模型。在绝大多数情况下性能远超 select 和 poll。目前流行的高性能 web 服务器 Nginx 正式依赖于 epoll 提供的高效网络套接字轮询服务。但是,在并发连接不高的情况下,多线程+阻塞 I/O 方式可能性能更好。

参考:

  1. IO多路复用的三种机制Select,Poll,Epoll

每天用心记录一点点。内容也许不重要,但习惯很重要!

posted on 2019-07-04 06:05  binarylei  阅读(2801)  评论(1编辑  收藏  举报

导航