IO多路复用
介绍
IO多路复用是一个IO模型,指的是复用同一个线程监控多路网络连接。
现在大多的IO多路复用都是同步模型,Reactor模式。
Reactor和Proactor
两种多路复用IO方案。
在进行IO编程中,通常用到两种模式:Reactor和Proactor。
Reactor模式是基于同步IO的,而Proactor模式是和异步IO相关的。
Java的NIO就是Reactor,当有事件触发时,服务器端得到通知,进行相应的处理。AIO引入的是Proactor模式。
多路复用解决的问题
IO实现主要有BIO和NIO两种机制,AIO用的比较少。
他们都会有多多少少的问题。
BIO
- 每个请求都需要创建独立的线程,与对应的客户端进行数据处理。
- 当并发数大时,需要创建大量线程来处理连接,系统资源占用较大。
- 连接建立后,如果当前线程暂时没有数据可读,则当前线程会一直阻塞在 Read 操作上,造成线程资源浪费。
实现方式
IO多路复用在底层实现上主要有三种实现方式:select、poll、epoll。
这些方法是操作系统提供给应用的接口,毕竟所有的IO最终都是落实在操作系统的IO上。
一个个人理解,java中的NIO包,在使用Selector的情况下就是使用了IO多路复用,windows下底层用的是poll方法。
在linux中一切皆是文件传输,网络的Socket通信也是,所以会为每一个Socket建立一个文件描述符,保存在操作系统内核空间中,select,poll,epoll采用不同的机构来存储这些文件描述符。
这里的文件描述符也可以认为就是Socket。
select
select 实现多路复用的方式是,将已连接的Socket都放到一个文件描述符集合,然后调用select函数将文件描述符集合拷贝到内核里,让内核来检查是否有网络事件产生,检查的方式很粗暴,就是通过遍历文件描述符集合的方式,当检查到有事件产生后,将此Socket标记为可读或可写, 接着再把整个文件描述符集合拷贝回用户态里,然后用户态还需要再通过遍历的方法找到可读或可写的Socket,然后再对其处理。这个过程中需要用户态到内核态,内核态到用户态两次拷贝。
select使用固定长度的BitsMap,表示文件描述符集合,而且所支持的文件描述符的个数是有限制的,在Linux系统中,由内核中的FD_SETSIZE限制, 默认最大值为1024,只能监听0~1023的文件描述符。
对于select这种方式,需要进行2次「遍历」文件描述符集合,一次是在内核态里,一个次是在用户态里 ,而且还会发生2次「拷贝」文件描述符集合,先从用户空间传入内核空间,由内核修改后,再传出到用户空间中。
poll
poll与select相比没有什么变化,相对于select,poll不在使用BitsMap存储所关注的文件描述符,取而代之用动态数组,以链表形式来组织,突破了select的文件描述符个数限制,当然还会受到系统文件描述符限制。
都是使用「线性结构」存储进程关注的Socket集合,因此都需要遍历文件描述符集合来找到可读或可写的 Socket,时间复杂度为 O(n),而且也需要在用户态与内核态之间拷贝文件描述符集合
epoll
epoll在内核中采用红黑树存储所有的文件描述符,将需要监控的Socket通过epoll_ctl()函数加入内核的红黑树里,这样就不需要像select/poll每次操作时都传入整个socket集合,只需要传入一个待检测的socket,减少了内核和用户空间大量的数据拷贝和内存分配。
epoll采用事件驱动机制,内部维护了一个链表来记录就绪事件,当某个socket有事件发生时,通过回调函数内核会将其加入到这个就绪事件列表中,当用户调用epoll_wait()函数时,只会返回有事件发生的文件描述符的个数,不需要像select/poll那样轮询扫描整个socket集合,大大提高了检测的效率。
事件触发模式
epoll支持两种事件触发模式,分别是边缘触发(ET)和水平触发(LT)。epoll默认采用水平触发模式,select/poll只有水平触发模式。
- 边缘触发: 使用边缘触发模式时,当被监控的Socket描述符上有可读事件发生时,服务器端只会从epoll_wait中苏醒一次,即使进程没有调用read函数从内核读取数据,也依然只苏醒一次,因此我们程序要保证一次性将内核缓冲区的数据读取完。边缘触发模式一般和非阻塞IO一起搭配使用,因为边缘模式下一般会循环的从文件描述符(Socket)读取数据,如果是阻塞的,阻塞IO就会阻塞,程序无法继续执行。
- 水平触发: 使用水平触发模式时,当被监控的Socket上有可读事件发生时,服务器端不断地从epoll_wait中苏醒,直到内核缓冲区数据被read函数读完才结束,目的是告诉我们有数据需要读取。
一般而言边缘触发的效率高于水平触发。
水平触发的意思是只要满足事件的条件,比如内核中有数据需要读,就一直不断地把这个事件传递给用户;而边缘触发的意思是只有第一次满足条件的时候才触发,之后就不会再传递同样的事件了。np
区别
一切因文件描述符而起。
— | select | poll | epoll |
---|---|---|---|
数据结构 | bitmap | 动态数组 | 红黑树 |
最大连接数 | 1024 | 无上线 | 无上限 |
fd拷贝 | 每次调用select拷贝 | 每次调用poll拷贝 | fd首次调用epoll_ctl拷贝,每次调用epoll_wait不拷贝 |
工作效率 | 轮询:O(n) | 轮询:O(n) | 回调:O(1) |
总结
网路通信中的IO模型,从最基础的阻塞IO模型,到多进程,多线程模型,在发展到多路复用IO模型。
传统的多线程、多进程模型,多个进程、线程的调度,和资源的消耗,上下文切换都会称为它们的瓶颈。
多路复用IO解决了上面的问题,linux下提供了三种API:select,poll,epoll。
select和poll 并没有本质区别,它们内部都是使用「线性结构」来存储进程关注的Socket集合。它们的缺陷在于,当客户端越多,Socket集合越大时,它的遍历和拷贝会带来很大的开销。