天天看点

详解IO模型一、IO基础原理二、IO模型

目录

一、IO基础原理

1、操作系统的IO

2、IO的基本概念

二、IO模型

1、同步阻塞IO:BIO

2、同步非阻塞IO:NIO

3、IO多路复用

4、异步IO:AIO

一、IO基础原理

1、操作系统的IO

    应用程序进行IO的读写,依赖于底层操作系统的IO读写,是通过操作系统的两大系统调用read、write来实现的。应用程序调用操作系统的read、write接口,完成数据在应用程序的进程缓冲区和操作系统的内核缓冲区之间的交互。

    操作系统设置数据缓冲区的目的,是为了提升性能。外部设备的直接读写,涉及操作系统的中断。发生系统中断时,需要保存之前的进程数据和状态等信息,而结束中断之后,还需要恢复之前的进程数据和状态等信息。操作系统如果频繁访问外部设备,就会产生很多不必要的开销。

    所以,Linux操作系统设置了一个唯一的内核缓冲区,并为每个上层应用分别分配一个独享的进程缓冲区。有了内存缓冲区,上层应用使用read系统调用时,仅仅把数据从内核缓冲区复制到上层应用的缓冲区(进程缓冲区);上层应用使用write系统调用时,仅仅把数据从进程缓冲区复制到内核缓冲区中。底层操作会对内核缓冲区进行监控,等待缓冲区达到一定数量的时候,再进行IO设备的中断处理,集中执行物理设备的实际IO操作,这种机制提升了系统的性能。至于什么时候中断(读中断、写中断),由操作系统的内核来决定,用户程序则不需要关心。

    服务端应用的IO操作流程如下:

详解IO模型一、IO基础原理二、IO模型

    根据上述流程图可知,数据的输入输出流程如下:

1、数据的输入流程分为两个阶段:

  • 等待数据准备好。这一步指的是等待数据从网络中到达网卡。当所等待的分组到达时,它被复制到内核中的某个缓冲区。这个工作由操作系统自动完成,用户程序无感知。
  • 从内核向进程复制数据。这一步指的是把数据从内核缓冲区复制到应用进程缓冲区。

2、数据的输出流程同样分为两个阶段:

  • 将数据从进程缓冲区复制到内核缓冲区。
  • Linux内核通过网络IO,将内核缓冲区中的数据写入网卡,网卡通过底层的通信协议,会将数据发送给目标客户端。

2、IO的基本概念

1、阻塞与非阻塞

    阻塞IO,指的是需要内核IO操作彻底完成后,才返回到用户空间执行用户的操作。阻塞指的是用户空间程序的执行状态。传统的IO模型都是同步阻塞IO。在Java中,默认创建的socket都是阻塞的。

    非阻塞IO,指的是用户空间的程序不需要等待内核IO操作彻底完成,可以立即返回用户空间执行用户的操作,即处于非阻塞的状态,与此同时内核会立即返回给用户一个状态值。

    简单来说,阻塞是指用户空间(调用线程)一直在等待,而不能干别的事情;非阻塞是指用户空间(调用线程)拿到内核返回的状态值就返回自己的空间,IO操作可以干就干,不可以干,就去干别的事情。

2、同步与异步

    同步IO,是一种用户空间与内核空间的IO发起方式。同步IO是指用户空间的线程是主动发起IO请求的一方,内核空间是被动接受方。异步IO则反过来,是指系统内核是主动发起IO请求的一方,用户空间的线程是被动接受方。

二、IO模型

    常见的IO模型有四种:同步阻塞IO(Blocking IO)、同步非阻塞IO(Non-Blocking IO)、IO多路复用(IO-Multiplexing)、异步IO(Asynchronous IO)。

1、同步阻塞IO:BIO

    在Java应用程序进程中,默认情况下,所有的socket连接的IO操作都是同步阻塞IO(Blocking IO)。

    在阻塞式IO模型中,应用程序从IO系统调用开始,直到系统调用返回,在这段时间内,Java进程是阻塞的。返回成功后,应用进程开始处理用户空间的缓存区数据。

    同步阻塞IO的执行流程:

详解IO模型一、IO基础原理二、IO模型

    从上面的图可以看出,阻塞IO在内核进行IO执行的两个阶段,用户线程都被阻塞了。完成的同步阻塞IO的执行流程:

  1. 用户线程发起read系统调用,请求读取数据;
  2. 操作系统收到read系统调用请求后,系统内核开始准备数据。如果数据还没有到达内核缓冲区,比如还没有接收到完整的socket数据包,内核会先等待数据到达。这是数据输入流程的第一步;
  3. 数据完整到达后,内核会将数据从内核缓冲区复制到进程缓冲区。这是数据输入流程的第二步。
  4. 当数据完整复制到进程缓冲区后,用户线程才会结束阻塞状态。

    阻塞IO的优点:应用的程序开发非常简单;在阻塞等待数据期间,用户线程挂起。在阻塞期间,用户线程基本不会占用CPU资源。

    阻塞IO的缺点:一般情况下,会为每个连接配备一个独立的线程;反过来说,就是一个线程维护一个连接的IO操作。在高并发的应用场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大。

2、同步非阻塞IO:NIO

    IO模型里的NIO和Java的NIO是两码事。Java的NIO实际上是New IO,是为了和Java1.4版本以前的Old IO做区分。但是由于Java在New IO里面实现了IO的非阻塞,所以也被称为Non-Blocking IO。但是实际上,Java的NIO使用的是IO多路复用模型。

    IO模型里的NIO描述的是,在read系统调用的第一阶段,即内核缓冲区准备数据的时候,用户线程如果发起read系统调用,如果内核的数据还没有准备好,操作系统内核会立即返回一个调用失败的信息。需要注意的是,NIO的非阻塞只是在read系统调用的第一阶段才是非阻塞,如果内核的数据已经准备好了,此时会进入第二阶段,即从内核缓冲区拷贝数据到进程缓冲区,此时的用户线程依然是阻塞的。

    由于在内核数据准备好之前,read系统调用会立即返回失败,所以应用程序的线程需要不断地进行IO系统调用,轮询数据是否已经准备好,直到完成IO系统调用为止。

    在Linux系统下,socket连接默认是阻塞模式,但是可以通过设置将socket变成为非阻塞的模式(Non-Blocking)。

    同步非阻塞IO的执行流程:

详解IO模型一、IO基础原理二、IO模型

    同步非阻塞IO的优点:每次发起的IO系统调用,在内核等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。

    同步非阻塞IO的缺点:不断地轮询内核,这将占用大量的CPU时间,效率低下。

    总体来说,在高并发应用场景下,同步非阻塞IO也是不可用的。一般Web服务器不使用这种IO模型。这种IO模型一般很少直接使用,而是在其他IO模型中使用非阻塞IO这一特性。

3、IO多路复用

    IO多路复用模型主要是用来解决NIO模型中轮询等待的问题。这也是Java的NIO使用的IO模型。

    在IO多路复用模型中,引入了一种新的系统调用,查询IO的就绪状态。通过该系统调用,一个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核能够将就绪的状态返回给应用程序。随后,应用程序根据就绪的状态,进行相应的IO系统调用。

    在Linux系统中,对应的系统调用为select/epoll系统调用。select系统调用,几乎在所有的操作系统上都有支持,具有良好的跨平台特性。epoll是在Linux 2.6内核中提出的,是select系统调用的Linux增强版本。

    在IO多路复用模型中通过select/epoll系统调用,单个应用程序的线程,可以不断地轮询成百上千的socket连接,当某个或者某些socket网络连接有IO就绪的状态,就返回对应的可以执行的读写操作。

    IO多路复用读操作的主要流程:

  1. 选择器注册。将需要read操作的目标socket网络连接,提前注册到select/epoll选择器中;
  2. 等待连接就绪。选择器调用select/epoll系统调用后,会阻塞等待直到有socket连接就绪后,再返回就绪的连接。这里的就绪,一般指的是内核缓存区的数据准备好了;
  3. 用户线程获取到就绪的socket连接后,对该连接发起read系统调用,操作系统内核将数据从内核缓冲区拷贝到程序缓冲区。过程中用户线程阻塞;
  4. 复杂完成后用户线程获取到结果,阻塞终止。

    IO多路复用的执行流程:

详解IO模型一、IO基础原理二、IO模型

    IO多路复用模型的IO涉及两种系统调用,一种是select/epoll(就绪查询),一种是IO操作(read/write)。IO多路复用模型建立在操作系统的基础设施之上,即操作系统的内核必须能够提供多路分离的系统调用select/epoll。

    和NIO模型相似,多路复用IO也需要轮询。负责select/epoll状态查询调用的线程,需要不断地进行select/epoll轮询,不断查找出IO操作就绪的socket连接。

    IO多路复用模型与同步非阻塞IO模型是有密切关系的。对于注册在选择器上的每一个可以查询的socket连接,一般都设置成为同步非阻塞模型。这一点是由操作系统内核完成的,对于用户程序而言是无感知的。

    IO多路复用模型的优点:与一个线程维护一个连接的阻塞IO模式相比,使用select/epoll的最大优势在于,一个选择器查询线程可以同时处理成千上万个连接(Connection)。系统不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。

    IO多路复用模型的缺点:本质上,select/epoll系统调用是阻塞式的,属于同步IO。都需要在读写事件就绪后,由系统调用本身负责进行读写,也就是说这个读写过程是阻塞的。

4、异步IO:AIO

    AIO的基本流程是:用户线程通过系统调用,向内核注册某个IO操作。内核在整个IO操作(包括数据准备、数据复制)完成后,通知用户程序,用户执行后续的业务操作。

    在异步IO模型中,在整个内核的数据处理过程中,包括内核将数据从网络物理设备(网卡)读取到内核缓冲区、将内核缓冲区的数据复制到用户缓冲区,用户程序都不需要阻塞。

    详细来说,AIO的执行流程:

  1. 当用户线程发起了read系统调用,立刻就可以开始去做其他的事,用户线程不阻塞。
  2. 内核就开始了IO的第一个阶段:准备数据。等到数据准备好了,内核就会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存)。
  3. 内核会给用户线程发送一个信号(Signal),或者回调用户线程注册的回调接口,告诉用户线程read操作完成了。
  4. 用户线程读取用户缓冲区的数据,完成后续的业务操作。

    在内核等待数据和复制数据的两个阶段,用户线程都不是阻塞的。用户线程需要接收内核的IO操作完成的事件,或者用户线程需要注册一个IO操作完成的回调函数。正因为如此,异步IO有的时候也被称为信号驱动IO。

    异步IO的执行流程:

详解IO模型一、IO基础原理二、IO模型

    异步IO的优点:理论上来说,异步IO是真正的异步输入输出,它的吞吐量高于IO多路复用模型的吞吐量。

    异步IO异步模型的缺点:应用程序仅需要进行事件的注册与接收,其余的工作都留给了操作系统,也就是说,需要底层内核提供支持。就目前而言,Windows系统下通过IOCP实现了真正的异步IO。而在Linux系统下,异步IO模型在2.6版本才引入,目前并不完善,其底层实现仍使用epoll,与IO多路复用相同,因此在性能上没有明显的优势。

继续阅读