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

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

Netty 系列目录(http://www.javashuo.com/article/p-hskusway-em.html)编程

既然 select/poll/epoll 都是 I/O 多路复用的具体的实现,之因此如今同时存在,其实他们也是不一样历史时期的产物api

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

1、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只需一次。数据结构

2、总结

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

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

相关文章
相关标签/搜索