被虐前奏
面试官:说一下高并发IO底层原理?
面试者:呃。。。嗯。。。这个那个。。。我们都是用XX框架
结果:卒
理解一下高并发原理,展现真正的实力
一、IO读写基础原理
IO读写分为read和write两块,在不同的操作系统中,IO读写的底层调用可能有些区别,但基本功能是一样的。read调用,并不是直接从物理磁盘读取,同样,write调用也不是直接把数据写进物理磁盘,在他们中间还有一层,就是缓冲区,而缓冲区又包括:内核(kernel)缓冲区和用户进程缓冲区。具体可以看下图
在Java中,完整的Socket请求和响应如下:
- 客户端调用:系统通过网卡读取客户端的请求,将数据读取到内核缓冲区;
- 程序请求数据:Java通过read命令,将数据从内核缓冲区送入Java的用户缓冲区;
- Java服务端数据处理:在Java用户空间中处理客户请求数据;
- Java服务端返回数据:数据处理完,通过write命令,将构建好的响应数据从用户缓冲区写入内核缓冲区;
- 发送数据到客户端:内核通过网络IO,将内核缓冲区中的数据通过网卡,适时将数据发送给目标客户端。
二、四种常见的IO模型
1、同步阻塞IO(Blocking IO)
我们先了解一下概念:
同步与异步:同步指的是用户空间是主动发起IO请求的一方,内核空间是被动接受的一方;异步则是将以上过程反过来;
阻塞与非阻塞:阻塞指的用户空间发起请求后,需要等待内核空间彻底完成后,才将数据返回到用户空间;非阻塞指的是用户空间不需要等待内核空间操作完成,可以立即返回用户用户空间的过程 。
传统的IO模型都是同步阻塞IO,在Java中,默认创建的socket都是同步阻塞的。Java应用从发起IO调用开始,直到系统调用返回,Java进程都是阻塞的,直到有数据返回成功后,应用程序才能开始处理用户缓冲区的数据,具体流程如下:
由上图可以看出,每一步必须等待上一步的完成才能进行。
同步阻塞IO优点:开发简单,容易入门;在阻塞等待期间,用户线程挂起,在挂起期间不会占用CPU资源。
同步阻塞IO缺点:一个线程维护一个IO,不适合大并发,在并发量大的时候需要创建大量的线程来维护网络连接,内存、线程开销灰常大。
2、同步非阻塞IO(Non-blocking IO)
参考以上概念,当处于非阻塞IO时,用户空间与内核空间可以更快的交互,只要拿到内核返回的状态值,就可以继续干自己的事情,而不用做无谓的等待。
在Java中,将socket的属性设置为NONBLOCK即为非阻塞模式。在这个模式中,会有以下两种情况:
在内核缓冲区没有数据时,系统调用会马上返回失败状态给调用方;
在内核缓冲区有数据时,此时是会阻塞的,直到数据从内核缓冲区复制到用户缓冲区,复制完成后,系统调用返回成功,继而用户的应用进程开始处理用户空间的数据,具体流程如下:
由上图可以看出,在内核还没有准备好数据的时候,用户线程发起IO请求,会立即返回;为了最终读取到数据,用户线程需要不断发起IO调用;历经多次探险后,终于有数据到达内核时,此时,用户线程会进入阻塞状态,当数据成功复制到用户缓冲区,用户线程成功读取到数据后,才会解除阻塞状态,重新运行起来。
同步非阻塞IO优点:每次发起IO调用,在内核等待数据的过程中可以立即返回,用户线程不会阻塞,实时性较好。
同步非阻塞IO缺点:多个线程不断轮询内核是否有数据,占用大量CPU时间,效率不高。一般Web服务器不会采用此模式。
3、IO多路复用(IO Multiplexing)
如何解决同步非阻塞IO轮询造成效率低下的问题,这时IO多路复用出现了。
经典的Reactor反应器设计模式,也可称为异步阻塞IO,是众多高性能高并发服务器的基础,比如Nginx、Netty、Redis等。Java中NIO(New IO)里的Selector选择器也是基于此模式。在Linux系统中,对应的系统调用为select/epoll系统调用,当内核缓冲区可读或者可写时,内核将就绪的状态主动通知相应的用户程序,应用程序收到就绪状态,进行相应的IO调用,具体流程如下:
由上图可以看出,该模式下,read操作流程如下:
- 注册选择器,将read操作的目标socket提前注册到select/epol选择器中(类比Java中NIO的selector);
- 轮询,通过选择器的查询方法,查询注册过的所有socket连接的就绪状态,内核个就绪的socket列表(当内核监控到注册过的socket有数据准备好了,就会将该socket加入到就绪列表)。注意,当用户调用select查询方法时,此时整个线程会处于阻塞状态;
- 用户线程获取到就绪列表后,根据其中的socket连接,发起read请求,用户线程阻塞,内核开始复制数据,将数据从内核缓冲区复制到用户缓冲区;
- 复制完成后,内核返回结果,用户线程才会解除阻塞,待用户线程读取成功数据后,线程继续。
IO多路复用优点:系统不必创建维护大量线程,只使用一个线程,一个选择器即可同时处理成千上万个连接,大大减少了系统开销。
IO多路复用缺点:本质上,select/epoll系统调用是阻塞式的,属于同步IO,需要在读写事件就绪后,由系统调用进行阻塞的读写。
4、异步IO(Asynchronous IO)
异步IO,指的是用户空间与内核空间的调用方式返过来。内核空间是主动调用者,用户空间变成被动接收者。用户空间的线程向内核空间注册各种IO事件的回调函数,由内核主动触发调用。
AIO的基本流程是:用户线程通过系统调用,向内核注册某个IO操作。在整个内核的数据处理中, 包括数据从网卡到内核缓冲区,内核缓冲区到用户缓冲区,用户程序都不需要阻塞。具体流程如下:
由上图可以看出,该模式下,在内核等待数据和复制数据的两个阶段,用户线程都不会阻塞。read操作流程如下:
- 用户发起read请求,内核立刻返回成功状态,用户不会阻塞,可以马上去做其它事情;
- 内核开始准备数据,待数据准备好了,内核会主动将数据从内核缓冲区复制到用户缓冲区;
- 此时,内核会给用户线程发送一个信号,或者回调用户线程注册的回调接口,通知用户read操作完成了;
- 用户线程收到通知后,读取用户缓冲区的数据,完成后续业务。
异步IO优点:真正实现了异步非阻塞,吞吐量在这几种模式中是最高的。
异步IO缺点:应用程序只需要进行事件的注册与接收,其余工作都交给了操作系统内核,所以需要内核提供支持。在Linux系统中,异步IO在其2.6才引入,目前也还不是灰常完善,其底层实现仍使用epoll,与IO多路复用相同,因此在性能上没有明显占优。