底层原理 | IO模型

1 Linux的五种IO模型

IO 是内存和外部设备 ( 硬盘、终端和网络等 ) 拷贝数据的过程。

在Linux下有5种可用的I/O模型:

  1. 阻塞式I/O:Blocking IO
  2. 非阻塞式I/O: Non-blocking IO
  3. 多路复用I/O:IO multiplexing
  4. 信号驱动式I/O:Signal-driven IO
  5. 异步I/O:Asynchronous IO

对于这五种IO模型,Java并不是一开始就都全部支持,而是有一个逐步演进的过程:

  • 在JDK1.4之前,Java的IO模型只支持阻塞式IO(Blocking IO),简称为BIO;
  • 在JDK1.4时,支持了I/O多路复用模型,相对于之前的IO模型,这是一个新的模型,所以称之为NIO(New IO),有新就有旧,所以有时也把BIO称之为OIO(old IO),其实都是一个意思。到现在为止,JDK1.8都已经出来了,JDK1.4时引入的nio包,也没有什么新鲜的了,所以更多的人愿意把NIO理解为None-Blocking IO,即非阻塞IO;
  • 在JDK1.7时,对NIO包进行了升级,支持了异步I/O(Asynchronous IO),简称为AIO,因为是对nio包的升级,所有有时又称之为NIO2.0;

在这里,我们以一个网络IO来举例,对于一个network IO(以read举例),它会涉及到两个系统对象:一个是调用这个IO的进程,另一个就是系统内核。当一个read操作发生时,它会经历两个阶段:
在这里插入图片描述

  • 阶段1:等待数据准备好;
  • 阶段2:将数据从内核拷贝到进程中;

图中忽略了很多细节,仅显示了涉及到的基本步骤 ,注意图中用户空间和内核空间的概念:

  • 用户空间是常规进程所在区域。 JVM 就是常规进程,驻守于用户空间。用户空间是非特权区域:比如,在该区域执行的代码就不能直接访问硬件设备。
  • 内核空间是操作系统所在区域。内核代码有特别的权力:它能与设备控制器通讯,控制着用户区域进程的运行状态,等等。最重要的是,所有 I/O 都直接(如这里所述)或间接通过内核空间。

当进程请求 I/O 操作的时候,它执行一个系统调用将控制权移交给内核。C/C++程序员所熟知的底层函数 open( )、 read( )、 write( )和 close( )要做的无非就是建立和执行适当的系统调用。当内核以这种方式被调用时,它随即采取必要的步骤,找到进程所需数据,并把数据传送到用户空间内的指定缓冲区。内核试图对数据进行高速缓存或预读取,因此进程所需数据可能已经在内核空间里了。如果是这样,该数据只需简单地拷贝出来即可。如果数据不在内核空间,则进程被挂起,内核着手把数据读进内存。

了解了这两个阶段的作用之后,我们接下来就可以深入讲解五种IO模型了,他们的区别就是在这两个阶段上有着不同的逻辑

2 Blocking IO

在 Linux 中,默认情况下所有的 socket 都是阻塞的,一个典型的读操作流程大概是这样:
在这里插入图片描述

  • 第一步:等待数据从网络中到达。当所有等待数据到达时,它被复制到内核中的某个缓冲区。
  • 第二步:把数据从内核缓冲区复制到应用程序缓冲区。

当用户进程发出 read 操作时,内核就开始了IO的第一个阶段:准备数据。对于 network io 来说,很多时候数据在一开始还没有到达(比如,还没有收到一个完整的UDP包),这个时候内核就要等待足够的数据到来。而在用户进程这边,整个进程会被阻塞。当内核一直等到数据准备好了,它就会将数据从内核中拷贝到用户内存,然后内核返回结果,用户进程才解除阻塞的状态,重新运行起来。

所以,blocking IO的特点就是在IO执行的两个阶段都被block了。

3 非阻塞式I/O

Linux 下,可以通过设置 socket 使其变为 non-blocking。当对一个 non-blocking socket 执行读操作时,流程是这个样子:
Image.png
从图中可以看出,当用户进程发出 read 操作时,如果内核中的数据还没有准备好,那么它并不会阻塞用户进程,而是立刻返回一个 error。 从用户进程角度讲 ,它发起一个 read 操作后,并不需要等待,而是马上就得到了一个结果。用户进程判断结果是一个 error 时,它就知道数据还没有准备好,于是它可以再次发送 read 操作。一旦内核中的数据准备好了,并且又再次收到了用户进程的 system call,那么它马上就将数据拷贝到了用户内存,然后返回。

所以,用户进程第一个阶段不是阻塞的,需要不断的主动询问内核数据好了没有;第二个阶段依然总是阻塞的。

4 I/O多路复用

称为多路IO模型或IO复用,意思是可以检查多个IO等待的状态。有三种IO复用模型:select、poll和epoll。其实它们都是一种函数,用于监控指定文件描述符的数据是否就绪。

就绪指的是对某个系统调用不再阻塞了,可以直接执行IO。例如对于read()来说,数据准备好了就是就绪状态,此时read()可以直接去读取数据且能立即读取到数据。

IO复用同非阻塞IO本质一样,不过利用了新的 select 系统调用,由内核来负责本来是请求进程该做的轮询操作。看似比非阻塞IO还多了一个系统调用开销,不过因为可以支持多路IO,才算提高了效率。

它的基本原理就是 select /epoll 这个 function 会不断的轮询所负责的所有 socket,当某个 socket 有数据到达了,就通知用户进程。它的流程如图:
Image.png
当用户进程调用了 select,那么整个进程会被阻塞,而同时,内核会“监视”所有 select 负责的 socket,当任何一个 socket 中的数据准备好了,select 就会返回。这个时候用户进程再调用 read 操作,将数据从内核拷贝到用户进程。

这个图和 blocking IO 的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个 system call(select 和 recvfrom),而 blocking IO 只调用了一个 system call(recvfrom)。但是,用 select 的优势在于它可以同时处理多个 connection。

所以,如果处理的连接数不是很高的话,使用 select/epoll 的 web server 不一定比使用 multi-threading + blocking IO 的 web server 性能更好,可能延迟还更大。

select/epoll 的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。

在 IO multiplexing model 中,实际中对于每一个 socket,一般都设置成为 non-blocking,但是,如上图所示,整个用户的进程其实是一直被 阻塞的。只不过进程是被 select 这个函数阻塞,而不是被 socket IO 给阻塞。

5 信号驱动式I/O

用的很少,就不做讲解了。直接上图
Image.png

6 异步I/O

这类函数的工作机制是告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到用户空间)完成后通知我们。如图:
Image.png
用户进程发起 read 操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个 asynchronous read 之后,首先它会立刻返回,所以不会对用户进程产生任何阻塞。然后,内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,内核会给用户进程发送一个 signal,告诉它 read 操作完成了。 在这整个过程中,进程完全没有被阻塞。

7 同步IO和异步IO、阻塞和非阻塞的区分

阻塞和非阻塞,体现在当前进程是否可执行,是否能获取到CPU。

当阻塞和非阻塞的概念体现在IO模型上:

  • 阻塞IO:从开始发起IO操作开始就阻塞,直到IO完成才返回,所以进程会立即进入睡眠态;
  • 非阻塞IO:发起IO操作时,如果当前数据已就绪,则切换到内核态由内核完成数据拷贝(从kernel buffer拷贝到user buffer),此时进程被阻塞,因为它的CPU已经被内核抢走了。如果发起IO操作时数据未就绪,则立即返回而不阻塞,即进程继续享有CPU,可以继续任务。但进程不知道数据何时就绪,所以通常会采用轮循代码(比如while循环)不断判断数据是否就绪,当数据最终就绪后,切换到内核态,进程仍然被阻塞;

同步和异步,考虑的是两边数据是否同步(比如kernel buffer和user buffer之间数据是否同步)。同步和异步的区别体现在两边数据尚未完成同步时的行为:

  • 同步:在保持两边数据同步的过程中,进程被阻塞,由内核抢占其CPU去完成数据同步,直到两边数据同步,进程才被唤醒;
  • 异步:在保持两边数据同步的过程中,由内核默默地在后台完成数据同步,内核不会抢占进程的CPU,所以进程自身不被阻塞,当内核完成两端数据同步时,通知进程已同步完成;

这里阻塞和非阻塞、同步和异步都是广义的概念,上面所做的解释适用于所有使用这些术语的情况,而不仅仅是本文所专注的IO模型。

回到阻塞、非阻塞、同步、异步的IO模型,再对它们啰嗦啰嗦。

阻塞、非阻塞、IO复用、信号驱动都是同步IO模型。需注意,虽然不同IO模型在加载数据到kernel buffer的数据准备过程中可能阻塞、可能不阻塞,但kernel buffer才是 read() 函数读取数据时的对象,同步的意思是让kernel buffer和user buffer数据同步。在保持kernel buffer和user buffer同步的过程中,CPU将从执行 read() 操作的进程切换到内核态,内核获取CPU拷贝数据到user buffer,所以执行 read() 操作的进程在这个同步的阶段中是被阻塞的。

只有异步IO模型才是异步的,因为它调用的是具有【神力】的异步IO函数(如aio_read()),调用这些函数时会请求内核,当数据已经拷贝到user buffer后,通知进程并执行指定的操作。

需要注意的是,无论是哪种IO模型,在将数据从kernel buffer拷贝到user buffer的这个阶段,都是需要CPU参与的。只不过,同步IO模型和异步IO模型中,CPU参与的方式不一样:

  • 同步IO模型中,调用 read() 的进程会切换到内核,由内核占用CPU来执行数据拷贝,所以原进程在此阶段一直被阻塞;
  • 异步IO模型中,由内核在后台默默的执行数据拷贝,所以原进程在此阶段不被阻塞

在这里插入图片描述


参考文章:
  1. Unix五种IO模型
  2. 五种IO模型透彻分析
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值