参考地址:http://www.cnblogs.com/findumars/p/6361627.html
https://time.geekbang.org/column/article/9293
目录
多路复用IO(IO Multiplexing)- 事件驱动IO
什么是IO
Java中I/O操作主要是指使用Java进行输入,输出操作。 Java所有的I/O机制都是基于数据流进行输入输出,这些数据流表示了字符或者字节数据的流动序列
IO要经历的阶段
以接收(read)IO流为例,会涉及到两个对象:调用IO的进程(线程)、系统内核(kernel)
一个read操作:
1)数据准备阶段(Socket --> 内核)
2)将数据从内核拷贝到进程中(内核 --> 进程)
本文讨论的背景是Linux环境下的network IO。本文最重要的参考文献是Richard Stevens的“UNIX® Network Programming Volume 1, Third Edition: The Sockets Networking ”,6.2节“I/O Models ”,Stevens在这节中详细说明了各种IO的特点和区别,如果英文够好的话,推荐直接阅读。Stevens的文风是有名的深入浅出,所以不用担心看不懂。本文中的流程图也是截取自参考文献。
阻塞IO(Blocking IO)
*特点:IO执行的两个阶段(等待+拷贝)都被阻塞了
角色:
- 客户端(进程(应用application)+内核kernel+Socket)
- 服务端(Socket)
服务端内的IO过程
datagram ready 就是从Socket获取数据的结果
当用户调用了recvfrom(),如果此时内核没有准备好数据(no datagram ready),整个用户进程就①阻塞
当内核准备好数据(datagram ready),内核就将数据复制到用户进程的内存,这个过程用户进程②阻塞直到kernel返回结果(copy complete --> return OK)
简而言之:等待数据和拷贝数据都阻塞了
客户端与服务端之间的IO过程UDP(recvfrom() + sendto())
刚才分析过,recvfrom()是阻塞的,而Socket的其他方法sendto()也是阻塞的,这种时候就是“一问一答”,也就是服务器专心只解决你一个客户端的问题,此时服务器的线程无法执行任何运算或响应任何的网络请求,影响其他进程/线程。这时就可以用多线程/多进程来让服务器分身出来解决其他线程/进程的请求。
解决服务器IO阻塞(服务器端-多进程/多线程)
*开启多进程/多线程不代表 异步,因为还是自己(自己是进程/线程)来处理
要交给不同的对象(比如内核)来处理,然后自己向下执行才算是异步
多进程/多线程的目的就是让每一个连接都拥有独立的进程/线程,这样任何一个连接的阻塞都不会影响其他进程/线程
![]()
UDP的多线程服务器模型
![]()
TCP的多线程服务器模型 *一个Socket可以accpet()多次原因: accpet()返回的是一个新socket
而通过多线程和多进程解决的区别,其实就是进程和线程的区别:最主要的是资源问题
- 多进程就相当于,每有一个项目,就开一个公司来做这个项目(在父进程的基础上完全拷贝一个子进程fork(),在 Linux 内核中,会复制文件描述符的列表,也会复制内存空间,还会复制一条记录当前执行到了哪一行程序的进程)
- 多线程就相当于,在同一个公司,开多一个项目组来做这个项目(很多资源,例如文件描述符列表、进程空间,还是共享的,只不过多了一个引用而已)
多线程和多进程解决服务器IO阻塞的不足之处
上述多线程的服务器模型似乎完美的解决了为多个客户机提供问答服务的要求,但其实并不尽然。如果要同时响应成百上千路的连接请求,则无论多线程还是多进程都会严重占据系统资源,降低系统对外界响应效率(太多线程/进程),而线程与进程本身也更容易进入假死状态。
很多程序员可能会考虑使用“线程池”或“连接池”。“线程池”旨在减少创建和销毁线程的频率,其维持一定合理数量的线程,并让空闲的线程重新承担新的执行任务。“连接池”维持连接的缓存池,尽量重用已有的连接、减少创建和关闭连接的频率。这两种技术都可以很好的降低系统开销,都被广泛应用很多大型系统,如websphere、tomcat和各种数据库等。但是,“线程池”和“连接池”技术也只是在一定程度上缓解了频繁调用IO接口带来的资源占用。而且,所谓“池”始终有其上限,当请求大大超过上限时,“池”构成的系统对外界的响应并不比没有池的时候效果好多少。所以使用“池”必须考虑其面临的响应规模,并根据响应规模调整“池”的大小。
走到这里,对于BIO(阻塞IO)的优化已经差不多了,而还是有局限性,也就是面对大规模的服务请求,会遇到瓶颈(线程池/连接池请求超过上限),所以可以考虑用NIO(非阻塞IO)
非阻塞IO(Non-Blocking IO)-轮询
*特点:非阻塞的接口相比于阻塞型接口,前者在被调用之后立即返回
*缺点:循环调用recvfrom()大幅度占用CPU资源
这里的非阻塞指的是,recvfrom()执行时发现内核还没准备好数据就直接返回
而在内核准备好数据时,复制内核数据到线程内存还是会阻塞线程的。
也就是等待非阻塞(等待Socket --> 内核),而复制阻塞(内核复制 --> 线程内存)了,而对于异步就是两者都非阻塞
Linux下,可以通过设置socket来使其变为非阻塞IO
服务端内的IO过程
可以看到recvfrom(),如果内核还没有数据会立刻返回结果(EWOULDBLOCK)
也就是说,对于用户发起一个recvfrom()操作可以立刻得到结果。如果得到是error,就知道内核还没准备好数据;反之..
所以,在NIO中,用户进程需要轮询内核是否准备好数据
客户端与服务端之间的IO
使用如下的函数可以将某句柄fd设为非阻塞状态(也就是将Socket设置成NIO?)
fcntl( fd, F_SETFL, O_NONBLOCK );可以看到recv(fd1)没有阻塞,因为有返回错误了就继续往下执行
多路复用IO(IO Multiplexing)- 事件驱动IO
*优势:能同时处理更多的连接,而不是对于单个连接能够处理的更快
解决NIO中循环调用recvfrom()占用高CPU的问题 --> 调用一个select()
服务器内的IO过程(select-1024个socket)
线程/进程执行select(阻塞)通过不断的轮询所有的socket,如果某个socket有数据到达了(return readable),就通知用户进程
与BIO的区别:应用(application)有了两个系统调用select + recvfrom,所以对于处理的连接数不是很多时,多路复用IO不一定就比BIO+多线程好
poll
轮询的是链表(准备就绪的socket)
epoll
轮询的是链表(准备就绪的socket),还有红黑树(存放未准备就绪的socket),共享内存(加快线程间传输速度)
将红黑树放到链表的过程中会执行回调函数callback,通知线程,这样就不用轮询了
信号驱动IO
*特点:等待(不阻塞)+ 拷贝(阻塞)服务端内IO
通过传SIGIO给signal handler来提醒线程数据准备好了
类似于NIO,但是不用循环执行recvfrom(),但是多了一个系统调用establish SIGIO,适用于长时间Socket没有准备好数据的情况
异步IO(Asynchronous IO)- 真正的非阻塞
*特点:IO执行的两个阶段(等待+拷贝)都没有阻塞
*特点:事情交给内核来处理(异步)
服务端的IO
从用户(application)的角度:发起read操作后就可以做其他事了(所谓异步:就好像所有事情交给内核来处理了)
从内核(kernel)的角度:收到一个asynchronous read(aio_read)后,首先会立刻返回(read),然后等数据准备完成,将数据拷贝到用户内存,然后给用户进程发送一个信号(deliver signal)表示read操作完成
总结
这个图是对于要执行IO操作的进程所执行的方法
☆☆☆☆BIO/NIO/多路复用/信号驱动IO 都是同步IO(还得自己负责等待和复制)☆☆☆☆
异步IO就像进程将整个IO操作交给内核处理
对于BIO和NIO,BIO的两个阶段(等待和复制)都会阻塞,而NIO只有在复制的时候才会阻塞