聊聊五种 I/O 模型
什么是 I/O
I/O 是 Input/Output 的简写,即输入/输出,是计算机与外部设备(键盘、鼠标、磁盘等)通信的统称,与具体实现无关。
与外部设备的通信其实就是对外部设备进行读取或写入数据的过程,比如对文件的读写操作可以称为文件 I/O、对套接字的读写操作称为网络 I/O。
转载请注明来源地址:https://her-cat.com/posts/2021/05/12/talking-about-five-io-models/
同步 I/O 和异步 I/O
同步和异步是相对于获取数据的过程而言的。
- 同步 I/O:包含阻塞 I/O、非阻塞 I/O、I/O 多路复用、信号驱动式I/O 四种 I/O 模型,它们的共同点就是在执行 I/O 操作时会阻塞进程,直到 I/O 操作完成。
- 异步 I/O:执行 I/O 操作时不会阻塞进程。
在用户进程中执行 read 系统调用时,内核将数据从内核空间拷贝用户进程空间。如果没有读到数据,那么进程会一直处于阻塞状态,当读取到数据时才会恢复进程,继续执行后面的逻辑,所以我们称这个操作是同步的。
异步只需要执行 aio_read 系统调用告诉内核从哪儿读取数据就可以了,进程不用等待立即返回,内核会异步地将数据从内核空间拷贝到用户进程空间。
阻塞 I/O 模型
套接字在创建时默认就是阻塞的。
用户进程执行 read 系统调用,如果数据没有准备好,那么进程将会被挂起,直到数据准备好之后内核将数据拷贝给用户进程。
举一个烧开水的例子,水壶是套接字,水壶里的水是数据,水未烧开说明数据没有准备好。
现在有 A、B、C 三个水壶,进程查询 A 的状态,发现水还没有烧开就一直等着水烧开,哪怕 B、C 的水烧开了也不管,直到 A 的水烧开了才会去处理下一个水壶。
缺点:每次只能对一个套接字进行操作,就算其它套接字数据准备好了也没办法立即处理。
非阻塞 I/O 模型
在 PHP 中可以调用 socket_set_nonblock 函数将套接字设置为非阻塞的。
非阻塞 I/O 在数据未准备好的情况下,执行 read 系统调用将会立即返回,应用程序可以使用循环不停地轮训内核,直到数据准备好,内核将数据从拷贝到应用程序中。
伪代码:
while (true) {
rbytes = read(fd);
if (rbytes < 0 && errno == EWOULDBLOCK) {
continue;
}
// 处理数据
}
用烧开水的例子来解释就是,进程不断地轮询每个水壶的状态,当某个水壶的水烧开了之后就执行下一步操作。
缺点:需要不停地轮询内核,浪费系统资源。
I/O 多路复用模型
多路复用就是多个网络请求复用同一个进程,让单进程的应用程序拥有了同时处理多个套接字的能力,避免不停地轮询内核,造成资源浪费。
将需要监听的套接字交给内核,然后进程被挂起陷入休眠状态,当套接字数据准备好时,内核将对应的套接字及事件返回给进程并唤醒进程,进程就可以执行 read 系统调用读取数据,这个时候数据肯定是准备好的。
目前常见的有三种多路复用机制,分别是 select、poll、epoll。
多路复用机制 | 平台支持 | 底层实现 | 时间复杂度 | 最大连接数 | fd 拷贝 |
---|---|---|---|---|---|
select | Linux/Windows | 数组 | O(n) | 1024 | 每次调用 select 都需要从用户进程拷贝到内核 |
poll | Linux | 链表 | O(n) | 无上限 | 每次调用 poll 都需要从用户进程拷贝到内核 |
epoll | Linux | 红黑树 | O(1) | 无上限 | 调用 epoll_ctl 时需要从用户进程拷贝到内核,epoll_wait 不需要 |
从上表可以看出 epoll 性能最好,所以当程序运行在 Linux 系统上应该使用 epoll,如果是 Windows 可以使用 select,这样我们就可以实现跨平台的多路复用应用程序。
烧开水的例子:将 A、B、C 三个水壶交给内核,当有水烧开时内核就通知进程哪个水壶烧开了。
I/O 多路复用中的套接字必须设置为非阻塞的。
信号驱动式 I/O 模型
调用 sigaction 安装 SIGIO 信号处理器,为套接字设置宿主进程,当套接字的数据准备好时,操作系统会触发 SIGIO I/O 就绪信号,就会执行安装的信号处理器,在信号处理器中执行 I/O 操作。
进程会安装 SIGIO 信号的处理函数,让内核有数据准备好时就触发 SIGIO 信号,并执行该信号对应的处理函数。
用水壶的例子来解释就是给每个水壶都安装了一个蜂鸣器,当水烧开时就开始响,进程就知道哪个水壶烧开了。
异步 I/O 模型
前面这四种 I/O 模型都是同步 I/O,不管是进程是如何知道数据是否准备好的,最终执行 read 系统调用从内核拷贝数据到用户进程的过程是同步的,而异步 I/O 的区别就在于这里。
异步 I/O 的读写操作都是立即返回的,读写操作由内核异步地执行,数据拷贝的过程不会阻塞用户进程。
相当于告诉内核,水烧开了就倒在这个杯子里,我想喝水了就自己去喝。
总结
同步 I/O 和异步 I/O 的区别:内核将数据从内核空间拷贝到用户进程空间时,是否会阻塞用户进程。
阻塞 I/O 和非阻塞 I/O 的区别:数据没有准备好的时候是否会阻塞用户进程。