IO学习(2)-各种IO模型

  1. Linux操作系统一般分为内核空间用户空间
  2. IO一般有2个阶段:(1)数据准备阶段,磁盘文件读入到内核空间。(2)数据拷贝阶段,内核空间拷贝到用户空间。
  3. 阻塞&&非阻塞、同步&&非同步,其实是两个不同的概念。
    • 用户程序在IO数据准备阶段调用read()方法后,若内核未直接返回结果,是阻塞
    • 用户程序在IO数据准备阶段调用read()方法后,若内核直接返回结果,是非阻塞
    • IO数据拷贝阶段若是由用户程序负责,那么就是同步
    • IO数据拷贝阶段若是由操作系统负责,那么就是异步
  4. 而IO模型一般分为:
  • 阻塞IO:即用户程序调用read()方法后,在IO过程的两个阶段中,用户程序一直阻塞。
  • 非阻塞IOIO第一阶段,用户程序发出read请求后,应用程序轮询查询内核数据是否准备好,该线程虽是阻塞的,持续占用CPU时间,但每次轮询都直接返回给用户程序结果,即非阻塞IO应用程序再次调用read方法拷贝数据,此阶段也是block(阻塞)。由应用程序负责处理。故同步
  • 多路复用IO :在内核中轮询数据是否准备好,(select或者poll也是阻塞方法),当数据准备好之后,会通知应用程序进行数据拷贝。这个是JAVA NIO的实现原理,本质上也是同步非阻塞。
  • 异步IO:操作系统采用了epoll进行处理请求,故不必轮询IO连接。内核空间到用户空间的拷贝,是操作系统独立完成的,故是异步非阻塞。

1. 用户空间&内核空间

现代的操作系统大都通过内核空间用户空间的设计来保护操作系统自身的安全性和稳定性。
所有的系统资源管理都是在内核空间中完成的。比如读写磁盘文件从网络接口读写数据等等。我们的应用程序是无法直接进行这样的操作的。但是我们可以通过内核提供的接口来完成这样的任务。

1.1 内核空间:受保护的内存空间,只有操作系统可以访问;
1.2 用户空间:供应用程序使用的空间,在这里可指JVM虚拟机;

2. 文件/网络读(Read)的阶段

从内核空间和用户空间的角度看一看整个 Linux 系统的结构。它大体可以分为三个部分,从下往上依次为:硬件 -> 内核空间 -> 用户空间。
在硬件之上,内核空间中的代码控制了硬件资源的使用权,用户空间中的代码只有通过内核暴露的系统调用接口(System Call Interface)才能使用到系统中的硬件资源。

2.1 JVM操作io,调用内核暴露的系统调用接口;
2.2 IO执行的第一阶段:数据从文件读取到内核空间;
2.3 IO执行的第二阶段:数据从内核空间拷贝到用户 空间JVM中;
2.4 io数据返回;

3. 阻塞和非阻塞

阻塞非阻塞针对的是操作io后得到的返回结果而言的。
阻塞:调用io后,一直等待,直到文件复制到用户空间;
非阻塞:调用io后,立刻返回给结果;

需要注意的是:阻塞状态下,程序不会浪费CPU。CPU就会执行其他的线程。

3.1 阻塞模型

BIO的特点是IO执行的两个阶段都会被阻塞。

IO阻塞模型.png

以套接字接口为例来讲解此模型:

  1. 内核空间数据准备阶段:应用系统调用recvfrom(PS:作用:接收一个数据报并保存源地址 ),内核空间(kernel)就开始IO的数据准备的阶段。【此时是阻塞状态】对于network IO来说,在还没有收到一个完整的数据包情况下,kernel将会等到足够数据到来。
  2. 内存空间copy用户空间:在用户进程这边,整个进程会被阻塞。当kernel将数据准备好了,进程就会将数据从kernel空间拷贝到用户内存中。然后kernel返回执行结果,用户进程才解除block状态,重新运行起来。

3.2 非阻塞模型

非阻塞IO模型.png
  1. 定时查询内核空间(轮询-非阻塞[本质上是一个个的小阻塞]) :当用户进程发出read操作时,kernel没有准备好的时候,系统并不会block用户进程,而是立刻返回一个error。用户进程发现返回结果是一个error时,用户进程就知道kernel数据还没有准备好,于是用户进程就可以隔一段时间再次发送read操作。
  2. 内核空间数据准备好(阻塞状态):等到kernel中的数据准备好了,并且又再次收到了用户进程的系统调用(system call) recvfrom就马上将数据拷贝到用户内存,然后返回。拷贝整个数据的过程,进程仍然是属于阻塞的状态

(敲黑板,划重点),这段时间内,进程是被block的,所以用户进程需要不断的主动询问kernel数据好了没有。非阻塞IO不会交出CPU,而是一直会占用着CPU。

典型的非阻塞IO模型:

while(true){
    data = socket.read();
    if(data!= error){
        do some things...
        break;
    }
}

但是对于非阻塞IO就有一个非常严重的问题,在while循环中需要不断地去询问内核数据是否就绪,这样会导致CPU占用率非常高,因此一般情况下很少使用while循环这种方式来读取数据。

非阻塞IO将大的整片时间的阻塞分成多个小的阻塞,所以非阻塞IO的recvfrom被系统调用之后,进程并没有被阻塞,内核返回信息给用户进程。

PS:按照小胖的理解,好似一个悲观锁,一个乐观锁(CAS)

3.3 多路复用IO模型(非阻塞)

多路复用模型是目前使用较多的模型,JAVA NIO 实际上就是多路复用IO。

多路复用IO模型.png
  1. 监控进程阻塞-等待内核回调:当用户进程调用了select,那么整个进程会被block,同时,kernel会“监视”所有select负责的socket
  2. 内核回调-用户进程读取数据:当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel拷贝到用户进程。IO multiplexingblocking IO其实并没有太大的不同,因为IO mmultiplexing需要使用两个system call(select和recvfrom);

注:多路复用,目的不是加快单个连接的处理速度,而是处理更多的连接。

程序是被select这个函数block,而不是被socket IO给block的。

在多路复用IO模型中,只需要使用一个线程就可以管理多个socket,系统不需要建立新的进程或者线程,也不必维护这些线程和进程,并且只有在真正有socket读写事件进行时,才会使用IO资源,所以它大大减少了资源占用。
在Java NIO中,是通过selector.select()去查询每个通道是否有到达事件,如果没有事件,则一直阻塞在那里,因此这种方式会导致用户线程的阻塞。

多路复用和非阻塞的区别:

多路复用IO为何比非阻塞IO模型的效率高是因为在非阻塞IO中,不断地询问socket状态时通过用户线程去进行的,而在多路复用IO中,轮询每个socket状态是内核在进行的,这个效率要比用户线程要高的多。

多路复用原理:
深度理解select、poll和epoll
使用select 或者 poll 函数,在发生I/O事件后,无差别的轮询所有流,对他们进行操作。只不过poll结构是链表,没有select的1024个连接的限制。

IO多路复用的请求是异步的吗:

IO多路复用模型,是由内核态去判断socket是否有数据到达。实际上请求依旧会被阻塞(是被select/poll/epoll方法阻塞)。

  • Redis服务端就是典型的IO多路复用模型,即服务端使用单线程处理请求,将并发请求串行化。(也可以理解为选择器线程和处理请求的程序都运行在里面,这个线程即是IO线程,也是Worker线程)。
  • Java NIO也是IO复用模型,例如服务端接收20个连接,创建20个通道,并把他们注册到选择器上,此时不需要额外的线程。当某个通道有数据时,才用一个线程去处理它,因为20个通道是陆陆续续的有数据的,所以服务端最多的情况下也是几个线程去同时运行。

有两种线程,选择器运行在主线程中,Worker线程是新的线程。

3.4 异步IO模型

异步IO是需要操作系统的底层支持,在Java 7中,提供了Asynchronous IO。

异步IO.png
  1. 用户发出请求:用户进程发起read操作后,立刻就可以开始去做其它的事。
  2. 内核直接返回:从kernel的角度,当它受到一个异步读取操作之后,首先评它会立刻返回,所以不会对用户进程产生任何阻塞。
  3. 操作完成-调用用户程序:然后,kernel会等待数据准备完成。然后将数据拷贝到用户内存,拷贝完成后,kernel会给用户进程发送一个signal,交由用户进程操作。

数据内核空间--->用户空间的复制,是操作系统自己完成的。上面三种IO方式,在内核拷贝阶段都会引起用户系统阻塞。


4. 同步和非同步

4.1 同步:由用户系统完成内核空间到用户空间的复制,用户程序处于阻塞状态。
4.2 异步:由操作系统完成内核空间到用户空间的复制,依赖于操作系统。

5. 结论

image.png

注意同步IO和异步IO与阻塞IO和非阻塞IO是不同的两组概念。

  1. 阻塞IO和非阻塞IO是反映在IO操作的第一个阶段,在查看数据是否就绪时是如何处理的。阻塞IO会一直等待Socket IO,而非阻塞IO会直接返回。
  2. 同步IO和异步IO的关键区别反映在数据拷贝阶段是由用户线程完成还是内核完成。所以说异步IO必须要有操作系统的底层支持。

故:
传统IO:同步阻塞IO。
NIO:同步不阻塞IO。(多路复用)
BIO:异步不阻塞IO。(操作系统+多路复用)

参考文章:

相关文章

IO学习(1)Java-BIO体系学习
IO学习(2)-各种IO模型
IO学习(3)— IO和NIO的区别
IO学习(4)— select、poll、epoll的区别

推荐阅读更多精彩内容