IO复用
select、poll、epoll都是IO复用的基本实现,select出现的最后,之后是poll,再是epoll。
Select
我们使用select就可以实现同时处理多个网络连接的IO请求,基本原理就是程序调用select,然后整个程序就进入阻塞状态,这个时候,kernel内核就会轮询检查所有select负责的文件描述符fd,当找到其中哪个数据准备好了文件描述符,会返回给select,select通知系统调用,将数据从内核复制到进程的缓存区。
原理图如下:
时间复杂度未O(n)
缺点:
进程可以打开的fd有限制,(32位机1024个,64位2048个)(原因是存储fd是一个固定大小的数组)
对socket进行扫描是线性扫描,即采用轮询的方法,效率较低。
用户空间和内核空间之间复制数据非常的消耗资源
poll
poll的基本原理和select非常的类似,但是采用的是链表来存储fd,且poll相比select不会修改描述符。poll相对于select提供了更多的事件类型,并且对描述符的重复利用比select高。
select和poll的返回结果没有声明哪些描述符已经准备好了,如果返回值大于0.,应用进程就需要使用轮询的方式找到IO完成的描述符。这也是影响效率的一大因素
epoll
epoll提供了三个函数:
// 建立一個 epoll 对象,并传回它的id
int epoll_create(int size);
//事件注册函数,将需要监听的事件和需要监听的fd交给epoll对象
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
//等待注册的事件被触发或者timeout发生
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epoll主要解决了这些问题:
-
对fd的数量没有限制,所以最大数量与能打开的fd数量有关。
-
epoll不再需要每次调用都从用户空间将fd_set复制到内核
-
select和poll都是主动去轮询,需要遍历每个fd。而epoll采用的是被动触发的方式,给fd注册了相应的事件的时候,为每个fd指定了一个回调函数,当数据准备好后,就会把就绪的fd加入到就绪队列中,epoll_wait的工作方式实际上就是再这个就绪队列中查看有没有就绪的fd,如果有,就唤醒就绪队列上的等待者,然后调用回调函数。
-
就是select和poll只能通知有fd已经就绪了,但不能知道究竟是哪个fd就绪,所以select和poll就要去主动轮询一遍找到就绪的fd。而epoll则是不但可以知道有fd可以就绪,而且还具体可以知道就绪fd的编号,所以直接找到就可以,不用轮询。这也是主动式和被动式的区别。
epoll有两种触发方式:LT(水平触发),ET(边缘触发) -
LT模式:当epoll_wait()检查到描述符事件到达时,将此事件通知进行,进程可以不立即处理该事件,下次调用epoll_wait()会再次通知进程。是默认的一种模式,并且同时支持Blocking和No-Blocking。
-
ET模型:和LT模式不同的是,通知之后进程必须立即处理事件,下次再调用epoll_wait()时不会再得到事件到达的通知。ET模式很大程度上减少了epoll事件被重复触发的次数,因此效率比LT模式要高。只支持No-Blocking,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
应用场景
select应用场景
select的timeout参数精度为1nm,比poll和epoll的1ms精度更高,因此select适合实时性要求比较高的场景。select的可移植性非常的好。
poll应用场景
poll没有最大描述符数量的限制,如果平台支持并且对实时性要求不高,应该使用poll而不是select
epoll应用场景
只需要运行在linux平台下,有大量的描述符需要同时轮询,并且这些连接最好时长连接。在监听少量的描述符的适合,体现不出epoll的优势。
在需要监控的描述符状态变化多,而且都是非常短暂的,也没有必要使用epoll。因为epoll中的所有描述符都存储在内核中,造成了每次需要对描述符的状态改变都需要通过epoll_ctl()进行系统调用,频繁的系统调用降低了效率。并且因为epoll的描述符存储在内核中,不容易调试。