- 阻塞式I/O
- 非阻塞式I/O
- I/O复用式
- 信号驱动式IO
- 异步I/O
在unix操作系统中,一个IO操作主要经过两个阶段:1、等待数据准备;2、将数据从内核空间拷贝到用户进程中。示意图如下:
![](/其他/UNIX五种IO模型/UNIX中IO示意图.png)
- 用户空间是常规进程所在区域。 JVM 就是常规进程,驻守于用户空间。用户空间是非特权区域:比如,在该区域执行的代码就不能直接访问硬件设备。
内核空间是操作系统所在区域。内核代码有特别的权力:它能与设备控制器通讯,控制着用户区域进程的运行状态,等等。最重要的是,所有 I/O 都直接(如这里所述)或间接通过内核空间。
整个过程大致就应该是:
- 当用户进程请求I/O操作(包括文件操作、socket操作等)的时候,该用户进程会执行一个系统调用,将本进程的控制权移交给内核。
- 当内核以上述方式被调用,它就会采取一些必要步骤,找到进程所需数据,并把数据传送到用户空间内的指定缓冲区。
- 内核试图对数据进行高速缓存或预读取,如果进程所需的数据已经在内核空间里了,那么只需要简单地将数据拷贝出来即可;如果进程所需的数据不在内核空间,则进程会被挂起,内核将着手把数据读进内存。
1、阻塞式IO
在linux中,默认情况下所有的socket都是阻塞式的,一个典型的读操作流程大致是这样的:
- 等待数据从网络中到达,当所有等待的数据到达时,它被复制到内核中的某个缓冲区;
- 把数据从内核缓冲区复制到应用程序缓冲区
![](/其他/UNIX五种IO模型/1阻塞IO模型.png)
当用户进程调用了recvfrom这个系统调用,内核就开始了IO的第一个阶段:准备数据。对于网络IO来说,很多时候数据在一开始还没到达,这个时候内核就要等待足够的数据到达,那么在用户进程这边,整个进程就会被阻塞。当内核数据准备好了,recvfrom系统调用就会从内核中拷贝数据到用户内存,然后内核返回结果,用户进程最终才会解除阻塞状态,重新向下执行。
因此,阻塞式IO在 等待数据准备就绪与数据从内核拷贝到用户空间这两个过程都被阻塞了
2、非阻塞式IO
非阻塞模式IO的流程图大致是:
![](/其他/UNIX五种IO模型/2非阻塞IO模型.png)
当用户进程发出read操作时,如果内核中的数据还没有准备好,他不会阻塞用户进程,而是立刻返回一个错误信息,对于用户进程而言,它发起一个read操作后,并不需要等待,而是立马可以得到一个结果,如果结果是错误,它可以做其他的事情,等一段时间后再次发送read操作,一直往复,直到内核中的数据准备好为止,当内核数据准备好了并且用户再次发送了read请求后,内核将会把数据从内核空间拷贝到用户空间,最后返回,需要注意的是:在内核拷贝数据到用户空间过程是阻塞的,用户进程只能将数据拷贝完了才能做其他事情。
因此,此模式第一阶段是不阻塞的,第二阶段依然是阻塞的
3、多路复用式IO
多路复用式IO主要是针对非阻塞IO的改进版,多路复用式IO中有两个非常出名的技术:select、epoll,select/poll的好处就在于单个进程就可以同时处理多个网络连接的IO。
IO复用同非阻塞IO本质一样,不过利用了新的select系统调用,由内核来负责本来是请求进程该做的轮询操作。看似比非阻塞IO还多了一个系统调用开销,不过因为可以支持多路IO,才算提高了效率。
它的基本原理就是select /epoll这个function会不断的轮询所负责的所有socket,当某个socket有数据到达了,就通知用户进程。它的流程如图:
![](/其他/UNIX五种IO模型/3IO复用模型.png)
当用户进程调用了select
,那么整个进程会被阻塞,而同时,内核会“监视”所有select负责的socket,当任何一个 socket中的数据准备好了,select就会返回。这个时候用户进程再调用read操作,将数据从内核拷贝到用户进程。
这个图和阻塞式IO的图其实并没有太大的不同,事实上,还更差一些。因为这里需要使用两个system call (select 和 recvfrom),而阻塞式IO只调用了一个system call (recvfrom)。但是,用select的优势在于它可以同时处理多个connection。(多说一句。所以,如果处理的连接数不是很高的话,使用 select/epoll的服务器不一定比使用“多线程 + 阻塞式IO”的服务器性能更好,可能延迟还更大。
select/epoll的优势并不是对于单个连接能处理得更快,而是在于能处理更多的连接。
在多路复用式IO中,实际中,对于每一个socket,一般都设置成为non-blocking,但是,如上图所示,整个用户的进程其实是一直被阻塞的。只不过进程是被select这个函数阻塞,而不是被socket IO给阻塞。
4、信号驱动式IO
用户进程执行一个信号处理函数,然后立即返回,并继续原来的工作,当内核数据准备好时,内核就会为用户进程生成一个SIGIO信号,通过信号回调通知应用程序调用recvfrom来读取数据。示意图如下:
![](/其他/UNIX五种IO模型/4信号驱动IO模型.png)
5、异步IO
这类函数的工作机制是告知内核启动某个操作,并让内核在整个操作(包括将数据从内核拷贝到用户空间)完成后通知我们。如图:
![](/其他/UNIX五种IO模型/5异步IO模型.png)
用户进程发起read操作之后,立刻就可以开始去做其它的事。而另一方面,从内核的角度,当它受到一个asynchronous read之后,首先它会立刻返回,所以不会对用户进程产生任何阻塞。然后,内核会等待数据准备完成,然后将数据拷贝到用户内存,当这一切都完成之后,内核会给用户进程发送一个信号,告诉它read操作完成了。 在这整个过程中,进程完全没有被阻塞。
【异步IO与信号驱动式IO的区别在于:信号驱动IO在收到内核信号后,依旧需要调用recvfrom来阻塞式的读取数据,而异步IO在收到内核信号后,数据已经被内核拷贝到了用户空间】
总结
其实前四种I/O模型都是同步I/O操作,他们的区别在于第一阶段,而他们的第二阶段是一样的:在数据从内核复制到应用缓冲区期间(用户空间),进程阻塞于recvfrom调用。
有人可能会说,non-blocking IO并没有被阻塞啊。这里有个非常“狡猾”的地方,定义中所指的”IO operation”是指真实的IO操作,就是例子中的recvfrom这个系统调用。non-blocking IO在执行recvfrom这个系统调用的时候,如果内核的数据没有准备好,这时候不会阻塞进程。但是,当内核中数据准备好的时候,recvfrom会将数据从 内核拷贝到用户内存中,这个时候进程是被阻塞了,在这段时间内,进程是被阻塞的。