几个基本概念
阻塞:调用结果返回之前,当前线程会被挂起进入非可执行状态,cpu不会给线程分配时间片,该进程只有在得到结果之后才会返回。
非阻塞:在不能得到结果之前,当前线程不会被阻塞而是立即返回。

同步:调用者在发起一个功能调用时,在没得到结果之前该调用不会返回。
异步:调用者在发起一个功能调用后不能立即得到结果,当这个调用被处理完成后,会通过状态、通知和回调来通知调用者。

在JDK中,BIO出现在1.4之前,NIO是1.4之后出现,AIO是1.7出现。另外IO多路复用可以通过NIO实现,这里的NIO并非指下方里的‘非阻塞式IO(No_Blocking IO)。

linux操作系统中分了内核空间用户空间,所有的IO操作都得获得内核的支持,用户态的进程无法直接进行内核的IO操作,内核空间提供了系统调用,使得用户态的进程可以间接执行IO操作。

再进行五种模型介绍前,我们来先看下一次网络请求中服务端做了哪些操作以及IO的两个阶段。
一次网络请求服务端的操作
image.png
每个客户端会与服务端建立一次socket连接,服务端获取连接后,对于所有的数据的读写都需要经过操作系统的内核,通过系统调用内核将数据复制到用户进程的缓冲区,完成与客户端的交互,根据系统调用的方式分为阻塞与非阻塞,根据系统处理应用进程的方式分为同步与异步。

IO的两个阶段
IO的读写一般都要经过内核态和用户态的切换,以read为例,对于一次IO访问,会经历数据等待与数据拷贝两个阶段:

  1. 系统内核在等待系统设备(网卡)接收到数据后,把数据写到内核中。
  2. 系统内核在获取到数据后,将数据拷贝到用户进程的空间中。

从IO的两个阶段可以看出,数据需要从设备拷贝到内核缓冲区,再从内核缓冲区拷贝到应用进程

介绍完这些咱们来开始看下五种IO的具体情况。

阻塞式IO
image.png
每一次客户端产生的socket连接实际上是一个文件描述符(file descriptor),而每一个用户进程读取实际上了是一个文件描述符,这个时候的系统调用函数会等待网络请求数据的到达,和数据从内核空间复制到用户进程空间,也就是是,第一阶段的IO调用与第二阶段的IO执行都会阻塞,对于多个客户端连接,只能开辟多个线程来处理。(换一个角度去定义:当用户线程发出IO请求之后,内核会去查看数据是否就绪,如果没有就绪就会等待数据就绪,而用户线程就会处于阻塞状态,用户线程交出CPU。当数据就绪之后,内核会将数据拷贝到用户线程,并返回结果给用户线程,用户线程才解除block状态。参考:阻塞IO与非阻塞IO

非阻塞式IO(No_Blocking IO)
image.png
为解决阻塞IO模型的阻塞问题,系统的内核进行了改进,在内核中socket支持了非阻塞状态,当用户线程发起一个read操作后,并不需要等待,而是马上就得到了一个结果。如果结果是一个error时,它就知道数据还没有准备好,于是它可以再次发送read操作。一旦内核中的数据准备好了,并且又再次收到了用户线程的请求,那么它马上就将数据拷贝到了用户线程,然后返回。需要注意的是非阻塞IO就有一个非常严重的问题,用户线程会一直占有CPU,在while循环中需要不断地去询问内核数据是否就绪,这样会导致CPU占用率非常高。

IO多路复用
java里的selector,linux里的epoll就是采用这种模式。多路指网络连接,复用指同一个线程。可以通过NIO实现IO多路复用,channel与selector就是实现的基础,这里的channle好比linux里的文件描述符。在非阻塞IO模型中,虽然解决了IO调用阻塞的问题,但是产生了新的问题,如果现在有1万个连接,那用户线程会调用1万次系统调用read来进行处理,在用户空间这种开销太大,解决的思路就是让用户减少系统调用,但是用户自己实现不了,所以就导致了内核发生了进一步变。在内核空间中帮助用户进程遍历所有的文件描述符,将数据准备好的文件描述符返回给用户进程,该方式是阻塞IO,因为在第一阶段的IO调用会阻塞进程。

下面是select与poll示意图:
image.png
下面是epoll示意图:
image.png
对于epoll来说在第一阶段的epoll_wait依然是阻塞的,所以也是同步阻塞IO。
select/poll/epoll详细的信息可见四种网络模型里有关IO多路复用的说明

信号驱动式IO
在IO执行的数据准备阶段,不会阻塞用户进程,当用户进程需要等待数据的时候,会向内核发送一个信号,告诉内核需要数据,然后用户进程就继续做别的事了,而当内核中的数据准备好之后,内核会给用户进程发一个信号,用户进程收到信号后立马调用recvfrom去查收数据,该IO模型使用的较少。
image.png

异步IO(AIO)
应用进程通过aio_read告知内核启动某个操作,且在整个操作完成之后再通知应用进程,包括将数据从内核空间拷贝到用户空间。信号驱动IO是内核通知我们何时可以启动一个IO操作,而异步IO模型是由内核通知我们IO操作何时完成,是真正意义上的无阻塞IO操作。
image.png

总结
前四种模型的主要区别在于第一阶段,因为它们的第二阶段都是一样的:在数据从内核拷贝到应用进程的缓冲区期间进程都会阻塞。相反,异步IO模型在这两阶段都不会阻塞。
image.png

最后再提一下直接内存
直接内存并不是虚拟机运行时数据区的一部分,也不是java虚拟机规范中定义的内存区域。直接内存申请空间耗费更高的性能,直接内存IO读写的性能要优于普通的堆内存,对于java程序来说,系统内核读取堆类的对象需要根据代码段计算其偏移量来获取对象地址,效率较慢,不太适合网络IO的场景,对于直接内存来说更加适合IO操作,内核读取存放在直接内存中的对象较为方便,因为其地址就是裸露的进程虚拟地址,不需要jvm翻译。那么就可以使用mmap开辟一块直接内存mapbuffer和内核空间共享,并且该直接内存可以直接映射到磁盘上的文件,这样就可以通过调用本地的put而不用调用系统调用write就可以将数据直接写入磁盘,RandomAccessFile类就是通过开辟mapbuffer实现的读写磁盘。

参考的文章:
从5种网络IO模型到零拷贝
IO模型和基于事件驱动的IO多路复用模式


步履不停
38 声望13 粉丝

好走的都是下坡路