# I/O 模型
Unix 包含物种 IO
模型,分别是:阻塞 IO
、非阻塞 IO
、 IO
多路复用、信号驱动 IO
、异步 IO
。和 Java 的 IO
模型有下列对应(不严格)
Java-IO 模型 | Unix-IO 模型 |
---|---|
BIO | 阻塞式 IO |
NIO | IO 多路复用 |
AIO | 异步 IO |
# 内核态与用户态
我们将文件从磁盘加载到内存中。操作系统是怎么做的?
- 进程陷入内核态,通过系统调用执行文件阅读。
- 系统调用结束后,返回用户态。
所以 Unix 的五种 IO 模型的不同指出,就是这两个步骤的处理流程不同。
详细可以参考这篇文章:从根上理解用户态与内核态。
# 阻塞式 I/O
应用进程被阻塞,知道数据复制到应用进程缓冲区才返回。应该注意到,在阻塞的过程中,其它程序还可以执行,因此阻塞不意味着整个操作系统都被阻塞。因为其他程序还可以执行,因此不消耗 CPU
时间,这种模型的执行效率会比较高。
# 非阻塞式 I/O
应用进程执行系统调用之后,内核返回一个错误码。应用进程可以继续执行,但是需要不断的执行系统调用来获知 I/O 是否完成,这种方式称为轮询 ( polling
)。
# 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/O
的 CPU
利用率更高。
缺点:信号 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 详解