流:一个流可以是文件,socket,pipe等等可以进行I/O操作的内核对象。
read:从流中读取数据;
write:往流中写入数据;
阻塞:一直等待任务,有任务到来会被唤醒;
非阻塞忙轮询:一遍又一遍地询问有没有任务;
缓冲区:先把要获取或者写入的数据缓存起来,等到合适的时机再进行io操作。为什么要使用缓冲区呢?
首先看一下图:
例如从磁盘中读取一个文件的内容,用户态从内核态取数据,需要在用户态和内核态之间切换,会比较耗性能,写入数据也是如此,因此缓冲区可以减少用户态和内核态之间切换所消耗的性能。
IO阻塞:
(1)read:如果内核态缓冲区没有数据,那么将阻塞,如果内核态缓冲区从没有数据到有数据,就会唤醒阻塞的read线程;
(2)write:如果内核态缓冲区满了,无法在写入,此时将阻塞,如果内核态缓冲区从满状态到非满状态,就会唤醒阻塞的write线程。
IO模型:
(1)阻塞IO模型:这个模型有一个明显的缺点,就是一个线程只能处理一个IO操作,如果需要处理多个IO操作,就需要创建多个线程或者进程,这显然不可取;
(2)非阻塞忙轮询IO模型:轮询所有的流,如果可读取或写入,则执行,否则处理下一个流。这个模型有两个缺点:1.在所有流都不可读取和写入的时候,会造成CPU空转,浪费系统资源;2.不能执行处理可读取或写入的流,需要遍历所有的流并进行判断是否可读取或写入,再进行处理;
(3)select模型:对于上述的非阻塞忙轮询IO模型的CPU空转的缺点进行补充。
在所有流都不可读取和写入的时候,CPU会空转,那么引入一个select代理,用于检测所有流是否可读取和可写入,如果所有流都不可读取和写入,那么这个线程将阻塞,如果至少有一个流可读取或写入,那么就会将阻塞的线程唤醒。其实select模型下,维护一个fd_set数据结构(使用的是Bitmap位图算法),每一个元素与流关联,每次调用select()方法时,需要将这个数据结构拷贝到内核态,然后内核判断哪些流可读或可写,并写入到这个数据结构然后拷贝到用户态。因此 1.如果数据结构太大,在用户态和内核态之间拷贝会很耗性能;2.在内核态中,每次都要遍历这个数据结构的所有元素,比较耗性能;3.为了减少拷贝对性能的损害,内核对这个集合的大小做了限制(1024,大小不可修改);
(4)poll模型:这个模型跟select模型类似,只是没有对fd_set数据结构集合的大小做限制,因此只解决了select模型的第三个缺点,第一、二个缺点依然存在;
(5)epoll模型:该模型基于事件驱动的方式,使用一个文件描述符来管理多个描述符,描述符的数量没有限制。每当fd就绪,就会调用系统注册的回调函数,将fd加入到readyList中。进行IO操作的时候,只需要遍历这个readyList,而不需要遍历所有的fd。因此解决了CPU空转、遍历所有fd、大集合在用户态和内核态之间拷贝的性能消耗的缺点。
select | poll | epoll | |
---|---|---|---|
操作方式 | 遍历 | 遍历 | 回调 |
底层实现 | 数组 | 链表 | 红黑树 |
IO效率 | 每次调用都进行线性遍历,时间复杂度为O(n) | 每次调用都进行线性遍历,时间复杂度为O(n) | 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1) |
最大连接数 | 1024(x86)或2048(x64) | 无上限 | 无上限 |
fd拷贝 | 每次调用select,都需要把fd集合从用户态拷贝到内核态 | 每次调用poll,都需要把fd集合从用户态拷贝到内核态 | 调用epoll_ctl时拷贝进内核并保存,之后每次epoll_wait不拷贝 |
epoll是Linux目前大规模网络并发程序开发的首选模型。在绝大多数情况下性能远超select和poll。目前流行的高性能web服务器Nginx正式依赖于epoll提供的高效网络套接字轮询服务。但是,在并发连接不高的情况下,多线程+阻塞I/O方式可能性能更好。