目录html
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
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 机制的问题服务器
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 的性能开销问题。网络
// 函数建立一个 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 做了不少重大改进,现总结以下。并发
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 # 最大上限值
传统 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 之上了。
不管是 select、poll 仍是 epoll 都须要内核把 FD 消息通知给用户空间,如何避免没必要要的内存复制就显得很是重要,epoll 是经过内核和用户空间 mmap 同一块内存来实现的。
包括建立一个 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 的接口实现比较原始。
比较 | 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 方式可能性能更好。
参考:
天天用心记录一点点。内容也许不重要,但习惯很重要!