文章目录
一、select总结
select本质上是通过设置或者检查存放fd标志位的数据结构来进行下一步处理。它仅仅知道了,有I/O事件发生了,却并不知道是哪那几个流(可能有一个,多个,甚至全部),我们只能无差别轮询所有流,找出能读出数据,或者写入数据的流,对他们进行操作。所以select具有O(n)的无差别轮询复杂度,同时处理的流越多,无差别轮询时间就越长。
【优点】:
-
select()的可移植性更好,在某些Unix系统上不支持poll()和epoll();
-
select() 对于超时值提供了更好的精度:微秒,而poll是毫秒。
【缺点】:
-
单个进程可监视的fd数量被限制(FD_SIZE为1024);
-
需要维护一个用来存放大量fd的数据结构,这样会使得用户空间和内核空间在传递该结构时复制开销大;
-
对fd进行扫描时是线性扫描。fd剧增后,IO效率较低,因为每次调用都对fd进行线性扫描遍历,所以随着fd的增加会造成遍历速度慢的性能问题;
-
select() 函数的超时参数在返回时也是未定义的,考虑到可移植性,每次在超时之后在下一次进入到select之前都需要重新设置超时参数。
二、poll总结
poll与select不同,通过一个pollfd数组向内核传递需要关注的事件,故没有描述符个数的限制,pollfd中的events字段和revents分别用于标示关注的事件和发生的事件,故pollfd数组只需要被初始化一次。
poll的实现机制与select类似,其对应内核中的sys_poll,只不过poll向内核传递pollfd数组,然后对pollfd中的每个描述符进行poll,相比处理fdset来说,poll效率更高。poll返回后,需要对pollfd中的每个元素检查其revents值,来得指事件是否发生。
【优点】:
-
poll() 不要求开发者计算最大文件描述符加一的大小;
-
poll() 在应付大数目的文件描述符的时候相比于select速度更快;
-
它没有最大连接数的限制,原因是它是基于链表来存储的。
【缺点】:
-
大量的fd的数组被整体复制于用户态和内核地址空间之间,而不管这样的复制是不是有意义;
-
与select一样,poll返回后,需要轮询pollfd来获取就绪的描述符。
三、epoll总结
执行epoll_ create时,创建了红黑树和就绪链表,执行epoll_ ctl时,如果增加socket句柄,则检查在红黑树中是否存在,存在立即返回,不存在则添加到树干上,然后向内核注册回调函数,用于当中断事件来临时向准备就绪链表中插入数据。执行epoll_wait时立刻返回准备就绪链表里的数据即可。
epoll可以理解为event poll,不同于忙轮询和无差别轮询,epoll会把哪个流发生了怎样的I/O事件通知我们。所以我们说epoll实际上是事件驱动(每个事件关联上fd)的,此时我们对这些流的操作都是有意义的。(复杂度降低到了O(1))。
【优点】:
-
没有最大并发连接的限制,能打开的FD的上限远大于1024(1G的内存上能监听约10万个端口);
-
效率提升,不是轮询的方式,不会随着FD数目的增加效率下降。只有活跃可用的FD才会调用callback函数;即Epoll最大的优点就在于它只管你“活跃”的连接,而跟连接总数无关,因此在实际的网络环境中,Epoll的效率就会远远高于select和poll。
【缺点】:
- 不能跨平台,在连接数较少的时候效率也不一定会比select和epoll高。
四、三种方式总结
-
select,poll,epoll都是IO多路复用的机制。I/O多路复用就通过一种机制,可以监视多个描述符,一旦某个描述符就绪(一般是读就绪或者写就绪),能够通知程序进行相应的读写操作。但select,poll,epoll本质上都是同步I/O,因为他们都需要在读写事件就绪后自己负责进行读写,也就是说这个读写过程是阻塞的,而异步I/O则无需自己负责进行读写,异步I/O的实现会负责把数据从内核拷贝到用户空间。
-
select,poll实现需要自己不断轮询所有fd集合,直到设备就绪,期间可能要睡眠和唤醒多次交替。 而epoll其实也需要调用 epoll_ wait不断轮询就绪链表,期间也可能多次睡眠和唤醒交替,但是它是设备就绪时,调用回调函数,把就绪fd放入就绪链表中,并唤醒在 epoll_wait中进入睡眠的进程。虽然都要睡眠和交替,但是select和poll在“醒着”的时候要遍历整个fd集合,而epoll在“醒着”的 时候只要判断一下就绪链表是否为空就行了,这节省了大量的CPU时间,这就是回调机制带来的性能提升。
-
select,poll每次调用都要把fd集合从用户态往内核态拷贝一次,并且要把current往设备等待队列中挂一次,而epoll只要一次拷贝,而且把current往等待队列上挂也只挂一次(在epoll_wait的开始,注意这里的等待队列并不是设备等待队列,只是一个epoll内 部定义的等待队列),这也能节省不少的开销。
【适用情况】:
表面上看epoll的性能最好,但是在连接数少并且连接都十分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。
select低效是因为每次它都需要轮询。但低效也是相对的,视情况而定,也可通过良好的设计改善。
五、一些面试问题
1、epoll读到一半又有新事件来了怎么办?
避免在主进程epoll再次监听到同一个可读事件,可以把对应的描述符设置为EPOLL_ONESHOT,效果是监听到一次事件后就将对应的描述符从监听集合中移除,也就不会再被追踪到。读完之后可以再把对应的描述符重新手动加上。
2、阻塞套接字和非阻塞套接字的区别?
-
阻塞的套接字,会让read阻塞,直到读到所需要的所有字节;
-
非阻塞的套接字,会让read读完fd中的数据后就返回,但如果原本你要求读10个数据,这时只读了8个数据,如果你不再次使用select来判断它是否可读,而是直接read,很可能返回EAGAIN。
3、ET和LT模式下的阻塞与非阻塞?
在LT(水平触发)模式下,也就是默认的模式,epoll_wait返回可读事件,表明socket一定收到了数据,我们可以调用read函数来读取数据。如果指定读取的数据大于缓冲区数据,无论socket是阻塞还是非阻塞的,read不会阻塞,read返回读取的真实数据。在read之后再次调用read,如果socket是阻塞的,read将阻塞,再次收到数据read才返回。此时如果指定读取的数据大于缓冲区,epoll_wait则不再触发,否则epoll_wait将再次触发,因为还有未读完的数据在缓冲区。
在ET(边缘触发)模式下,只有新的数据来到时才会触发,因此在这种情况下,有数据时必须循环读取数据直到read返回-1,并且错误码为EAGAIN,才算读取了全部的缓冲区数据。
-
对于监听的 sockfd,最好使用水平触发模式,边缘触发模式会导致高并发情况下,有的客户端会连接不上。如果非要使用边缘触发,可以用 while 来循环 accept()。
-
对于读写的 connfd,水平触发模式下,阻塞和非阻塞效果都一样,因为在阻塞模式下,如果数据读取不完全则返回继续触发,反之读取完则返回继续等待。建议设置非阻塞。
-
对于读写的 connfd,边缘触发模式下,必须使用非阻塞 IO,并要求一次性地完整读写全部数据(如果不一次性读取一个事件上的数据,会干扰下一个事件)。
参考:https://www.cnblogs.com/aspirant/p/9166944.html
https://blog.csdn.net/lixungogogo/article/details/52226501