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

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

原文地址:https://www.cnblogs.com/binarylei/p/11130079.html