前面总结了很多IO、NIO相关的基础知识点,还总结了IO和NIO之间的区别及各自适用场景,本文会从另一个视角来学习一下IO,即IO模型。什么是IO模型?对于不同人、在不同场景下给出的答案是不同的,所以先限定一下本文的上下文:Linux环境下的network IO。

  本文会从如下几个方面展开:

  一些基础概念

  I/O模型

  总结

1. 一些基础概念

  IO模型这个概念属于比较基础的底层概念,在此之前容我再先简单介绍一些涉及到的更底层的概念,帮助对I/O模型的理解:

1.1 用户空间与内核空间

  现在操作系统都是采用虚拟存储器,对于32位操作系统而言,它的寻址空间(虚拟存储空间)为4G(2的32次方)。操作系统的核心是内核,独立于普通的应用程序,可以访问受保护的内存空间,也有访问底层硬件设备的所有权限。为了保证用户进程不能直接操作内核(kernel),保证内核的安全,操心系统将虚拟空间划分为两部分,一部分为内核空间,一部分为用户空间。针对linux操作系统而言,将最高的1G字节(从虚拟地址0xC0000000到0xFFFFFFFF),供内核使用,称为内核空间,而将较低的3G字节(从虚拟地址0x00000000到0xBFFFFFFF),供各个进程使用,称为用户空间。

1.2 文件描述符

  对于内核而言,所有打开文件都由文件描述符引用。文件描述符是一个非负整数。当打开一个现存文件或创建一个新文件时,内核向进程返回一个文件描述符。当读、写一个文件时,用open或create返回的文件描述符标识该文件,将其作为参数传送给read或write。文件描述符这一概念往往只适用于UNIX、Linux这样的操作系统。

1.3 缓存 I/O

  缓存I/O又被称作标准I/O,大多数文件系统的默认I/O操作都属于缓存I/O。在Linux的缓存I/O机制中,操作系统会将I/O的数据缓存在文件系统的页缓存(page cache)中,也就是说,数据会先被拷贝到操作系统内核的缓冲区中,然后才会从操作系统内核的缓冲区拷贝到应用程序的地址空间。

  因为数据在传输过程中需要在应用程序地址空间和内核进行多次数据拷贝操作,所以这些数据拷贝操作所带来的CPU以及内存开销是非常大的,这也是缓存I/O所带来的缺点。

2. I/O模型

  上面有提到,对于一次IO访问(比如read),数据会先被复制到操作系统内核缓冲区中,然后再从操作系统内核的缓冲区复制到应用程序的地址空间。也就是说,当一个IO操作发生时,会经历两个阶段:

  1. 数据准备阶段(Waiting for the data to be ready);
  2. 将数据从内核复制到进程中(Copying the data from the kernel to the process);

  这是因为存在上面两个阶段,linux系统产生了下面5种I/O模型:

  • 阻塞I/O(blocking IO)
  • 非阻塞I/O(nonblocking IO)
  • I/O多路复用(IO multiplexing)
  • 信号驱动I/O(signal driven IO)
  • 异步I/O(asynchronous IO)

  下面我们来一一介绍(本文暂介绍除信号驱动I/O外其余4种IO模型)。

2.1 阻塞I/O(blocking IO)

  在linux中,默认情况下socket都是阻塞式的,一个典型的读操作流程大概是这样的:

  当用户进程调用recvfrom这个系统调用时,kernel就开始了IO的第一个阶段:数准备阶段(对于网络IO来说,很多时候数据在一开始还没有到达。比如,还没有收到一个完整的TCP包。这个时候kernel就要等待直到所有数据到达),这个过程相当于将数据被复制到操作系统内核的缓冲区,是需要一个过程,需要等待的。而在用户进程这边,整个进程会被阻塞(当然,是进程自己选择的阻塞)。当kernel一直等到数据准备好了,它就会将数据从kernel中复制到用户内存,然后kernel返回结果,用户进程才会解除block的状态,重新运行起来。

  Blocking IO最大的特点就是在IO执行的两个阶段都被会阻塞。

2.2 非阻塞I/O(nonblocking IO)

  可以将设置socket设置为non-blocking模式,对于此时的读操作,流程大概是这个样子的:

  当用户进程发起recvfrom这个系统调用时,如果kernel中的数据还没有准备好,那么它并不会block用户进程,而是立刻返回一个error。从用户进程角度讲 ,它发起一个recvfrom调用,马上就得到了一个结果,不管有没有数据。用户进程判断结果是一个error时,它就知道数据还没有准备好,于是它可以再次发起recvfrom操作。一旦kernel中的数据准备好了,并且又再次收到了用户进程的system call,那么它接下来就将数据复制到用户内存,然后返回。

  Nonblocking IO的特点是用户进程需要不断的主动询问kernel数据好了没有,这个过程不阻塞,但是在IO的第二个阶段还是需要等待内核将数据复制到用户进程的,也就是这部分还是会阻塞的。

2.3 I/O多路复用(IO multiplexing)

  IO multiplexing就是我们说的IO多路复用,有些地方也称这种IO方式为event driven IO。主要是通过select/epoll来实现单个process同时处理多个网络连接的IO,它的基本原理是依赖select,poll,epoll这些function来不断的轮询负责的所有socket,当某个socket有数据到达了,就通知用户进程。

  当用户进程调用了select时整个进程会被block,同时kernel会“监视”所有select负责的socket,当任何一个socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从kernel复制到用户进程。

  所以,I/O多路复用的特点是通过一种机制使得一个进程能同时等待多个文件描述符(至于为什么是文件描述符,因为对于kernel而言,所有打开文件都由文件描述符引用,而一次IO连接也相当于打开文件),而这些文件描述符(套接字描述符)其中的任意一个进入读就绪状态,select()函数就可以返回。

  IO多路复用是建立在Nonblocking IO之上的,即通过单个线程来“监视”多个IO连接,谁准备就绪了就处理谁,处理的过程和Nonblocking的过程是一样的。

2.4 异步I/O(asynchronous IO)

  Asynchronous IO不同于上面三种模型,先看一下它的流程:

  用户进程发起read操作之后,立刻就可以开始去做其它的事情了。另一方面,从kernel的角度,当它收到一个asynchronous read调用之后,首先它会立刻返回,所以不会对用户进程产生任何block。然后,kernel会等待数据准备完成,并且数据准备好之后再将数据复制到用户内存,当这一切都完成之后,kernel会给用户进程发送一个signal,告诉它read操作完成了。

  其实,从这里可以看出异步I/O最大的特点就是,将前面提到的两步IO操作全部异步化了,整个过程完全不阻塞,而Nonblocking IO在复制数据到用户进程这一步其实还是阻塞的,只是数据准备阶段不阻塞而已。

3. 总结

  本文总结了linux网络IO中常见的五种IO模型,虽说不是Java IO,但是Java IO、NIO中也是遵循同样的IO模型,关于这一点,后面会专门写一篇文章来阐述。

  五种IO模型分别为:阻塞I/O(blocking IO)、非阻塞I/O(nonblocking IO)、I/O多路复用(IO multiplexing)、信号驱动I/O(signal driven IO)、异步I/O(asynchronous IO)。在这五种模型的基础上,有两个概念不得不提,阻塞和非阻塞、同步和异步,这两个概念容易搞混,:

3.1 阻塞和非阻塞

  阻塞很好理解,就是进程或者线程停在那里等待某个状态,什么都不干。但是什么情况称为阻塞,什么情况称为非阻塞呢?在IO模型中的定义是取决于前面提到的第一阶段:数据准备阶段,也就是说,在这个阶段会导致进程或线程阻塞的IO就成为阻塞式IO,反之就是非阻塞式IO。所以Nonblocking IO在第一阶段是可以做别的事情的,但是在第二阶段任然是阻塞的,这点需要注意。

3.2 同步和异步

  在说明同步IO模型和异步IO模型的区别之前,需要先给出两者的定义。POSIX的定义是这样子的:

  • A synchronous I/O operation causes the requesting process to be blocked until that I/O operation completes;
  • An asynchronous I/O operation does not cause the requesting process to be blocked;

  两者的区别就在于执行"IO Operation"的时候是否会导致进程或线程阻塞,这个"IO Operation"是指前面提到的第二阶段:将数据从kernel复制到用户进程中。按照这个定义,前面说的Blocking IO、Non-blocking IO、IO multiplexing就都属于synchronous IO,只有异步IO才属于Asynchronous IO,因为只有它在整个IO过程中都不会导致阻塞。

  最后再附上一张五种IO模型比较图,以帮助理解:

参考文献

Linux IO模式及 select、poll、epoll详解

Java NIO学习系列五:I/O模型的更多相关文章

  1. Java NIO学习系列六:Java中的IO模型

    前文中我们总结了linux系统中的5中IO模型,并且着重介绍了其中的4种IO模型: 阻塞I/O(blocking IO) 非阻塞I/O(nonblocking IO) I/O多路复用(IO multi ...

  2. Java NIO学习系列四:NIO和IO对比

    前面的一些文章中我总结了一些Java IO和NIO相关的主要知识点,也是管中窥豹,IO类库已经功能很强大了,但是Java 为什么又要引入NIO,这是我一直不是很清楚的?前面也只是简单提及了一下:因为性 ...

  3. Java NIO学习系列一:Buffer

    前面三篇文章中分别总结了标准Java IO系统中的File.RandomAccessFile.I/O流系统,对于I/O系统从其继承体系入手,力求对类数量繁多的的I/O系统有一个清晰的认识,然后结合一些 ...

  4. Java NIO学习系列三:Selector

    前面的两篇文章中总结了Java NIO中的两大基础组件Buffer和Channel的相关知识点,在NIO中都是通过Channel和Buffer的协作来读写数据的,在这个基础上通过selector来协调 ...

  5. Java NIO学习系列七:Path、Files、AsynchronousFileChannel

    相对于标准Java IO中通过File来指向文件和目录,Java NIO中提供了更丰富的类来支持对文件和目录的操作,不仅仅支持更多操作,还支持诸如异步读写等特性,本文我们就来学习一些Java NIO提 ...

  6. Java NIO学习系列二:Channel

    上文总结了Java NIO中的Buffer相关知识点,本文中我们来总结一下它的好兄弟:Channel.上文有说到,Java NIO中的Buffer一般和Channel配对使用,NIO中的所有IO都起始 ...

  7. Java NIO学习笔记五 FileChannel(文件通道)

    Java NIO FileChannel Java NIO FileChannel是连接文件的通道.使用FileChannel,您可以从文件中读取数据和将数据写入文件.Java NIO FileCha ...

  8. Java NIO 学习笔记五 缓冲区补充

    1.缓冲区分配 方法   以 ByteBuffer 为例 (1)使用静态方法 ByteBuffer buffer = ByteBuffer.allocate( 500 ); allocate() 方法 ...

  9. Java IO学习笔记五:BIO到NIO

    作者:Grey 原文地址: Java IO学习笔记五:BIO到NIO 准备环境 准备一个CentOS7的Linux实例: 实例的IP: 192.168.205.138 我们这次实验的目的就是直观感受一 ...

随机推荐

  1. Delphi检测用户是否具有administrator权限(OpenThreadToken,OpenProcessToken,GetTokenInformation,AllocateAndInitializeSid和EqualSid)

    检测用户是否具有administrator权限const SECURITY_NT_AUTHORITY: TSIDIdentifierAuthority = (Value: (0, 0, 0, 0, 0 ...

  2. Delphi XE6 如何设计并使用FireMonkeyStyle

    介绍   FireMonkey使用Style来控制控件的显示方式. 每个控件都有一个StyleLookup属性,FireMonkey就是通过控件的这个属性来在当前窗体的StyleBook控件中查找匹配 ...

  3. 从 docker 到 runC

    笔者在前文<RunC 简介>和<Containerd 简介>中分别介绍了 runC 和 containerd.本文我们将结合 docker 中的其它组件探索 docker 是如 ...

  4. ZooKeeper学习第五期--ZooKeeper管理分布式环境中的数据(转)

    转载来源:https://www.cnblogs.com/sunddenly/p/4092654.html 引言 本节本来是要介绍ZooKeeper的实现原理,但是ZooKeeper的原理比较复杂,它 ...

  5. 使用LinkedList模拟一个堆栈或者队列数据结构。

    堆栈:先进后出 First in last out filo 队列:先进先出 First in last out filo使用LinkedList的方法,addFirst addLast getFir ...

  6. Django 你需要掌握的模型层(标签、过滤器、模板的继承与导入)

    Django 模型层(标签.过滤器.模板的继承与导入) 好文章来自超哥:https://www.cnblogs.com/guanchao/p/11006062.html   过滤器/自定义过滤器 模板 ...

  7. HBase 学习之路(四)—— HBase集群环境配置

    一.集群规划 这里搭建一个3节点的HBase集群,其中三台主机上均为Regin Server.同时为了保证高可用,除了在hadoop001上部署主Master服务外,还在hadoop002上部署备用的 ...

  8. Spring5深度源码分析(三)之AnnotationConfigApplicationContext启动原理分析

    代码地址:https://github.com/showkawa/spring-annotation/tree/master/src/main/java/com/brian AnnotationCon ...

  9. Oracle insert all用法简介

    insert all是oracle中用于批量写数据的 现在直接通过例子学习一下,比较简单直观,例子来自<收获,不止SQL优化>一书 环境准备 create table t as selec ...

  10. 08、MySQL—字符串型

    字符串型 1.Char 定长字符:指定长度之后,系统一定会分配指定的空间用于存储数据 基本语法: char(L),L代表字符数(中文与英文字母一样),L长度为0到255 2.Varchar 变长字符: ...