【java】Java I/O 模型

描述 java I/O 模型

操作系统 IO 相关概念

内核态 / 用户态

为了限制不同的程序之间的访问能力,防止他们获取别的程序的内存数据,或者随意访问外围设备,CPU 为指令划分了访问等级。而在操作系统中,这将分为内核态和用户态两个等级

  • 内核态:CPU可以访问内存所有数据,包括外围设备,例如硬盘,网卡。CPU 也可以将自己从一个程序切换到另一个程序
  • 用户态:只能受限的访问内存,且不允许访问外围设备。占用CPU的能力被剥夺,CPU 可以被抢占

程序从内核态转换为用户态,或从用户态转换为内核态都需要一定的 CPU 资源

用户空间、内核空间

同内核态 / 用户态一样,内存虚拟地址空间也被分为两部分,一部分由内核使用,一部分由用户进程使用。

IO 模型

读操作分为内核准备数据和将数据从内核拷贝到用户空间两个阶段,如图

用户态与核心态

同样,写操作也分为从用户空间拷贝数据到内核,再从内核写数据到设备两个阶段

Linux 系统 I/O 模型

关于同步,阻塞的解释

同步和异步:描述的是用户线程与内核的交互方式

  • 同步是指用户线程发起 IO 请求后需要等待或者轮询内核 IO 操作完成后才能继续执行
  • 异步是指用户线程发起 IO 请求后仍继续执行自身指令(不等待),当内核 IO 操作完成后会通知用户线程,或者调用用户线程注册的回调函数

阻塞和非阻塞:描述的是用户线程调用内核 IO 操作的方式

  • 阻塞是指 IO 操作需要彻底完成后才返回到用户空间
  • 而非阻塞是指 IO 操作被调用后立即返回给用户一个状态值,无需等到 IO 操作彻底完成

一个 IO 操作其实分成了两个步骤:发起 IO 请求和实际的 IO 操作

  • 同步 IO 和异步 IO 的区别就在于第二个步骤是否阻塞,如果实际的 IO 读写阻塞请求进程,那么就是同步 IO
  • 阻塞 IO 和非阻塞 IO 的区别在于第一步,发起 IO 请求是否会被阻塞,如果阻塞直到完成那么就是传统的阻塞 IO,如果不阻塞那么就是非阻塞 IO

Linux 的几种 I/O 模型

  1. 同步阻塞IO(Blocking IO)
  2. 同步非阻塞IO(Non-blocking IO)
  3. IO多路复用(IO Multiplexing)
  4. 异步IO
  5. 信号驱动式IO(很少用,不解释)

同步阻塞 IO

最简单的 IO 模型,用户线程在读写时被阻塞

应用程序为了执行读写操作,会调用相应的一个系统调用,将系统控制权交给内核(用户态到内核态切换),然后就进行等待(就是被阻塞)。用户线程这时不能做任何事情。内核开始执行这个系统调用,执行完毕后复制数据到用户态内存,向用户线程返回响应。用户线程得到响应后,就不再阻塞,并进行后面的工作。

同步非阻塞

在同步阻塞的基础上,用户线程读写时,不被阻塞,而是立即返回,之后用户线程不断发起 IO 请求。

数据未到达时系统返回一状态值,数据到达后才真正读取数据,为了拿到数据,需不断轮询,无谓地消耗了大量的 CPU,一般很少直接使用这种模型,而是在其他 IO 模型中使用非阻塞 IO 这一特性

IO多路复用

多路复用有一个选择器(Selector),将需要处理的 Channel (其实就是 socket)注册到选择器上。然后轮询 Selector(调用 select 并阻塞线程)。当有 Channel 准备完成,可以进行操作(读,写,连接)时,select 会返回。这时用户线程需要迭代所有 SelectionKey(对应 Channel 的网络事件) 并做相应的业务处理。

另一种多路复用是 epoll,将 socket 注册到 epoll(由 epoll_create 创建),将注册需要处理的网络事件。然后轮询(调用 epoll_wait,阻塞线程)。当可以进行操作时,epoll_wait 返回,且返回所有准备好的网络事件,用户线程只需要处理已经准备好的网络事件

当数十万并发连接存在时,epoll 和 select 的区别比较明显。在这种情况下,可能每一毫秒只有数百个活跃的连接,同时其余数十万连接在这一毫秒是非活跃的,Selector 将会浪费大量时间在查询哪些非活跃的连接上。而 epoll 可以节省时间。

另外,每次调用 select,需要把 fd 集合从用户态拷贝到内核态,而 select 支持的文件描述符数量太小从单个连接的处理流程看,多路复用有点像同步阻塞 IO,但多路复用使得用户可以在一个线程内同时处理多个 socket 的 IO 请求。

异步IO

调用发出后,系统立刻返回,实际处理这个调用的函数在完成后,通过状态、通知和回调来通知调用者的输入输出操作。异步IO的工作机制是:告知内核启动某个操作,并让内核在整个操作完成后通知

Java I/O 模型

jdk 提供了几种 I/O 模型

  1. bio,采用同步阻塞模型,JDK 1.4开始支持
  2. nio,采用多路复用模型,JDK 1.4开始支持
  3. aio,采用异步 IO 模型,JDK 1.7开始支持

Reactor 模式

reactor 模式建立在多路复用的基础上,这个模式有两个主要角色:事件分离器(dispatcher),事件处理器(handler)

  • 事件分离器:处理读写,连接等行为,通过由线程池实现
  • 事件处理器:同一个线程实现,实现事件循环,不断调用多路分离函数 select (或 epoll_wait)。当有某个事件被激活时,调用关联的事件处理器处理

reactor 处理流程如下

reactor-model

Proactor 模式

proactor 模式建立在异步 IO 上。

用户线程将 AsynchronousOperation(读/写等)、Proactor 以及操作完成时的 CompletionHandler 注册到 AsynchronousOperationProcessor。

当用户线程调用异步 API 后,用户线程继续执行自己的任务,AsynchronousOperationProcessor 会开启独立的内核线程执行异步操作,实现真正的异步读写。当异步 IO 操作完成时,将调用 CompletionHandler

处理流程如下:

proactor-model