引用
select的原理和用法:FD_SET的使用
写在前面
在阻塞模型中,线程发起IO会阻塞直到IO完成才返回。在单线程无并发的工作中这并无不妥,但如果是一套需要高并发的逻辑,例如网络服务器,此时这种模型显然不合理。
从本文开始介绍经典常用的非阻塞IO模型,各种模型如何调配调度对于IO调用的合理安排能大大提高系统的效率、提升吞吐率降低系统资源调度的额外开销。
模型阐释
从 IO模型:2、阻塞模型 结合 IO模型:1、IO模型初识 知识,我们可以知道accept函数其实是从已连接队列中取出一个就绪的套接字进行IO数据拷贝,也就意味着,在取出这个套接字时队列中依然可能存在0-N个已就绪的套接字,而这些套接字却只能排队依次等待其前面的IO完成,这样的等待时间无疑是漫长的。
而非阻塞模型解决了上述问题,但是所有请求的线程不断地轮询内核的IO状态,这使得CPU进入了忙等,大量的CPU运行时机用于轮序IO是否就绪。
因此自然而然我们会想到我们可以在非阻塞模型中派一个线程进行轮询,当存在一个或多个就绪IO时,一次性将所有就绪的 fd(File Descriptor) 取出,将就绪的fd派发对应线程让其执行逻辑,这样就减少了大量的IO轮询开销。
特点
能同时处理更多的连接,而不是对于单个连接能够处理的更快。非阻塞IO模型(NIO)中,需要 N (N>=1)次轮询系统调用,然而借助 select 的IO多路复用模型,只需要发起一次询问就够了,大大优化了性能。
select函数返回后,是通过 遍历fdset ,找到就绪的描述符 fd 。(仅知道有I/O事件发生,却不知是哪几个流,所以 遍历所有流)。且select参数的fd_set结构长度有限,一般是1024。
因为 存在连接数限制,所以后来又提出了 poll。与select相比, poll解决了 连接数限制问题。但是呢,select和poll一样,还是需要通过遍历文件描述符来获取已经就绪的 socket 。如果同时连接的大量客户端,在一时刻可能只有极少处于就绪状态,伴随着监视的描述符数量的增长, 效率也会线性下降。
使用epool的多路复用
为了解决 select/poll 存在的问题,多路复用模型 epoll 诞生,它采用事件驱动来实现。
epoll先通过 epoll_ctl 来注册一个 fd (文件描述符),一旦基于某个 fd 就绪时,内核会采用回调机制,使当前fd处于有信号状态 ,当进程调用 epoll_wait 时便得到通知。这样省去遍历文件描述符 的操作提升了效率,而是采用 监听事件回调的机制。这就是epoll的亮点。
select、poll、epoll的区别
轮询函数 | 同步数据结构 | 获取就绪fd方法 | 复杂度 | 最大连接 | fd数据拷贝 |
select | 数组 | 遍历 | O(n) | 1024 | 内核拷贝到用户空间 |
pool | 链表 | 遍历 | O(n) | 无限制 | 内核拷贝到用户空间 |
epool | 红黑树和链表 | 事件回调 | O(1) | 无限制 | 使用内存映射(mmap) |
epoll明显优化了IO的执行效率,但在线程调用 epoll_wait 时,未就绪的线程处于阻塞状态,其实大部分线程都处于阻塞状态。
总结
在本篇中涉及的模型优化了 IO就绪前队列等待的问题,加快了响应速度,并且减少了IO轮询的CPU负担,之后又进一步降低了程序调用IO的成本。
该模型针对获取IO就绪状态的流程几乎达到最优。
但同样存在不足:
1、存在多少IO请求就对应多少条线程,并发数达到一定数量后反而降低效率。
2、这些线程大部分时间依然在等待对应的IO转为就绪。