分享

IO的概念和5种IO模型

 xkl135 2018-06-24

一、什么是IO?

我们都知道unix世界里、一切皆文件、而文件是什么呢?文件就是一串二进制流而已、不管socket、还是FIFO、管道、终端、对我们来说、一切都是文件、一切都是流、在信息交换的过程中、我们都是对这些流进行数据的收发操作、简称为I/O操作(input and output)、往流中读出数据、系统调用read、写入数据、系统调用write、不过话说回来了、计算机里有这么多的流、我怎么知道要操作哪个流呢?做到这个的就是文件描述符、即通常所说的fd、一个fd就是一个整数、所以对这个整数的操作、就是对这个文件(流)的操作、我们创建一个socket、通过系统调用会返回一个文件描述符、那么剩下对socket的操作就会转化为对这个描述符的操作、不能不说这又是一种分层和抽象的思想、

二、IO交互

通常用户进程中的一个完整IO分为两阶段:

  1. 用户空间 <-----> 内核空间、

  2. 内核空间 <-----> 设备空间、


内核空间中存放的是内核代码和数据、而进程的用户空间中存放的是用户程序的代码和数据、不管是内核空间还是用户空间、它们都处于虚拟空间中、Linux使用两级保护机制:0级供内核使用、3级供用户程序使用、

操作系统和驱动程序运行在内核空间、应用程序运行在用户空间、两者不能简单地使用指针传递数据、因为Linux使用的虚拟内存机制、其必须通过系统调用请求kernel来协助完成IO动作、内核会为每个IO设备维护一个缓冲区、用户空间的数据可能被换出、当内核空间使用用户空间指针时、对应的数据可能不在内存中

对于一个输入操作来说、进程IO系统调用后、内核会先看缓冲区中有没有相应的缓存数据、没有的话再到设备中读取、因为设备IO一般速度较慢、需要等待、内核缓冲区有数据则直接复制到进程空间、

所以、对于一个网络输入操作通常包括两个不同阶段:
(1)等待网络数据到达网卡 –> 读取到内核缓冲区
(2)从内核缓冲区复制数据 –> 用户空间

IO有内存IO、网络IO和磁盘IO三种、通常我们说的IO指的是后两者

三、POSIX

对IO底层交互感兴趣的小伙伴可以好好了解一下POSIX(Portable Operating System Interface for Computing System)、我对深沉次原理也不怎么熟、之所以写此篇博文也是为了后面的Java IO学习、深入浅出点到即可、此章节给有兴趣的朋友一个引子、

四、IO模型

《UNIX网络编程》说得很清楚、5种IO模型分别是阻塞IO模型、非阻塞IO模型、IO复用模型、信号驱动的IO模型、异步IO模型、前4种为同步IO操作、只有异步IO模型是异步IO操作、请仔细阅读IO交互便于理解IO模型

(一)阻塞IO模型

当用户进程调用了recvfrom这个系统调用、内核就开始了IO的第一个阶段:准备数据、对于网络IO来说、很多时候数据在一开始还没有到达(比如、还没有收到一个完整的UDP包)、这个时候内核就要等待足够的数据到来、而在用户进程这边、整个进程会被阻塞、当内核一直等到数据准备好了、它就会将数据从内核中拷贝到用户内存、然后返回结果、用户进程才解除阻塞的状态、重新运行起来、几乎所有的程序员第一次接触到的网络编程都是从listen()、send()、recv()等接口开始的、这些接口都是阻塞型的、

blocking IO的特点就是在IO执行的两个阶段(等待数据和拷贝数据两个阶段)都被阻塞了、

典型应用:阻塞Socket、Java BIO

  • 进程阻塞挂起不消耗CPU资源、及时响应每个操作

  • 实现难度低、开发应用较容易

  • 适用并发量小的网络应用开发

  • 不适用并发量大的应用、因为一个请求IO会阻塞进程、所以、得为每请求分配一个处理进程(线程)以及时响应、系统开销大

(二)非阻塞IO模型

当用户进程发出read操作时、如果内核中的数据还没有准备好、那么它并不会block用户进程、而是立刻返回一个error、从用户进程角度讲、它发起一个read操作后、并不需要等待、而是马上就得到了一个结果、用户进程判断结果是一个error时、它就知道数据还没有准备好、于是它可以再次发送read操作、一旦内核中的数据准备好了、并且又再次收到了用户进程的系统调用、那么它马上就将数据拷贝到了用户内存、然后返回、非阻塞的接口相比于阻塞型接口的显著差异在于、在被调用之后立即返回、

在非阻塞式IO中、用户进程其实是需要不断的主动询问kernel数据准备好了没有

典型应用:Socket 设置 NONBLOCK

  • 进程轮询(重复)调用、消耗CPU的资源

  • 实现难度低、开发应用相对阻塞IO模式较难

  • 适用并发量较小、且不需要及时响应的网络应用开发

(三)IO复用模型

多个的进程的IO可以注册到一个复用器(select)上、当用户进程调用该select、select会监听所有注册进来的IO、如果select所有监听的IO在内核缓冲区都没有可读数据、select调用进程会被阻塞、而当任一IO在内核缓冲区中有可数据时、select调用就会返回、而后select调用进程可以自己或通知另外的进程(注册进程)来再次发起读取IO、读取内核中准备好的数据、多个进程注册IO后、只有一个select调用进程被阻塞

IO复用相对阻塞和非阻塞更难简单说明、所以额外解释一段、其实IO复用模型和阻塞IO模型并没有太大的不同、事实上、还更差一些、因为这里需要使用两个系统调用(select和 recvfrom)、而阻塞IO模型只有一次系统调用(recvfrom)、但是、用select的优势在于它可以同时处理多个连接、所以如果处理的连接数不是很高的话、使用select/epoll的web server不一定比使用多线程加阻塞IO的web server性能更好、可能延迟还更大、select/epoll的优势并不是对于单个连接能处理得更快、而是在于能处理更多的连接

在IO复用模型中、对于每一个socket、一般都设置成为非阻塞、但是、如上图所示、整个用户的进程其实是一直被阻塞的、只不过进程是被select这个函数阻塞、而不是被socket IO给阻塞

典型应用:Java NIO、Nginx(epoll、poll、select)

  • 专一进程解决多个进程IO的阻塞问题、性能好、Reactor模式

  • 实现、开发应用难度较大

  • 适用高并发服务应用开发、一个进程/线程响应多个请求

(四)信号驱动式IO模型

信号驱动式IO就是指进程预先告知内核、向内核注册一个信号处理函数、然后用户进程返回不阻塞、当内核数据就绪时会发送一个信号给进程、用户进程便在信号处理函数中调用IO读取数据、从图中明白实际IO内核拷贝到用户进程的过程还是阻塞的、信号驱动式IO并没有实现真正的异步、因为通知到进程之后、依然是由进程来完成IO操作、

这和后面的异步IO模型很容易混淆、需要理解IO交互并结合五种IO模型的比较阅读

在信号驱动式IO模型中、依然不符合POSIX描述的异步IO、只能算是半异步、并且实际中并不常用、

典型应用:(不知道)

  • 回调机制、实现、开发应用难度大

(五)异步IO模型

用户进程发起aio_read(POSIX异步IO函数aio_或者lio_开头)操作之后、给内核传递描述符、缓冲区指针、缓冲区大小和read相同的三个参数以及文件偏移(与lseek类似)、告诉内核当整个操作完成时、如何通知我们、立刻就可以开始去做其它的事、而另一方面、从内核的角度、当它受到一个aio_read之后、首先它会立刻返回、所以不会对用户进程产生任何阻塞、然后、内核会等待数据准备完成、然后将数据拷贝到用户内存、当这一切都完成之后、内核会给用户进程发送一个信号、告诉它aio_read操作完成了

异步IO的工作机制是:告知内核启动某个操作、并让内核在整个操作完成后通知我们、这种模型与信号驱动的IO区别在于、信号驱动IO是由内核通知我们何时可以启动一个IO操作、这个IO操作由用户自定义的信号函数来实现、而异步IO模型是由内核告知我们IO操作何时完成、

这和前面的信号驱动式IO模型很容易混淆、需要理解IO交互并结合五种IO模型的比较阅读

在异步IO模型中、真正实现了POSIX描述的异步IO、是五种IO模型中唯一的异步模型

典型应用:Java 7 AIO、高性能服务器应用

  • 不阻塞、数据一步到位、Proactor模式

  • 需要操作系统的底层支持、LINUX 2.5 版本内核首现、2.6 版本产品的内核标准特性

  • 回调机制、实现、开发应用难度大

  • 非常适合高性能高并发应用

(六)五种IO模型的比较

  1. 阻塞IO和非阻塞IO的区别在哪?
    前面的介绍中其实已经很明确的说明了这两者的区别、调用阻塞会一直阻塞住对应的进程直到操作完成、而非阻塞IO在内核还没准备数据的情况下会立刻返回、阻塞和非阻塞关注的是进程在等待调用结果时的状态、阻塞是指调用结果返回之前、当前进程会被挂起、调用进程只有在得到结果才会返回、非阻塞调用指不能立刻得到结果、该调用不会阻塞当前进程、

  2. 同步IO和异步IO区别在哪?
    两者的区别就在于同步做IO操作的时候会将进程阻塞、按照这个定义、之前所述的阻塞IO、非阻塞IO、IO复用、信号驱动都属于同步IO、有人可能会说、非阻塞IO并没有被阻塞啊、这里有个非常狡猾的地方、定义中所指的IO操作是指真实的IO操作、就是例子中的recvfrom这个系统调用、非阻塞IO在执行recvfrom这个系统调用的时候、如果内核的数据没有准备好、这时候不会阻塞进程、但是、当内核中数据准备好的时候、recvfrom会将数据从内核拷贝到用户内存中、这个时候进程是被阻塞了、信号驱动也是同样的道理、在这段时间内、进程是被阻塞的、而异步IO则不一样、当进程发起IO操作之后、就直接返回再也不理睬了、直到内核发送一个信号、告诉进程说IO完成、在这整个过程中、进程完全没有被阻塞、

    同异步IO的根本区别在于、同步IO主动的调用recvfrom来将数据拷贝到用户内存、而异步则完全不同、它就像是用户进程将整个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

  3. 信号驱动式IO和异步IO的区别?
    这里之所以单独拿出来是因为如果还没有清除IO概念很容易混淆、所以理解IO模型之前一定要理解IO概念、如果看完前面两个问题、相信也能理解信号驱动IO与异步IO的区别在于启用异步IO意味着通知内核启动某个IO操作、并让内核在整个操作(包括数据从内核复制到用户缓冲区)完成时通知我们、也就是说、异步IO是由内核通知我们IO操作何时完成、即实际的IO操作也是异步的、信号驱动IO是由内核通知我们何时可以启动一个IO操作、这个IO操作由用户自定义的信号函数来实现

五、总结

全篇最大的难点在于真正理解数据是如何从设备空间扭转到内核空间再到用户空间

    本站是提供个人知识管理的网络存储空间,所有内容均由用户发布,不代表本站观点。请注意甄别内容中的联系方式、诱导购买等信息,谨防诈骗。如发现有害或侵权内容,请点击一键举报。
    转藏 分享 献花(0

    0条评论

    发表

    请遵守用户 评论公约

    类似文章 更多