服务器常见I/O模型

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了服务器常见I/O模型相关的知识,希望对你有一定的参考价值。

参考技术A 1.同步阻塞IO(Blocking IO)

首先,解释一下这里的阻塞与非阻塞:阻塞IO,指的是需要内核IO操作彻底完成后,才返回到用户空间执行用户的操作。阻塞指的是用户空间程序的执行状态。传统的IO模型都是同步阻塞IO。在Java中,默认创建的socket都是阻塞的。其次,解释一下同步与异步:同步IO,是一种用户空间与内核空间的IO发起方式。同步IO是指用户空间的线程是主动发起IO请求的一方,内核空间是被动接受方。异步IO则反过来,是指系统内核是主动发起IO请求的一方,用户空间的线程是被动接受方.
 example:
   在Java应用程序进程中,默认情况下,所有的socket连接的IO操作都是同步阻塞IO(Blocking IO)。在阻塞式IO模型中Java应用程序从IO系统调用开始,直到系统调用返回,在这段时间内,Java进程是阻塞的。返回成功后,应用进程开始处理用户空间的缓存区数据。同步阻塞IO的具体流程,如图2-2所示。

举个例子,在Java中发起一个socket的read读操作的系统调用,
流程大致如下:
(1)从Java启动IO读的read系统调用开始,
用户线程就进入阻塞状态。
(2)当系统内核收到read系统调用,就开始准备数据。一开始,数据可能还没有到达内核缓冲区(例如,还没有收到一个完整的socket数据包),这个时候内核就要等待。
(3)内核一直等到完整的数据到达,就会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存),然后内核返回结果(例如返回复制到用户缓冲区中的字节数)。
(4)直到内核返回后,用户线程才会解除阻塞的状态,重新运行起来。总之,阻塞IO的特点是:在内核进行IO执行的两个阶段,用户线程都被阻塞了。
阻塞IO的优点是:应用的程序开发非常简单;在阻塞等待数据期间,用户线程挂起。在阻塞期间,用户线程基本不会占用CPU资源。阻塞IO的缺点是:一般情况下,会为每个连接配备一个独立的线程;反过来说,就是一个线程维护一个连接的IO操作。在并发量小的情况下,这样做没有什么问题。但是,当在高并发的应用场景下,需要大量的线程来维护大量的网络连接,内存、线程切换开销会非常巨大。因此,基本上阻塞IO模型在高并发应用场景下是不可用的

2.同步非阻塞IO(Non-blocking IO)
非阻塞IO,指的是用户空间的程序不需要等待内核IO操作彻底完成,可以立即返回用户空间执行用户的操作,即处于非阻塞的状态,与此同时内核会立即返回给用户一个状态值。简单来说:阻塞是指用户空间(调用线程)一直在等待,而不能干别的事情;非阻塞是指用户空间(调用线程)拿到内核返回的状态值就返回自己的空间,IO操作可以干就干,不可以干,就去干别的事情。非阻塞IO要求socket被设置为NONBLOCK。强调一下,这里所说的NIO(同步非阻塞IO)模型,并非Java的NIO(New IO)库。
example:
socket连接默认是阻塞模式,在Linux系统下,可以通过设置将socket变成为非阻塞的模式(Non-Blocking)。使用非阻塞模式的IO读写,叫作同步非阻塞IO(None BlockingIO),简称为NIO模式。在NIO模型中,应用程序一旦开始IO系统调用,会出现以下两种情况:
(1)在内核缓冲区中没有数据的情况下,系统调用会立即返回,返回一个调用失败的信息。
(2)在内核缓冲区中有数据的情况下,是阻塞的,直到数据从内核缓冲复制到用户进程缓冲。复制完成后,系统调用返回成功,应用进程开始处理用户空间的缓存数据。
同步非阻塞IO的流程,如图2-3所示。

举个例子。发起一个非阻塞socket的read读操作的系统调用,流程如下:
(1)在内核数据没有准备好的阶段,用户线程发起IO请求时,立即返回。所以,为了读取到最终的数据,用户线程需要不断地发起IO系统调用。
(2)内核数据到达后,用户线程发起系统调用,用户线程阻塞。内核开始复制数据,它会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存),然后内核返回结果(例如返回复制到的用户缓冲区的字节数)。
(3)用户线程读到数据后,才会解除阻塞状态,重新运行起来。也就是说,用户进程需要经过多次的尝试,才能保证最终真正读到数据,而后继续执行。同步非阻塞IO的特点:应用程序的线程需要不断地进行IO系统调用,轮询数据是否已经准备好,如果没有准备好,就继续轮询,直到完成IO系统调用为止。同步非阻塞IO的优点:每次发起的IO系统调用,在内核等待数据过程中可以立即返回。用户线程不会阻塞,实时性较好。同步非阻塞IO的缺点:不断地轮询内核,这将占用大量的CPU时间,效率低下。总体来说,在高并发应用场景下,同步非阻塞IO也是不可用的。一般Web服务器不使用这种IO模型。这种IO模型一般很少直接使用,而是在其他IO模型中使用非阻塞IO这一特性。在Java的实际开发中,也不会涉及这种IO模型。这里说明一下,同步非阻塞IO,可以简称为NIO,但是,它不是Java中的NIO,虽然它们的英文缩写一样,希望大家不要混淆。Java的NIO(NewIO),对应的不是四种基础IO模型中的NIO(NoneBlockingIO)模型,而是另外的一种模型,叫作IO多路复用模型(IOMultiplexing)。

3.IO多路复用(IO Multiplexing)

即经典的Reactor反应器设计模式,有时也称为异步阻塞IO,Java中的Selector选择器和Linux中的epoll都是这种模型。
如何避免同步非阻塞IO模型中轮询等待的问题呢?这就是IO多路复用模型。在IO多路复用模型中,引入了一种新的系统调用,查询IO的就绪状态。在Linux系统中,对应的系统调用为select/epoll系统调用。通过该系统调用,一个进程可以监视多个文件描述符,一旦某个描述符就绪(一般是内核缓冲区可读/可写),内核能够将就绪的状态返回给应用程序。随后,应用程序根据就绪的状态,进行相应的IO系统调用。目前支持IO多路复用的系统调用,有select、epoll等等。select系统调用,几乎在所有的操作系统上都有支持,具有良好的跨平台特性。epoll是在Linux2.6内核中提出的,是select系统调用的Linux增强版本。在IO多路复用模型中通过select/epoll系统调用,单个应用程序的线程,可以不断地轮询成百上千的socket连接,当某个或者某些socket网络连接有IO就绪的状态,就返回对应的可以执行的读写操作。举个例子来说明IO多路复用模型的流程。发起一个多路复用IO的read读操作的系统调用,流程如下:
(1)选择器注册。在这种模式中,首先,将需要read操作的目标socket网络连接,提前注册到select/epoll选择器中,Java中对应的选择器类是Selector类。然后,才可以开启整个IO多路复用模型的轮询流程。
(2)就绪状态的轮询。通过选择器的查询方法,查询注册过的所有socket连接的就绪状态。通过查询的系统调用,内核会返回一个就绪的socket列表。当任何一个注册过的socket中的数据准备好了,内核缓冲区有数据(就绪)了,内核就将该socket加入到就绪的列表中。当用户进程调用了select查询方法,那么整个线程会被阻塞掉。
(3)用户线程获得了就绪状态的列表后,根据其中的socket连接,发起read系统调用,用户线程阻塞。内核开始复制数据,将数据从内核缓冲区复制到用户缓冲区。
(4)复制完成后,内核返回结果,用户线程才会解除阻塞的状态,用户线程读取到了数据,继续执行。

IO多路复用模型的特点:IO多路复用模型的IO涉及两种系统调用(SystemCall),另一种是select/epoll(就绪查询),一种是IO操作。IO多路复用模型建立在操作系统的基础设施之上,即操作系统的内核必须能够提供多路分离的系统调用select/epoll。和NIO模型相似,多路复用IO也需要轮询。负责select/epoll状态查询调用的线程,需要不断地进行select/epoll轮询,查找出达到IO操作就绪的socket连接。IO多路复用模型与同步非阻塞IO模型是有密切关系的。对于注册在选择器上的每一个可以查询的socket连接,一般都设置成为同步非阻塞模型。仅是这一点,对于用户程序而言是无感知的。IO多路复用模型的优点:与一个线程维护一个连接的阻塞IO模式相比,使用select/epoll的最大优势在于,一个选择器查询线程可以同时处理成千上万个连接(Connection)。系统不必创建大量的线程,也不必维护这些线程,从而大大减小了系统的开销。Java语言的NIO(NewIO)技术,使用的就是IO多路复用模型。在Linux系统上,使用的是epoll系统调用。IO多路复用模型的缺点:本质上,select/epoll系统调用是阻塞式的,属于同步IO。都需要在读写事件就绪后,由系统调用本身负责进行读写,也就是说这个读写过程是阻塞的。如何彻底地解除线程的阻塞,就必须使用异步IO模型。

4.异步IO(Asynchronous IO)

异步IO,指的是用户空间与内核空间的调用方式反过来。用户空间的线程变成被动接受者,而内核空间成了主动调用者。这有点类似于Java中比较典型的回调模式,用户空间的线程向内核空间注册了各种IO事件的回调函数,由内核去主动调用.
异步IO模型(AsynchronousIO,简称为AIO)。AIO的基本流程是:用户线程通过系统调用,向内核注册某个IO操作。内核在整个IO操作(包括数据准备、数据复制)完成后,通知用户程序,用户执行后续的业务操作。在异步IO模型中,在整个内核的数据处理过程中,包括内核将数据从网络物理设备(网卡)读取到内核缓冲区、将内核缓冲区的数据复制到用户缓冲区,用户程序都不需要阻塞.

举个例子。发起一个异步IO的read读操作的系统调用,流程如下:
(1)当用户线程发起了read系统调用,立刻就可以开始去做其他的事,用户线程不阻塞。
(2)内核就开始了IO的第一个阶段:准备数据。等到数据准备好了,内核就会将数据从内核缓冲区复制到用户缓冲区(用户空间的内存)。
(3)内核会给用户线程发送一个信号(Signal),或者回调用户线程注册的回调接口,告诉用户线程read操作完成了。
(4)用户线程读取用户缓冲区的数据,完成后续的业务操作。异步IO模型的特点:在内核等待数据和复制数据的两个阶段,用户线程都不是阻塞的。用户线程需要接收内核的IO操作完成的事件,或者用户线程需要注册一个IO操作完成的回调函数。正因为如此,异步IO有的时候也被称为信号驱动IO。异步IO异步模型的缺点:应用程序仅需要进行事件的注册与接收,其余的工作都留给了操作系统,也就是说,需要底层内核提供支持。理论上来说,异步IO是真正的异步输入输出,它的吞吐量高于IO多路复用模型的吞吐量。就目前而言,Windows系统下通过IOCP实现了真正的异步IO。而在Linux系统下,异步IO模型在2.6版本才引入,目前并不完善,其底层实现仍使用epoll,与IO多路复用相同,因此在性能上没有明显的优势。大多数的高并发服务器端的程序,一般都是基于Linux系统的。因而,目前这类高并发网络应用程序的开发,大多采用IO多路复用模型。大名鼎鼎的Netty框架,使用的就是IO多路复用模型,而不是异步IO模型。

本资料收集于Netty、Redis、Zookeeper高并发实战,希望大家购买正版图书地址:https://www.jd.com/zxnews/6997df6e93bc8192.html

网络I/O模型--5种常见的网络I/O模型

 

阻塞与非阻塞

  阻塞就是卡在那儿什么也不做,双方之间也没有信息沟通。

  非阻塞就是即使对方不能马上完成请求,双方之间也有信息的沟通。

同步与异步

  同步就是一件事件只由一个过程处理完成,不论阻塞与非阻塞,最后完成这个事情的都是同一个过程

  异步就是一件事由两个过程完成,前面一个过程通知,后面一个过程接受返回的结果。

异步和事件驱动(multi IO)

  异步是指数据准备好并且已经拷贝到用户空间,在通知用户来取数据

       事件驱动理解为准备好数据了但是没有拷贝到用户空间,这个时候去通知用户,用户再去取数据,经过拷贝过程取得数据。 

 

5种常见的网络I/O模型

  • blocking I/O  -- 阻塞类型的I/O
  • nonblocking I/O -- 非阻塞类型的I/O
  •  I/O Multiplexing -- 多路复用型I/O 
  •  Signal-Driven I/O -- 信号驱动型I/O
  •  Asynchronous I/O -- 异步I/O 

   技术分享图片

 

 

1. blocking I/O  -- 阻塞类型的I/O

  流程图如下:

    技术分享图片

  linux下socket默认是阻塞的,阻塞模式在准备数据和拷贝数据两个过程都是阻塞的,在这期间客户端一直卡着,双方也没有数据交流。

2.nonblocking I/O -- 非阻塞类型的I/O

  流程图:

    技术分享图片

 

     linux下使用fcntl方法将socket设置为非阻塞模式 

    flags = fcntl(sockfd, F_GETFL, 0);                        //获取文件的flags值。

              fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);   //设置成非阻塞模式;

  非阻塞模式下,如果数据还没有准备好,服务端会返回error,客户端根据这个error判断后,再次发起recv,直到数据准备好,这个过程虽然客户端也在这里卡着了,但是这期间一直和服务有着信息交换。

 

3.I/O Multiplexing -- 多路复用型I/O

  流程图:

       技术分享图片

 

  多路复用型IO,有的也称为事件驱动型IO,常用select,poll,epoll来处理多个IO连接状态。当某个IO连接的数据准备好了,select返回,通知用户进行read操作,这种IO的好处是一次可以监听多个连接,坏处是要两次调用,两次返回,单个连接和非阻塞IO没有多大的性能提升。

    在多路复用模型中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的process其实是一直被block的。只不过process是被select这个函数block,而不是被socket IO给block。因此select()与非阻塞IO类似。

 

4.Signal-Driven I/O -- 信号驱动型I/O

  流程图:

  技术分享图片

 

  首先我们允许套接口进行信号驱动I/O,并安装一个信号处理函数,进程继续运行并不阻塞。当数据准备好时,进程会收到一个SIGIO信号,可以在信号处理函数中调用I/O操作函数处理数据。

 

5.Asynchronous I/O -- 异步I/O

   流程图:

    技术分享图片

  当一个异步过程调用发出后,调用者不能立刻得到结果。实际处理这个调用的部件在完成后,通过状态、通知和回调来通知调用者的输入输出操作

  

 

 

参考资源:

  http://www.cnblogs.com/findumars/p/6361627.html

  https://www.cnblogs.com/kunhu/p/3624000.html

 

  

以上是关于服务器常见I/O模型的主要内容,如果未能解决你的问题,请参考以下文章

socket server服务器开发常见的并发模型

socket编程:I/O模型

(转)高性能I/O模型

Java NIO

Java NIO浅析

C++服务器设计:基于I/O复用的Reactor模式