I/O

I/O模型

Posted by 杨一 on 2020-03-30

什么是IO

我们常说的IO,指的是文件的输入和输出,但是在操作系统层面是如何定义IO的呢?到底什么样的过程可以叫做是一次IO呢?

拿一次磁盘文件读取为例,我们要读取的文件是存储在磁盘上的,我们的目的是把它读取到内存中。可以把这个步骤简化成把数据从硬件(硬盘)中读取到用户空间中。

Java中,主要有三种IO模型,分别是阻塞IO(BIO)、非阻塞IO(NIO)和 异步IO(AIO)。

Java中提供的IO有关的API,在文件处理的时候,其实依赖操作系统层面的IO操作实现的,对操作系统的各种IO模型的封装。

比如在Linux 2.6以后,Java中NIO和AIO都是通过epoll(多路复用器)来实现的,而在Windows上,AIO是通过IOCP来实现的。

Linux 五种I/O模型

同步I/O

阻塞I/O模型

阻塞 I/O 是最简单的 I/O 模型,一般表现为进程或线程等待某个条件,如果条件不满足,则一直等下去。条件满足,则进行下一步操作。

应用进程通过系统调用 recvfrom 接收数据,但由于内核还未准备好数据报,应用进程就会阻塞住,直到内核准备好数据报,recvfrom 完成数据报复制工作,应用进程才能结束阻塞状态。

缺点: 比较耗费时间,比较适合那种对鱼的需求量小的情况(并发低,时效性要求低)。

用户进程发出请求调用内核,内核发现没有数据,用户线程就一直阻塞,但是这个阻塞是不消耗cpu资源的,随后完成数据拷贝,进程结束。

非阻塞I/O模型

应用进程与内核交互,目的未达到之前,不再一味的等着,而是直接返回。然后通过轮询的方式,不停的去问内核数据准备有没有准备好。如果某一次轮询发现数据已经准备好了,那就把数据拷贝到用户空间中。

应用进程通过 recvfrom 调用不停的去和内核交互,直到内核准备好数据。如果没有准备好,内核会返回error,应用进程在得到error后,过一段时间再发送recvfrom请求。在两次发送请求的时间段,进程可以先做别的事情。

缺点: 还是得循环监看是否存在数据。

用户进程发起请求,内核发现没有数据立即返回结果,这时用户进程不再阻塞等待;不过用户进程会循环询问是否有数据,当内核返回存在数据时,这时用户进程还是得阻塞,等待数据拷贝。

信号驱动I/O模型

应用进程在读取文件时通知内核,如果某个 socket 的某个事件发生时,请向我发一个信号。在收到信号后,信号对应的处理函数会进行后续处理。

应用进程预先向内核注册一个信号处理函数,然后用户进程返回,并且不阻塞,当内核数据准备就绪时会发送一个信号给进程,用户进程便在信号处理函数中开始把数据拷贝的用户空间中。

信号驱动是用户进程调用时告诉内核,如果有数据就发信号给我,然后同步阻塞等待数据拷贝,但是这里有一个重点就是 等待信号是异步的 ,最终数据拷贝仍然是同步。

I/O复用模型

多个进程的IO可以注册到同一个管道上,这个管道会统一和内核进行交互。当管道中的某一个请求需要的数据准备好之后,进程再把对应的数据拷贝到用户空间中。

IO多路转接是多了一个select函数,多个进程的IO可以注册到同一个select上,当用户进程调用该select,select会监听所有注册好的IO,如果所有被监听的IO需要的数据都没有准备好时,select调用进程会阻塞。当任意一个IO所需的数据准备好之后,select调用就会返回,然后进程在通过recvfrom来进行数据拷贝。

这里的IO复用模型,并没有向内核注册信号处理函数,所以,他并不是非阻塞的。进程在发出select后,要等到select监听的所有IO操作中至少有一个需要的数据准备好,才会有返回,并且也需要再次发送请求去进行文件的拷贝。

用户进程下开启一个线程用来监听所注册的socket,如果所监听的多个socket,有几个发生数据流能立即监听到,然后数据同步拷贝,并且I/O复用有三种模式可选择:select、poll、epoll。

概述 select poll epoll
操作方式 遍历 遍历 回调
底层实现 数组 链表 哈希表
IO效率 每次调用都进行线性遍历,时间复杂度为O(n) 每次调用都进行线性遍历,时间复杂度为O(n) 事件通知方式,每当fd就绪,系统注册的回调函数就会被调用,将就绪fd放到readyList里面,时间复杂度O(1)
最大连接数 1024(x86)或2048(x64) 无上限 无上限

三种IO复用比较

异步I/O模型

异步I/O

应用进程把IO请求传给内核后,完全由内核去操作文件拷贝。内核完成相关操作后,会发信号告诉应用进程本次IO已经完成。

用户进程发起aio_read操作之后,给内核传递描述符、缓冲区指针、缓冲区大小等,告诉内核当整个操作完成时,如何通知进程,然后就立刻去做其他事情了。当内核收到aio_read后,会立刻返回,然后内核开始等待数据准备,数据准备好以后,直接把数据拷贝到用户控件,然后再通知进程本次IO已经完成。

下图是几种常见I/O模型的对比:

mark
借鉴微信文章:https://mp.weixin.qq.com/s?__biz=Mzg3MjA4MTExMw==&mid=2247484746&idx=1&sn=c0a7f9129d780786cabfcac0a8aa6bb7&source=41#wechat_redirect

学习美团文章:https://www.zhihu.com/search?type=content&q=NIO

拓展知秋文章:https://juejin.im/post/5c2cc075f265da611037298e