# I/O 模型

Unix 包含物种 IO 模型,分别是:阻塞 IO 、非阻塞 IOIO 多路复用、信号驱动 IO 、异步 IO 。和 Java 的 IO 模型有下列对应(不严格)

Java-IO 模型Unix-IO 模型
BIO阻塞式 IO
NIOIO 多路复用
AIO异步 IO

# 内核态与用户态

我们将文件从磁盘加载到内存中。操作系统是怎么做的?

  • 进程陷入内核态,通过系统调用执行文件阅读。
  • 系统调用结束后,返回用户态。
    所以 Unix 的五种 IO 模型的不同指出,就是这两个步骤的处理流程不同。

内核态与用户态

详细可以参考这篇文章:从根上理解用户态与内核态

# 阻塞式 I/O

应用进程被阻塞,知道数据复制到应用进程缓冲区才返回。应该注意到,在阻塞的过程中,其它程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,因此不消耗 CPU 时间,这种模型的执行效率会比较高。

阻塞式I/O

# 非阻塞式 I/O

应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询 ( polling )。

非阻塞式IO模型

# I/O 多路复用

I/O 多路复用是指用一个线程检查多个文件描述符(Socket)的状态。

类似与非阻塞,只不过轮询不是由用户线程去执行,而是由内核去轮询,内核监听程序监听到数据准备好后,调用内核函数复制数据到用户态。 select 系统调用(poll,充当代理类的角色,不断轮询注册到它这里的所有需要 IO 的文件描述符,有结果时,把结果告诉被代理的 recvfrom 函数,再去拿数据。

一个线程可以对多个 IO 端口进行监听,当 socket 有读写事件时分发到具体的线程进行处理。模型如下所示:

多路复用

我们着重讲解一下多路复用,这在之后讲解 Reactor 模型会用到。传统的 I/O 有个很大的缺点就是,为每一个客户端连接都准备一个线程,如果客户端是长连接(如果客户端还不发送数据,服务端线程会一直阻塞在 read 那里),线程也就要一直维护。随着这样的客户端连接越来越多,就会导致服务器资源耗尽。一个客户端会有很多种状态:读写,解码,编码,连接等,我们通过 Selector 选择器轮询这些客户端,只有客户端在对应的状态才会创建线程进行处理(比如真正开始读写才会创建线程执行,当然,这些由线程池维护)。

关于复用的几个系统调用(看不懂就算了):

# select

select 函数监视的文件描述符分 3 类,分别是 writefds、readfds、和 exceptfds。调用后 select 函数会阻塞,直到有描述符就绪(有数据 可读、可写、或者有 except),或者超时(timeout 指定等待时间,如果立即返回设为 null 即可),函数返回。当 select 函数返回后,可以通过遍历 fdset,来找到就绪的描述符。

# poll

poll 本质上和 select 没有区别,它将用户传入的数组拷贝到内核空间,然后查询每个 fd 对应的设备状态,如果设备就绪则在设备等待队列中加入一项并继续遍历,如果遍历完所有 fd 后没有发现就绪设备,则挂起当前进程,直到设备就绪或者主动超时,被唤醒后它又要再次遍历 fd。这个过程经历了多次无谓的遍历。

# epoll

epoll 支持水平触发和边缘触发,最大的特点在于边缘触发,它只告诉进程哪些 fd 刚刚变为就绪态,并且只会通知一次。还有一个特点是,epoll 使用 “事件” 的就绪通知方式,通过 epoll_ctl 注册 fd,一旦该 fd 就绪,内核就会采用类似 callback 的回调机制来激活该 fd,epoll_wait 便可以收到通知。

# 信号驱动 I/O

应用进程使用 sigaction 系统调用,内核立即返回,应用进程可以继续执行,也就是说等待数据阶段应用进程是非阻塞的。内核在数据到达时向应用进程发送 SIGIO 信号,应用进程收到之后在信号处理程序中调用 recvfrom 将数据从内核复制到应用进程中。

相比于非阻塞式 I/O 的轮询方式,信号驱动 I/OCPU 利用率更高。

缺点:信号 I/O 在大量 IO 操作时可能会因为信号队列溢出导致没法通知。

# 异步 I/O

在上面四种 I/O 模型中我们看到,进程在调用 recvfrom 到处理数据报时都会被阻塞,而异步 I/O 做到了真正的非阻塞。异步 I/O 也是依靠信号通知,但是通知的时候并不是数据报准备好了,而是 I/O 操作已经完成。

主进程只负责做自己的事情,等 IO 操作完成 (数据成功从内核缓存区复制到应用程序缓冲区) 时通过回调函数对数据进行处理。

相对于同步 IO ,异步 IO 不是顺序执行。用户进程进行 aio_read 系统调用之后,无论内核数据是否准备好,都会直接返回给用户进程,然后用户态进程可以去做别的事情。等到 socket 数据准备好了,内核直接复制数据给进程,然后从内核向进程发送通知。 IO 两个阶段,进程都是非阻塞的。

要实现真正的异步 I/O ,操作系统需要做大量的工作。目前 Windows 下通过 IOCP 实现了真正的异步 I/O

# 参考

Java 全栈知识体系:https://pdai.tech/md/java/io/java-io-model.html

Stevens W R, Fenner B, Rudoff A M. UNIX network programming[M]. Addison-Wesley Professional, 2004.

Boost application performance using asynchronous I/O (opens new window)

Synchronous and Asynchronous I/O (opens new window)

Linux IO 模式及 select、poll、epoll 详解 (opens new window)

poll vs select vs event-based (opens new window)

select / poll / epoll: practical difference for system architects (opens new window)

unix 网络编程 第一卷

五种 IO 模型介绍和对比

一文读懂高性能网络编程中的 I/O 模型

聊聊 IO 多路复用之 select、poll、epoll 详解