前面总结了很多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操作发生时,会经历两个阶段:
- 数据准备阶段(Waiting for the data to be ready);
- 将数据从内核复制到进程中(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详解
原文地址:https://www.cnblogs.com/volcano-liu/p/11001746.html