UNIX 系统下的 I/O 模型有 5 种:同步阻塞 I/O、同步非阻塞 I/O、I/O 多路复用、信号驱动 I/O 和异步 I/O。
什么是 I/O?
I/O 就是计算机内存与外部设备之间拷贝数据的过程。
为什么需要 I/O 模型?
CPU 的访问内存的速度远远高于外部设备,因此 CPU 是把外部设备的数据读取到内存中,然后再进行处理。当我们的程序通过 CPU 向外部设备发送一个 read 指令时,数据从外部设备拷贝到内存中需要一段时间,这个时候 CPU 没事干了,你的程序是主动把 CPU 让给别人?还是让 CPU 不停的查数据到了吗,数据到了吗....,这就是 IO 模型要解决的问题。

Unix 5 种 I/O 模型
一个进程的地址空间分为用户空间和内核空间,用户线程不能直接访问内核空间。所以当用户线程发起 I/O 操作后,网络数据读取操作会经历两个步骤:

1. 用户线程等待内核将数据从网卡拷贝到内核空间。
2. 内核将数据从内核空间拷贝到用户空间。
各种 I/O 模型的区别就是:它们实现这两个步骤的区别不一样。

阻塞和非阻塞 主要是针对第一个阶段,是指应用发起 I/O 操作时,是立即返回还是等待。立即返回就是非阻塞,反之阻塞。
同步非同步 主要是针对第二阶段。在用户空间和内核空间通信的过程中,数据从内核空间到应用空间的拷贝,是由内核主动发起还是由应用程序来触发。由内核主动触发的是非异步,反之是同步。其实可以发现,由用户线程主动发起的,在拷贝数据过程是线程还是阻塞的,而异步的时候,内核程序会将数据拷贝到用户空间指定的 buffer 中,然后再调用注册好的函数进行处理,这段时间,用户线程是没有阻塞的。

  1. 同步阻塞 I/O
    用户线程发起 read 调用后就阻塞了,让出 CPU。内核等待网卡数据到来,把数据从网卡拷贝到用户内核空间,接着把数据拷贝到用户空间,再把用户线程唤醒。
    null
  2. 同步非阻塞 I/O
    用户线程通过轮询(polling)的方式不断发起 read 操作,与阻塞 I/O 不一样的是,非阻塞将大的整片时间的阻塞分成 N 多的小的阻塞, 所以进程不断地有机会 '被' CPU 光顾。数据没有到达内核空间时,每次都返回失败 error,直到数据到了内核空间,这一次 read 调用后,在等待数据从内核空间拷贝到用户空间这段时间里,线程还是阻塞的,等数据到了用户空间再把线程唤醒。
    null
  3. I/O 多路复用
    用户线程的读取操作分成两步了,线程先发起 select 调用,目的是问内核数据准备好了吗?等内核把数据准备好了,用户线程再发起 read 调用。I/O 复用模型会用到 select、poll、epoll 函数,这几个函数也会使进程阻塞,但是和阻塞 I/O 所不同的的,这几个函数可以同时阻塞多个 I/O 操作。而且可以同时对多个读操作,多个写操作的 I/O 函数进行检测,直到有数据可读或可写时(不是等到 socket 数据全部到达再处理, 而是有了一部分数据就会调用用户进程来处理),才真正调用 I/O 操作函数。
    那为什么叫 I/O 多路复用呢?因为一次 select 调用可以向内核查多个数据通道(Channel)的状态,所以叫多路复用。因为有在 select 上阻塞,所以 IO 多路复用可以归为同步阻塞模式。
    null
    select、poll、epoll 的不同
    null
  4. 异步 I/O
    用户线程发起 read 调用的同时注册一个回调函数,read 立即返回,等内核将数据准备好后,再调用指定的回调函数完成处理。在这个过程中,用户线程一直没有阻塞。
    null
  5. 信号驱动 使用的比较少,在此不做记录。

针对这 5 中 IO 模型,采用一张图来总结一下:
image.png

Java I/O

Unix中的五种I/O模型,除信号驱动I/O外,Java对其它四种I/O模型都有所支持。其中Java最早提供的blocking I/O即是同步阻塞I/O,而NIO即是同步非阻塞I/O,同时通过NIO实现的Reactor模式即是I/O复用模型的实现,通过AIO实现的Proactor模式即是异步I/O模型的实现。
但是通过Reactor模式实现的NIO,和unix中的I/O多路复用是相同的概念,但这是一种编程模型,而不是原生支持。