目录
- [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)
-
工作原理
进程发起 I/O 系统调用(如
recvfrom读取网络数据)后,会立即被操作系统挂起,进入阻塞状态。内核会完整执行等待数据就绪和数据复制两个阶段,直到 I/O 操作全部完成,才会将结果返回给进程,进程解除阻塞,继续执行后续逻辑。 -
执行流程
用户进程调用
recvfrom→ 内核等待数据 → 数据就绪,内核拷贝数据至用户空间 → 系统调用返回,进程恢复运行。 -
特点与适用场景
- 优点:实现逻辑简单,代码编写和调试成本低,是入门级网络编程的默认模型。
- 缺点:并发能力极差。一个线程只能处理一个 I/O 连接,高并发场景下需要创建大量线程,带来巨大的内存和线程切换开销。
- 适用场景:并发量极低的简单单机程序、小型工具类服务。
1.2 非阻塞 I/O 模型(Non-blocking I/O)
-
工作原理
进程设置文件描述符为非阻塞模式后,发起 I/O 调用。若内核数据未就绪,内核会立即返回错误码(如
EWOULDBLOCK),进程不会阻塞,可执行其他任务。进程需要通过轮询的方式,反复发起 I/O 调用,直到数据就绪。数据就绪后,进程再次调用 I/O 接口,此时会阻塞等待数据复制完成。 -
执行流程
进程循环调用
recvfrom→ 内核返回未就绪错误 → 进程执行其他任务 → 重复轮询 → 数据就绪,调用阻塞完成数据拷贝 → 系统调用返回。 -
特点与适用场景
- 优点:进程在等待数据阶段不阻塞,可以充分利用 CPU 处理其他任务。
- 缺点:轮询会持续占用 CPU 资源,连接数越多,CPU 消耗越严重,高并发场景下效率极低。
- 适用场景:几乎不单独使用,通常作为 I/O 多路复用模型的基础技术组件。
1.3 I/O 多路复用模型(I/O Multiplexing)
-
工作原理
进程不直接监听单个 I/O 事件,而是通过
select/poll/epoll等系统调用,将多个需要监控的文件描述符(网络 Socket、文件句柄等)注册到内核。内核同时监控所有描述符的状态,当任意一个描述符数据就绪,系统调用就会返回。进程再遍历就绪的描述符,发起 I/O 调用完成数据复制(此阶段仍会阻塞)。 -
主流实现对比
实现方式 核心特点 缺陷 select 支持跨平台,有文件描述符数量限制(默认 1024) 需要遍历所有描述符,效率随描述符数量增加急剧下降 poll 移除了文件描述符的数量限制 同样需要线性遍历所有描述符,高并发场景性能瓶颈明显 epoll Linux 专属高效实现,无描述符数量限制,采用事件触发机制 仅支持 Linux 系统,跨平台性差 -
特点与适用场景
- 优点:单个线程可以管理海量 I/O 连接,大幅减少线程数量和切换开销,是高并发网络服务的主流方案。
- 缺点:数据复制阶段进程仍会阻塞,本质仍属于同步 I/O。
- 适用场景:高并发网络服务器,如 Nginx、Redis、Java NIO 等主流中间件和服务。
1.4 信号驱动 I/O 模型(Signal-driven I/O)
-
工作原理
进程通过系统调用,向内核注册 I/O 事件的信号处理函数。发起 I/O 请求后,进程立即返回,继续执行其他任务。内核等待数据就绪后,向进程发送指定信号(如
SIGIO)。进程捕获信号后,暂停当前任务,调用 I/O 接口,阻塞等待数据复制完成。 -
执行流程
进程注册信号处理函数 → 发起 I/O 请求并立即返回 → 内核等待数据就绪 → 内核发送
SIGIO信号 → 进程捕获信号,调用recvfrom完成数据拷贝。 -
特点与适用场景
- 优点:等待数据阶段无需阻塞和轮询,CPU 利用率优于非阻塞 I/O。
- 缺点:信号处理逻辑复杂,信号队列存在溢出风险;高并发场景下,大量信号会导致程序稳定性下降。
- 适用场景:生产环境使用极少,仅适用于连接数少、对实时性有一定要求的特殊场景。
1.5 异步 I/O 模型(Asynchronous I/O)
-
工作原理
异步 I/O 是真正意义上的异步操作。进程发起异步 I/O 调用(如
aio_read)并指定回调函数后,系统调用会立即返回。内核独立完成等待数据就绪 + 数据复制的全部流程,完成后通过信号或回调函数通知进程。进程全程无需阻塞,可直接使用已经拷贝到用户空间的数据。 -
执行流程
进程调用
aio_read并注册回调 → 调用立即返回,进程执行其他任务 → 内核完成数据等待与拷贝 → 内核通知进程 → 进程通过回调处理数据。 -
特点与适用场景
- 优点: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、进程间通信场景绑定,需要和「进程 / 线程同步」的概念严格区分,二者不可混为一谈。
-
同步通信
同步通信的核心特征:调用方发起调用请求后,在被调用方完成操作、返回最终结果之前,当前调用会一直阻塞,不会提前返回。
调用方会主动等待,直至拿到调用的返回结果,才能继续执行后续逻辑。结合我们之前学习的五种 I/O 模型,阻塞 I/O、非阻塞 I/O、I/O 多路复用、信号驱动 I/O 都属于同步 I/O,其底层逻辑也完全契合同步通信的定义 ------ 数据从内核缓冲区复制到用户缓冲区的阶段,调用进程始终需要等待操作完成。
-
异步通信
异步通信与同步通信的行为完全相反:调用方发起调用请求后,该调用会立即返回,调用返回时不会携带本次操作的最终结果。
调用方无需阻塞等待,可以继续执行其他任务。被调用方在后台独立完成全部操作后,会通过状态反馈、系统通知、回调函数 等方式,告知调用方操作已完成,并交付最终的处理结果。对应到 I/O 模型中,只有异步 I/O(AIO) 完全符合异步通信的定义。
3.1.2 易混淆概念区分:两种「同步」的本质差异
在多进程、多线程的学习中,我们会接触到「进程 / 线程同步与互斥」的概念。这里的进程 / 线程同步 ,和上文的同步通信是完全独立、语境不同的技术概念,学习时必须做好区分。
-
进程 / 线程同步
进程 / 线程同步,描述的是多个并发执行的进程或线程之间,为协同完成同一任务而产生的直接制约关系。
多个执行单元为了安全访问临界资源、保证业务执行的先后次序,需要在特定的逻辑节点进行等待、唤醒、消息传递,避免出现竞态条件、数据错乱等问题。例如:线程 A 必须等待线程 B 完成数据写入后,才能执行读取操作,这种协作约束的机制就是进程 / 线程同步。其核心目的,是保障并发程序执行的安全性与有序性。
-
语境判断方法
后续阅读技术资料、编写代码时,看到「同步」一词,先明确其所处的技术语境,即可快速区分:
- 若语境围绕函数调用、消息传递、I/O 操作 展开,讨论的是同步通信:核心关注调用是否阻塞、结果是否立即返回。
- 若语境围绕多进程 / 多线程并发、临界资源访问、执行次序协调 展开,讨论的是进程 / 线程同步:核心关注多个执行单元的协作约束。
3.2 阻塞 vs 非阻塞
阻塞和非阻塞的核心关注点,是进程或线程在发起系统调用、函数调用后,等待操作结果的过程中,自身所处的执行状态 。它和上一节的同步 / 异步通信属于两个完全独立的评判维度,二者可以相互组合,不能直接划等号。
-
阻塞调用
阻塞调用的核心特征:线程发起调用后,在对应操作彻底完成、最终结果返回之前,当前线程会被操作系统内核挂起,进入阻塞休眠状态。
该线程会暂时放弃 CPU 的使用权,不会执行任何后续指令,直到内核完成操作并返回结果,线程才会被唤醒,继续执行后续代码。结合我们学习的 I/O 模型,阻塞 I/O是最典型的阻塞调用场景,线程在等待数据就绪、内核拷贝数据的整个流程中,都处于阻塞状态。
-
非阻塞调用
非阻塞调用的核心特征:线程发起调用后,即便当前操作无法立刻完成、不能获取最终结果,该调用也不会阻塞当前线程,而是会立即返回。
调用返回时,内核通常会返回特定的状态码或错误码 ,告知调用方当前操作暂未就绪。线程可以在调用返回后,继续处理其他任务,后续再通过轮询、事件监听等方式,重新发起调用以获取最终结果。例如非阻塞 I/O 模型 ,数据未就绪时,
recvfrom调用会立即返回错误提示,线程无需挂起等待。
3.3 阻塞 / 非阻塞 与 同步 / 异步的核心区分
这是学习 I/O 模型时的高频易错点,补充该部分内容可以彻底厘清概念:
-
关注维度不同
- 阻塞 / 非阻塞:聚焦调用方线程的运行状态,判断线程是被挂起休眠,还是持续运行。
- 同步 / 异步:聚焦调用结果的交付方式,判断结果是调用时同步获取,还是通过后续通知、回调异步获取。
-
常见的组合形式
两个概念可以自由组合,形成不同的调用模式,也是 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 系统 中,所有通过open、socket等系统调用打开的文件描述符(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 类,均为文件描述符的属性 / 状态操作,术语与系统标准保持一致:
- 复制一个现有的文件描述符(
cmd=F_DUPFD/F_DUPFD_CLOEXEC):生成一个新的文件描述符,与原 fd 指向同一文件 / 设备,支持设置新 fd 的最小取值; - 获取 / 设置文件描述符标志(
cmd=F_GETFD/F_SETFD):操作 fd 本身的标志(如FD_CLOEXEC,进程执行exec时自动关闭该 fd); - 获取 / 设置文件状态标志 (
cmd=F_GETFL/F_SETFL):操作文件的 I/O 模式标志(设置非阻塞的核心操作 ,如O_NONBLOCK、O_APPEND等); - 获取 / 设置异步 I/O 的属主(
cmd=F_GETOWN/F_SETOWN):指定接收异步 I/O 信号(如SIGIO)的进程 ID 或线程 ID,为信号驱动 I/O 做准备; - 获取 / 设置文件记录锁(
cmd=F_GETLK/F_SETLK/F_SETLKW):对文件进行读 / 写锁控制,实现多进程对文件的同步访问,避免竞态。
我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞.
4.1.1 核心实操:将文件描述符设为非阻塞模式
前文讲解了阻塞 / 非阻塞的概念,这里补充最常用的实战用法 ------ 通过fcntl的F_GETFL和F_SETFL命令,将默认的阻塞 fd 改为非阻塞,这是网络编程、I/O 操作中最基础的步骤,步骤分为 3 步:
- 调用
fcntl(fd, F_GETFL)获取 fd 当前的文件状态标志; - 将获取到的标志与非阻塞标志
O_NONBLOCK进行按位或操作(保留原有标志,仅添加非阻塞属性); - 调用
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;
}
关键注意事项
- 不可直接设置
O_NONBLOCK:必须先获取原有标志再按位或,否则会覆盖 fd 原有的状态(如O_APPEND追加写、O_RDWR读写模式等); F_SETFL可修改的标志有限:仅能修改O_NONBLOCK(非阻塞)、O_APPEND(追加写)、O_ASYNC(异步 I/O)等少数标志,文件的读写模式(如O_RDONLY/O_WRONLY)无法通过fcntl修改;- 错误处理:fcntl 操作失败会返回 - 1 并设置
errno,可通过perror("fcntl")打印具体错误原因(如 fd 无效、权限不足)。