五种IO模型与非阻塞IO

目录

  • [1. 五种IO概念](#1. 五种IO概念)
    • [1.0 同步与异步 I/O 的核心界定](#1.0 同步与异步 I/O 的核心界定)
    • [1.1 阻塞 I/O 模型(Blocking I/O)](#1.1 阻塞 I/O 模型(Blocking I/O))
    • [1.2 非阻塞 I/O 模型(Non-blocking I/O)](#1.2 非阻塞 I/O 模型(Non-blocking I/O))
    • [1.3 I/O 多路复用模型(I/O Multiplexing)](#1.3 I/O 多路复用模型(I/O Multiplexing))
    • [1.4 信号驱动 I/O 模型(Signal-driven I/O)](#1.4 信号驱动 I/O 模型(Signal-driven I/O))
    • [1.5 异步 I/O 模型(Asynchronous I/O)](#1.5 异步 I/O 模型(Asynchronous I/O))
    • [1.6 五种 I/O 模型核心对比](#1.6 五种 I/O 模型核心对比)
  • [2. 五种IO模型](#2. 五种IO模型)
    • [2.1 阻塞IO](#2.1 阻塞IO)
    • [2.2 非阻塞IO](#2.2 非阻塞IO)
    • [2.3 IO多路转接](#2.3 IO多路转接)
    • [2.4 信号驱动IO](#2.4 信号驱动IO)
    • [2.5 异步IO](#2.5 异步IO)
  • [3. 高级IO重要概念](#3. 高级IO重要概念)
    • [3.1 同步通信 vs 异步通信(synchronous communication/ asynchronous](#3.1 同步通信 vs 异步通信(synchronous communication/ asynchronous)
      • [3.1.1 概念](#3.1.1 概念)
      • [3.1.2 易混淆概念区分:两种「同步」的本质差异](#3.1.2 易混淆概念区分:两种「同步」的本质差异)
    • [3.2 阻塞 vs 非阻塞](#3.2 阻塞 vs 非阻塞)
    • [3.3 阻塞 / 非阻塞 与 同步 / 异步的核心区分](#3.3 阻塞 / 非阻塞 与 同步 / 异步的核心区分)
    • [3.4 其他高级IO](#3.4 其他高级IO)
  • [4. 非阻塞IO](#4. 非阻塞IO)
    • [4.1 fcntl](#4.1 fcntl)
      • [4.1.1 核心实操:将文件描述符设为非阻塞模式](#4.1.1 核心实操:将文件描述符设为非阻塞模式)

1. 五种IO概念

在类 Unix/Linux 系统中,经典的五种 I/O 模型 分别是:阻塞 I/O、非阻塞 I/O、I/O 多路复用、信号驱动 I/O、异步 I/O

I/O 操作的核心分为两个关键阶段:等待数据就绪 (内核等待网络 / 磁盘数据,加载到内核缓冲区)、数据复制(将内核缓冲区的数据拷贝到用户进程的缓冲区)。不同模型的核心差异,就体现在这两个阶段进程是否阻塞、如何等待。

1.0 同步与异步 I/O 的核心界定

在 POSIX 标准中,同步 I/O异步 I/O 的判定依据是:数据复制阶段进程是否阻塞

  • 同步 I/O:数据复制时,进程处于阻塞状态,包括阻塞 I/O、非阻塞 I/O、I/O 多路复用、信号驱动 I/O。
  • 异步 I/O:等待数据 + 数据复制全程由内核后台完成,进程全程不阻塞。

1.1 阻塞 I/O 模型(Blocking I/O)

  1. 工作原理

    进程发起 I/O 系统调用(如recvfrom读取网络数据)后,会立即被操作系统挂起,进入阻塞状态。内核会完整执行等待数据就绪和数据复制两个阶段,直到 I/O 操作全部完成,才会将结果返回给进程,进程解除阻塞,继续执行后续逻辑。

  2. 执行流程

    用户进程调用recvfrom → 内核等待数据 → 数据就绪,内核拷贝数据至用户空间 → 系统调用返回,进程恢复运行。

  3. 特点与适用场景

    • 优点:实现逻辑简单,代码编写和调试成本低,是入门级网络编程的默认模型。
    • 缺点:并发能力极差。一个线程只能处理一个 I/O 连接,高并发场景下需要创建大量线程,带来巨大的内存和线程切换开销。
    • 适用场景:并发量极低的简单单机程序、小型工具类服务。

1.2 非阻塞 I/O 模型(Non-blocking I/O)

  1. 工作原理

    进程设置文件描述符为非阻塞模式后,发起 I/O 调用。若内核数据未就绪,内核会立即返回错误码(如EWOULDBLOCK),进程不会阻塞,可执行其他任务。进程需要通过轮询的方式,反复发起 I/O 调用,直到数据就绪。数据就绪后,进程再次调用 I/O 接口,此时会阻塞等待数据复制完成。

  2. 执行流程

    进程循环调用recvfrom → 内核返回未就绪错误 → 进程执行其他任务 → 重复轮询 → 数据就绪,调用阻塞完成数据拷贝 → 系统调用返回。

  3. 特点与适用场景

    • 优点:进程在等待数据阶段不阻塞,可以充分利用 CPU 处理其他任务。
    • 缺点:轮询会持续占用 CPU 资源,连接数越多,CPU 消耗越严重,高并发场景下效率极低。
    • 适用场景:几乎不单独使用,通常作为 I/O 多路复用模型的基础技术组件。

1.3 I/O 多路复用模型(I/O Multiplexing)

  1. 工作原理

    进程不直接监听单个 I/O 事件,而是通过select/poll/epoll等系统调用,将多个需要监控的文件描述符(网络 Socket、文件句柄等)注册到内核。内核同时监控所有描述符的状态,当任意一个描述符数据就绪,系统调用就会返回。进程再遍历就绪的描述符,发起 I/O 调用完成数据复制(此阶段仍会阻塞)。

  2. 主流实现对比

    实现方式 核心特点 缺陷
    select 支持跨平台,有文件描述符数量限制(默认 1024) 需要遍历所有描述符,效率随描述符数量增加急剧下降
    poll 移除了文件描述符的数量限制 同样需要线性遍历所有描述符,高并发场景性能瓶颈明显
    epoll Linux 专属高效实现,无描述符数量限制,采用事件触发机制 仅支持 Linux 系统,跨平台性差
  3. 特点与适用场景

    • 优点:单个线程可以管理海量 I/O 连接,大幅减少线程数量和切换开销,是高并发网络服务的主流方案。
    • 缺点:数据复制阶段进程仍会阻塞,本质仍属于同步 I/O。
    • 适用场景:高并发网络服务器,如 Nginx、Redis、Java NIO 等主流中间件和服务。

1.4 信号驱动 I/O 模型(Signal-driven I/O)

  1. 工作原理

    进程通过系统调用,向内核注册 I/O 事件的信号处理函数。发起 I/O 请求后,进程立即返回,继续执行其他任务。内核等待数据就绪后,向进程发送指定信号(如SIGIO)。进程捕获信号后,暂停当前任务,调用 I/O 接口,阻塞等待数据复制完成。

  2. 执行流程

    进程注册信号处理函数 → 发起 I/O 请求并立即返回 → 内核等待数据就绪 → 内核发送SIGIO信号 → 进程捕获信号,调用recvfrom完成数据拷贝。

  3. 特点与适用场景

    • 优点:等待数据阶段无需阻塞和轮询,CPU 利用率优于非阻塞 I/O。
    • 缺点:信号处理逻辑复杂,信号队列存在溢出风险;高并发场景下,大量信号会导致程序稳定性下降。
    • 适用场景:生产环境使用极少,仅适用于连接数少、对实时性有一定要求的特殊场景。

1.5 异步 I/O 模型(Asynchronous I/O)

  1. 工作原理

    异步 I/O 是真正意义上的异步操作。进程发起异步 I/O 调用(如aio_read)并指定回调函数后,系统调用会立即返回。内核独立完成等待数据就绪 + 数据复制的全部流程,完成后通过信号或回调函数通知进程。进程全程无需阻塞,可直接使用已经拷贝到用户空间的数据。

  2. 执行流程

    进程调用aio_read并注册回调 → 调用立即返回,进程执行其他任务 → 内核完成数据等待与拷贝 → 内核通知进程 → 进程通过回调处理数据。

  3. 特点与适用场景

    • 优点:I/O 全程无阻塞,CPU 利用率达到最高,是性能最优的 I/O 模型。
    • 缺点:编程逻辑复杂,不同操作系统的 API 和支持度差异极大,调试和维护成本高。
    • 适用场景:对性能、响应速度要求极致的场景,如高性能数据库、高速网络存储系统

1.6 五种 I/O 模型核心对比

模型 等待数据阶段 数据复制阶段 实现难度 CPU 利用率 并发能力
阻塞 I/O 阻塞 阻塞
非阻塞 I/O 非阻塞(轮询) 阻塞 较低 较差
I/O 多路复用 阻塞(监听多路) 阻塞 中等 较高 优秀
信号驱动 I/O 非阻塞(信号通知) 阻塞 较高 较高 一般
异步 I/O 非阻塞 非阻塞 最高 优秀

补充说明

日常高并发服务开发中,I/O 多路复用模型是最常用的选择。它在性能、开发成本、跨平台兼容性之间取得了极佳的平衡。异步 I/O 虽然性能顶尖,但受限于平台兼容性和开发复杂度,仅在极致性能场景下使用。

2. 五种IO模型

2.1 阻塞IO

  • 阻塞IO:在内核将数据准备好之前,系统调用会一直等待。所有的套接字,默认都是阻塞方式。阻塞IO是最常见的IO模型。

2.2 非阻塞IO

  • 非阻塞IO:如果内核还未将数据准备好,系统调用仍然会直接返回,并且返回EWOULDBLOCK错误码。

非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符,这个过程称为轮询。这对CPU来说是较大的浪费,一般只有特定场景下才使用。

2.3 IO多路转接

• IO多路转接:虽然从流程图上看起来和阻塞IO类似。实际上最核心在于IO多路转接能够同时等待多个文件描述符的就绪状态。

2.4 信号驱动IO

  • 信号驱动IO:内核将数据准备好的时候,使用SIGIO信号通知应用程序进行IO操作。

2.5 异步IO

  • 异步IO:由内核在数据拷贝完成时,通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。

小结

  • 任何IO过程中,都包含两个步骤。第一是等待,第二是拷贝。而且在实际的应用场景中,等待消耗的时间往往都远远高于拷贝的时间。让IO更高效,最核心的办法就是让等待的时间尽量少。

3. 高级IO重要概念

在这里, 我们要强调几个概念

3.1 同步通信 vs 异步通信(synchronous communication/ asynchronous

3.1.1 概念

同步通信与异步通信的核心关注对象,是进程 / 线程间的调用发起与结果交付的通信机制。这组概念常和网络 I/O、进程间通信场景绑定,需要和「进程 / 线程同步」的概念严格区分,二者不可混为一谈。

  1. 同步通信

    同步通信的核心特征:调用方发起调用请求后,在被调用方完成操作、返回最终结果之前,当前调用会一直阻塞,不会提前返回

    调用方会主动等待,直至拿到调用的返回结果,才能继续执行后续逻辑。结合我们之前学习的五种 I/O 模型,阻塞 I/O、非阻塞 I/O、I/O 多路复用、信号驱动 I/O 都属于同步 I/O,其底层逻辑也完全契合同步通信的定义 ------ 数据从内核缓冲区复制到用户缓冲区的阶段,调用进程始终需要等待操作完成。

  2. 异步通信

    异步通信与同步通信的行为完全相反:调用方发起调用请求后,该调用会立即返回,调用返回时不会携带本次操作的最终结果

    调用方无需阻塞等待,可以继续执行其他任务。被调用方在后台独立完成全部操作后,会通过状态反馈、系统通知、回调函数 等方式,告知调用方操作已完成,并交付最终的处理结果。对应到 I/O 模型中,只有异步 I/O(AIO) 完全符合异步通信的定义。

3.1.2 易混淆概念区分:两种「同步」的本质差异

在多进程、多线程的学习中,我们会接触到「进程 / 线程同步与互斥」的概念。这里的进程 / 线程同步 ,和上文的同步通信是完全独立、语境不同的技术概念,学习时必须做好区分。

  1. 进程 / 线程同步

    进程 / 线程同步,描述的是多个并发执行的进程或线程之间,为协同完成同一任务而产生的直接制约关系

    多个执行单元为了安全访问临界资源、保证业务执行的先后次序,需要在特定的逻辑节点进行等待、唤醒、消息传递,避免出现竞态条件、数据错乱等问题。例如:线程 A 必须等待线程 B 完成数据写入后,才能执行读取操作,这种协作约束的机制就是进程 / 线程同步。其核心目的,是保障并发程序执行的安全性与有序性

  2. 语境判断方法

    后续阅读技术资料、编写代码时,看到「同步」一词,先明确其所处的技术语境,即可快速区分:

    • 若语境围绕函数调用、消息传递、I/O 操作 展开,讨论的是同步通信:核心关注调用是否阻塞、结果是否立即返回。
    • 若语境围绕多进程 / 多线程并发、临界资源访问、执行次序协调 展开,讨论的是进程 / 线程同步:核心关注多个执行单元的协作约束。

3.2 阻塞 vs 非阻塞

阻塞和非阻塞的核心关注点,是进程或线程在发起系统调用、函数调用后,等待操作结果的过程中,自身所处的执行状态 。它和上一节的同步 / 异步通信属于两个完全独立的评判维度,二者可以相互组合,不能直接划等号。

  1. 阻塞调用

    阻塞调用的核心特征:线程发起调用后,在对应操作彻底完成、最终结果返回之前,当前线程会被操作系统内核挂起,进入阻塞休眠状态

    该线程会暂时放弃 CPU 的使用权,不会执行任何后续指令,直到内核完成操作并返回结果,线程才会被唤醒,继续执行后续代码。结合我们学习的 I/O 模型,阻塞 I/O是最典型的阻塞调用场景,线程在等待数据就绪、内核拷贝数据的整个流程中,都处于阻塞状态。

  2. 非阻塞调用

    非阻塞调用的核心特征:线程发起调用后,即便当前操作无法立刻完成、不能获取最终结果,该调用也不会阻塞当前线程,而是会立即返回

    调用返回时,内核通常会返回特定的状态码或错误码 ,告知调用方当前操作暂未就绪。线程可以在调用返回后,继续处理其他任务,后续再通过轮询、事件监听等方式,重新发起调用以获取最终结果。例如非阻塞 I/O 模型 ,数据未就绪时,recvfrom调用会立即返回错误提示,线程无需挂起等待。

3.3 阻塞 / 非阻塞 与 同步 / 异步的核心区分

这是学习 I/O 模型时的高频易错点,补充该部分内容可以彻底厘清概念:

  1. 关注维度不同

    • 阻塞 / 非阻塞:聚焦调用方线程的运行状态,判断线程是被挂起休眠,还是持续运行。
    • 同步 / 异步:聚焦调用结果的交付方式,判断结果是调用时同步获取,还是通过后续通知、回调异步获取。
  2. 常见的组合形式

    两个概念可以自由组合,形成不同的调用模式,也是 I/O 模型的分类依据:

    • 同步阻塞:经典阻塞 I/O,线程阻塞等待,结果同步返回。
    • 同步非阻塞:非阻塞 I/O,线程不阻塞,但需要主动轮询调用获取结果,仍属于同步通信。
    • 异步非阻塞:异步 I/O,线程不阻塞,结果由内核通过信号、回调异步通知,是高性能的常用组合。
    • 异步阻塞:实际工程开发中几乎没有实用价值,极少使用。

3.4 其他高级IO

非阻塞IO,纪录锁,系统V流机制,I/O多路转接(也叫I/O多路复用),readv和writev函数以及存储映射IO(mmap),这些统称为高级IO.我们此处重点讨论的是I/O多路转接

4. 非阻塞IO

4.1 fcntl

类 Unix/Linux 系统 中,所有通过opensocket等系统调用打开的文件描述符(fd,如网络 Socket、普通文件、管道、设备等),默认的 I/O 工作模式均为阻塞 I/O 。若要将其改为非阻塞模式,最常用的工具就是fcntl函数 ------ 该函数是对文件描述符进行属性控制与状态修改的核心系统调用

cpp 复制代码
#include <unistd.h>
#include <fcntl.h>

// 函数返回值:成功返回对应操作的结果(依cmd而定);失败返回-1,并设置errno
int fcntl(int fd, int cmd, ... /* arg */ );
// 函数参数说明
// fd:需要进行属性控制的文件描述符(如 socket 返回的 fd、open 返回的文件 fd);
// cmd:操作命令,决定 fcntl 要执行的具体功能(如获取状态、设置非阻塞、复制 fd 等);
// ...:可变参数,是否需要传参、传什么类型的参数,完全由cmd的取值决定 ------ 部分 cmd 无需额外参数,部分 // cmd 需要传入int类型的 arg 参数。

fcntl 的功能通过cmd参数区分,核心分为 5 类,均为文件描述符的属性 / 状态操作,术语与系统标准保持一致:

  1. 复制一个现有的文件描述符(cmd=F_DUPFD/F_DUPFD_CLOEXEC):生成一个新的文件描述符,与原 fd 指向同一文件 / 设备,支持设置新 fd 的最小取值;
  2. 获取 / 设置文件描述符标志(cmd=F_GETFD/F_SETFD):操作 fd 本身的标志(如FD_CLOEXEC,进程执行exec时自动关闭该 fd);
  3. 获取 / 设置文件状态标志cmd=F_GETFL/F_SETFL):操作文件的 I/O 模式标志(设置非阻塞的核心操作 ,如O_NONBLOCKO_APPEND等);
  4. 获取 / 设置异步 I/O 的属主(cmd=F_GETOWN/F_SETOWN):指定接收异步 I/O 信号(如SIGIO)的进程 ID 或线程 ID,为信号驱动 I/O 做准备;
  5. 获取 / 设置文件记录锁(cmd=F_GETLK/F_SETLK/F_SETLKW):对文件进行读 / 写锁控制,实现多进程对文件的同步访问,避免竞态。

我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞.

4.1.1 核心实操:将文件描述符设为非阻塞模式

前文讲解了阻塞 / 非阻塞的概念,这里补充最常用的实战用法 ------ 通过fcntlF_GETFLF_SETFL命令,将默认的阻塞 fd 改为非阻塞,这是网络编程、I/O 操作中最基础的步骤,步骤分为 3 步:

  1. 调用fcntl(fd, F_GETFL)获取 fd 当前的文件状态标志
  2. 将获取到的标志与非阻塞标志O_NONBLOCK 进行按位或操作(保留原有标志,仅添加非阻塞属性);
  3. 调用fcntl(fd, F_SETFL, 新标志)将修改后的标志写回 fd,完成非阻塞模式设置。

封装成工具函数(可直接复用)

cpp 复制代码
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

// 将文件描述符设置为非阻塞模式
// 返回值:成功返回0;失败返回-1
int set_nonblock(int fd) {
    // 1. 获取当前的文件状态标志
    int old_fl = fcntl(fd, F_GETFL);
    if (old_fl == -1) {
        return -1; // 获取失败,直接返回
    }
    // 2. 按位或O_NONBLOCK,添加非阻塞属性(保留原有所有标志)
    int new_fl = old_fl | O_NONBLOCK;
    // 3. 将新标志写回文件描述符
    if (fcntl(fd, F_SETFL, new_fl) == -1) {
        return -1;
    }
    return 0;
}

关键注意事项

  1. 不可直接设置O_NONBLOCK:必须先获取原有标志再按位或,否则会覆盖 fd 原有的状态(如O_APPEND追加写、O_RDWR读写模式等);
  2. F_SETFL可修改的标志有限:仅能修改O_NONBLOCK(非阻塞)、O_APPEND(追加写)、O_ASYNC(异步 I/O)等少数标志,文件的读写模式(如O_RDONLY/O_WRONLY)无法通过fcntl修改;
  3. 错误处理:fcntl 操作失败会返回 - 1 并设置errno,可通过perror("fcntl")打印具体错误原因(如 fd 无效、权限不足)。

...过云雨-CSDN博客

相关推荐
源远流长jerry2 小时前
dpdk之kni处理dns案例
linux·网络·网络协议·ubuntu·ip
玉梅小洋2 小时前
iperf 网络性能测试完整指南(含多服务端测试)
网络·测试工具·性能测试·iperf
Danileaf_Guo2 小时前
我们的WireGuard管理系统支持手机电脑了!全平台终端配置,支持扫码连接,一键搞定
网络
犀思云3 小时前
构建全球化多云网格:FusionWAN NaaS 在高可用基础设施中的工程实践
运维·网络·人工智能·系统架构·机器人
Black蜡笔小新3 小时前
国密GB35114平台EasyGBS筑牢安防安全防线,GB28181/GB35114无缝接入
网络·安全·音视频·gb35114
多多*4 小时前
2月3日面试题整理 字节跳动后端开发相关
android·java·开发语言·网络·jvm·adb·c#
vortex55 小时前
Alpine Linux syslinux 启动加固(密码保护)
linux·服务器·网络
犀思云5 小时前
网络运维减负:解构FusionWAN NaaS 面向企业广域网的技术逻辑演进
网络·智能仓储·fusionwan·专线·naas
倔强的石头1065 小时前
边缘侧时序数据的选型指南:网络不稳定、数据不丢、回传可控——用 Apache IoTDB 设计可靠链路
网络·apache·iotdb