一、 IO 五种模型

目录

一、用户空间和内核空间

二、BIO同步阻塞IO(Blocking IO)

二、同步非阻塞NIO(None Blocking IO)

三、IO多路复用模型(IO Multiplexing)

四、信号驱动IO(Signal Driven IO)

五、异步IO模型(Asynchronous IO)


一、用户空间和内核空间

操作系统的核心是内核,它独立与普通的应用程序,可以访问受保护的内核空间,也有访问底层硬件设备的所有权限。为了保护内核的安全,现在操作系统一般都强制用户不能直接操作内核,所以操作系统把内存空间划分成了两个部分:内核空间和用户空间。类似于饭店,老板将整个饭店划分成两个部分大厅和厨房,大厅用于顾客吃饭,厨房用于厨师做饭,厨房的门上一般贴着“厨房重地,闲人免进”。所以,当我们使用TCP发送数据的时候,需要先将数据从用户空间拷贝到内核空间,再由内核空间将数据从内核空间发送出去;当我们使用TCP读取数据的时候,数据先在内存空间准备好,再从内核空间拷贝到用户空间供用户进程使用。

所以,一次IO的读取操作分为两个阶段:

  • 等待内核空间数据准备阶段
  • 数据从内核空间拷贝到用户空间

IO读写的基础原理:

用户程序进行IO的读写依赖于底层的IO读写,涉及到两个缓冲区:内核缓冲区和进程缓冲区。

缓冲区的目的:减少频繁的与设备之间的物理交换。外部设备的直接读写涉及操作系统的中断,发生中断时需要保存之前的进程数据和状态等信息,而结束中断后还需要恢复,为了减少底层系统的时间、性能损耗,出现了内存缓冲区。

应用在read系统调用是,仅仅把数据从内核缓冲区复制到应用缓冲区(进程缓冲区),应用使用write系统调用时,仅仅把数据从进程缓冲去复制到内核缓冲区。底层操作系统会对内核缓冲区进行监控,等待缓冲区到达一定数量的时候,在进行IO设备的中断处理,集中执行物理设备的实际IO操作。用户程序的IO读写程序,在大多数情况下并没有进行实际的IO操作,而是在进程缓冲区和内核缓冲区之间进行数据交换。

为此,Unix根据以上将IO分为了一下五种模型:

  1. 阻塞型IO
  2. 非阻塞型IO
  3. IO多路复用
  4. 信号驱动IO
  5. 异步IO

二、BIO同步阻塞IO(Blocking IO)

阻塞IO,当用户进程发起请求时,需要内核IO操作彻底完成后,才返回到用户空间执行用户的操作。阻塞型IO在两个阶段是连续阻塞着的。并且每次客户端发送请求,服务端都需要建立一个线程去处理。

二、同步非阻塞NIO(None Blocking IO)

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

简单来说:阻塞是指用户空间(调用线程)一直在等待,而不能干别的事;非阻塞是指用户空间(调用线程)拿到内核返回的状态值就返回自己的空间,IO操作可以做就去做,不可以做,继续轮询。阻塞和非阻塞的区别是阻塞会导致当前线程挂起,会涉及内核态、用户态的切换,非阻塞是轮询,线程不会被挂起,占用CPU资源。非阻塞IO要求socket被设置为NONBLOCK。

三、IO多路复用模型(IO Multiplexing)

如何避免非阻塞IO模型中的轮询等待的问题呢,那就是IO多路复用模型。

IO多路复用,多个IO操作共同使用一个Select(选择器)去询问那些IO准备好了,selector负责通知那些数据准备好了的IO,他们在自己去请求内核。IO多路复用第一阶段也会阻塞在selector上,第二阶段拷贝数据也会阻塞。

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

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

举个例子来说明IO多路复用模型的流程。发起一个多路复用的IO的read操作的目标socket网络连接,流程如下:

  1. 选择注册器。首先,需要将read操作的目标socket网络连接提前注册到select/epoll选择器中,JAVA中对应的选择器类是Selector类。然后开启整个IO多路复用模型的轮询流程。
  2. 就绪状态的轮询。通过选择器的查询方法,查询注册过的所有socket就绪状态。通过查询的系统调用,内核会返回一个就绪的socket列表。当任何一个注册过的socket中的数据准备好了,内核缓冲区有数据(就绪)了,内核就将该socket加入到就绪的列表中。当用户线程调用了select查询方法,整个线程会被阻塞掉。
  3. 用户线程获得了就绪状态的列表后,根据其中的socket连接发起read系统调用,用户线程阻塞。内核开始复制数据,将数据从内核缓冲区复制到用户缓冲区。
  4. 复制完成后,内核返回结果,用户线程才会解除阻塞的状态,用户线程读取到了数据,继续执行。

       与NIO相比IO多路复用模式使用select/epoll,一个查询器可以同时处理大量的连接,系统不必为每一个连接创建线程,也不用在去维护这些线程,很大的节省了系统开销。

四、信号驱动IO(Signal Driven IO)

信号驱动IO,用户进程发起读取请求之前,先注册一个信号给内核说明自己需要什么数据,这个注册请求立即返回,等待内核数据准备好了主动通知用户进程,用户进程再去请求读取数据,此时,需要等待数据从内核空间拷贝到用户空间再返回。这个和餐厅下单取号,叫号取单类似。去餐厅点完单后拿到小号,就可以去做自己的事了。当餐厅准备好餐后,会叫号取餐,你再去取餐。

信号驱动,第一阶段不阻塞,第二阶段读取数据阻塞。

五、异步IO模型(Asynchronous IO)

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

大致流程如下:

  1. 用户线程发起read系统调用,立刻可以去干其他事,用户线程不用阻塞。
  2. 内核开始IO的第一阶段:准备数据。等到数据准备好了,内核就会将数据复制到用户缓冲区。
  3. 内核会给用户线程发送一个信号或者回调用户线程注册的毁掉接口,告诉用户线程read操作完成了。
  4. 用户线程读取用户缓冲区的数据,完成后续的业务操作。
原文地址:https://www.cnblogs.com/demo-alen/p/13547226.html