一、五种 I/O 模型详解(附"钓鱼"类比)
**在操作系统中,I/O(输入/输出)操作是程序与外部设备(如磁盘、网络)交互的核心机制。**所有 I/O 操作本质上都包含两个关键阶段:
- 等待数据就绪(Waiting for data to be ready)
- 将数据从内核拷贝到用户空间(Copying the data from kernel to user space)
不同的 I/O 模型在这两个阶段的行为不同,从而影响了程序的性能、资源利用率和编程复杂度。
为便于理解,我们以"钓鱼"作为比喻:
- 鱼 = 数据
- 钓鱼人 = 应用程序
- 鱼上钩 = 数据就绪
- 把鱼拉上岸 = 数据拷贝到用户空间
1、阻塞 I/O(Blocking I/O)
1.原理
-
当应用程序发起一个 I/O 系统调用(如
read())时,如果内核尚未准备好数据(例如网络数据未到达),该调用会一直阻塞 ,直到数据就绪并完成拷贝。
-
所有 socket 套接字默认都是阻塞模式。
2.钓鱼类比
你坐在池塘边钓鱼,眼睛盯着浮漂。一旦鱼没上钩,你就一动不动地等,什么事也不能做,直到鱼咬钩并被你拉上岸。
3.阻塞式recvfrom系统调用的执行流程

这张图展示了 一个典型的阻塞式 recvfrom 系统调用 的执行流程,描述了从应用程序发起网络数据接收请求到最终获取数据的完整过程。它涉及 用户空间(应用进程)与内核空间之间的交互,是理解 Linux/Unix 网络 I/O 模型的基础。

图中各部分说明
-
左侧:应用进程
- 表示运行在用户态的应用程序(如 Web 服务器、客户端等)。
- 调用
recvfrom()函数来接收 UDP 数据报。
-
右侧:内核
- 操作系统内核负责管理网络设备、缓冲区和系统资源。
- 处理网络数据的接收、存储和传输。
-
箭头表示控制流或数据流方向
详细流程分步讲解
第一步:应用进程调用 recvfrom
text
应用进程 → recvfrom → 系统调用 → 内核
- 应用程序通过
recvfrom()函数向操作系统发出请求,希望接收来自某个套接字的数据报(UDP)。 - 这是一个 系统调用(System Call),会触发从用户态切换到内核态。
- 此时,CPU 的执行权交给内核。
注意:
recvfrom()是一个 阻塞调用,意味着如果当前没有数据可读,进程会被挂起(阻塞),直到有数据到达。
第二步:内核检查是否有数据准备好
text
内核 → 无数据报准备好 → 等待数据
- 内核收到请求后,立即检查该套接字对应的 接收缓冲区(receive buffer) 是否已有数据。
- 如果 缓冲区为空 (即还没有收到任何数据),那么:
- 内核不会立刻返回;
- 它会让这个进程进入 "等待状态"(wait state),并将其加入等待队列;
- 同时释放 CPU,让其他进程运行(避免浪费 CPU 时间)。
这就是所谓的 "阻塞等待" ------ 应用进程被挂起,直到数据到达。
第三步:数据报到达并准备就绪
text
数据报准备好 → 拷贝数据报 → 将数据从内核拷贝到用户空间
- 当网络接口收到数据包(例如来自远程主机的 UDP 数据报)时:
- 数据先被存入内核的 网络缓冲区;
- 然后由协议栈处理(解析 IP/UDP 头部),确认目标套接字;
- 最终将数据放入该套接字的 接收缓冲区。
- 一旦数据进入接收缓冲区,内核就会唤醒之前因
recvfrom()而阻塞的进程。
这个阶段可能耗时较长,取决于网络延迟、对方发送速度等因素。
第四步:内核将数据从内核空间拷贝到用户空间
text
拷贝数据报 → 拷贝完成 → 返回成功指示
- 数据虽然已经在内核缓冲区中,但应用程序无法直接访问;
- 因此,内核需要将数据 复制一份 到应用程序指定的用户空间内存区域(即
recvfrom()的参数缓冲区)。 - 这个操作称为 "数据拷贝"(data copy),是从内核空间到用户空间的一次内存复制。
**注意:**这是一次 额外的内存拷贝开销,也是传统 I/O 模型性能瓶颈之一。
第五步:返回成功指示,应用进程继续执行
text
返回成功指示 → 处理数据报
- 数据拷贝完成后,内核将控制权返回给应用进程;
recvfrom()函数返回,并携带实际接收到的数据长度;- 应用进程此时可以安全地读取用户空间中的数据,并进行后续处理(如解析、显示、转发等)。
整体流程总结(按时间顺序)
| 步骤 | 描述 |
|---|---|
| 1 | 应用进程调用 recvfrom(),进入系统调用 |
| 2 | 内核发现无数据 → 进程阻塞,等待数据到达 |
| 3 | 网络数据到达,被内核接收并放入套接字缓冲区 |
| 4 | 内核唤醒阻塞的进程 |
| 5 | 内核将数据从内核缓冲区拷贝到用户空间缓冲区 |
| 6 | 拷贝完成,recvfrom() 返回成功 |
| 7 | 应用进程继续执行,处理接收到的数据 |
关键概念强调
阻塞 vs 非阻塞
- 图中展示的是 阻塞模式 (blocking mode):
- 若无数据,进程一直等待;
- 不占用 CPU,效率高。
- 相对地,非阻塞模式下
recvfrom()会立即返回-EAGAIN或-EWOULDBLOCK,需轮询或使用select/poll/epoll等机制。
用户空间 vs 内核空间
- 用户空间:应用程序代码运行的地方,不能直接访问硬件或内核资源。
- 内核空间:操作系统核心,负责硬件管理、内存分配、网络通信等。
- 数据必须经过 内核空间 → 用户空间 的拷贝才能被程序使用。
两次拷贝(经典问题)
- 数据从网卡 → 内核缓冲区(第一次拷贝)
- 数据从内核缓冲区 → 用户缓冲区(第二次拷贝)
- 这是传统 I/O 的主要性能瓶颈,后来出现 零拷贝技术(Zero-Copy) 来优化,如
mmap、sendfile、splice等。
实际意义
这个模型广泛应用于:
-
基于 UDP 的实时通信(如视频会议、游戏);
-
TCP 的
recv()接收流程也类似;
-
所有基于阻塞 socket 编程的场景。
理解这一流程有助于:
- 分析程序卡住的原因(是否在等待网络数据?);
- 优化网络性能(减少不必要的拷贝);
- 设计高性能服务器(如使用 epoll + 非阻塞 I/O)。
总结一句话
当应用调用阻塞的
recvfrom()时,若无数据,进程会暂停等待;一旦数据到达,内核将其从内核缓冲区拷贝到用户空间,然后通知进程继续执行。
这是一个经典的 同步阻塞 I/O 模型,是现代操作系统网络编程的基础。
4.特点
- 实现简单,逻辑清晰。
- 在等待期间,线程/进程被挂起,无法执行其他任务。
- 适用于并发量低、逻辑简单的场景。
5.适用场景
- 单线程命令行工具、简单客户端程序。
2、非阻塞 I/O(Non-blocking I/O)
1.原理
-
应用程序设置文件描述符为非阻塞模式(如通过
fcntl()设置O_NONBLOCK)。
-
调用
read()时,若数据未就绪,系统立即返回错误码EWOULDBLOCK或EAGAIN,不会阻塞。
-
程序需主动轮询(polling)反复尝试读取,直到成功。
2.钓鱼类比
你不停地把鱼竿甩进水里又拉出来,每次拉上来都看有没有鱼。没有?马上再甩一次。如此循环,直到钓到鱼为止。整个过程你非常忙碌,但大部分时间其实是在"白忙"。
3.非阻塞式recvfrom系统调用的执行流程

这张图展示了 非阻塞式 recvfrom 系统调用 的执行流程,是与上一图(阻塞式)相对应的另一种 I/O 模型。它体现了 轮询(Polling)机制 在网络编程中的应用。
图中各部分说明
- 左侧:应用进程
- 用户程序反复调用
recvfrom()来尝试接收数据。
- 用户程序反复调用
- 右侧:内核
- 操作系统内核负责管理网络数据的到达和缓冲区状态。
- 箭头表示控制流或返回值方向
详细流程分步讲解
第一步:应用进程首次调用 recvfrom
text
应用进程 → recvfrom → 系统调用 → 内核
- 应用程序发起一个
recvfrom()调用,请求从套接字接收 UDP 数据报。 - 这是一个 非阻塞系统调用(通常通过设置套接字为非阻塞模式实现)。
- 内核检查该套接字的接收缓冲区是否有数据。
注意:非阻塞模式下,即使没有数据,也不会让进程挂起。
第二步:无数据 → 返回 EWOULDBLOCK 错误
text
内核 → 无数据报准备好 → EWOULDBLOCK ← 回到应用进程
- 内核发现当前 没有任何数据报准备好(缓冲区为空);
- 它不会等待,而是立即返回一个错误码:
EWOULDBLOCK(或EAGAIN,取决于平台); - 表示:"我不能现在为你服务,但你可以稍后再试。"
此时应用进程 不会被阻塞,而是继续运行(比如去处理其他任务),但需要自己决定何时再次尝试。
第三步:应用进程重复调用 recvfrom(轮询)
text
应用进程 → recvfrom → 系统调用 → 内核 → EWOULDBLOCK ← 回来
- 应用程序在循环中不断重复调用
recvfrom(); - 每次调用都返回
EWOULDBLOCK,直到有数据到达; - 这种行为称为 "轮询"(Polling) 或 "忙等"(Busy-Waiting)。
示例代码(伪代码):
c
while (1) {
int n = recvfrom(sockfd, buf, len, 0, &addr, &addrlen);
if (n < 0) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
// 没有数据,稍后重试
usleep(100); // 短暂休眠避免CPU占用过高
continue;
}
// 其他错误处理
break;
}
// 成功收到数据,处理之
process_data(buf, n);
}
第四步:数据报到达并准备就绪
text
数据报准备好 → 拷贝数据报 → 将数据从内核拷贝到用户空间
- 当网络接口收到数据包(如来自远程主机的 UDP 报文):
- 数据被放入内核的接收缓冲区;
- 协议栈解析后确认目标套接字;
- 缓冲区变为"有数据可读"状态。
这个阶段可能延迟较久,取决于网络状况。
第五步:最后一次调用成功完成
text
recvfrom → 系统调用 → 数据报准备好 → 拷贝数据报 → 拷贝完成 → 返回成功指示
- 应用进程再次调用
recvfrom(); - 内核发现已有数据,于是开始将数据从内核缓冲区 拷贝到用户空间;
- 拷贝完成后,函数返回成功,并传回实际接收到的数据长度;
- 应用进程可以安全地处理这些数据。
第六步:应用进程处理数据报
text
处理数据报
- 应用程序对收到的数据进行后续操作(如解析、显示、转发等)。
关键概念强调
非阻塞 vs 阻塞
| 特性 | 阻塞模式 | 非阻塞模式 |
|---|---|---|
| 是否等待 | 是,进程挂起 | 否,立即返回 |
| 返回值 | 成功或等待 | 成功 / EWOULDBLOCK / 其他错误 |
| CPU 使用 | 低(不占用) | 可能高(轮询时) |
| 实现方式 | 默认行为 | 设置 O_NONBLOCK 标志 |
轮询(Polling)
- 应用程序主动、频繁地询问:"数据来了吗?"
- 优点:简单易懂,无需复杂事件机制;
- 缺点:效率低,尤其在网络空闲时浪费大量 CPU 时间(忙等);
- 常用于早期网络程序或轻量级场景。
数据拷贝过程(同阻塞模式)
- 数据仍需经历 内核缓冲区 → 用户空间 的拷贝;
- 这是传统 I/O 的性能瓶颈之一;
- 后续会引入
select/poll/epoll等机制来避免频繁轮询。
实际意义
这种模型适用于以下场景:
- 对实时性要求不高,但希望保持程序响应性的场合;
- 多路复用前的过渡方案;
- 学习操作系统原理的基础案例。
但它存在明显缺点:
- CPU 占用率高:如果轮询频率太高,会导致 CPU 不断切换上下文;
- 无法处理多个连接:单个线程只能处理一个套接字,且必须轮询所有。
总结一句话
在非阻塞模式下,应用进程反复调用
recvfrom(),每次若无数据则返回EWOULDBLOCK,直到数据到达才真正完成拷贝并返回成功。这是一种基于轮询的"主动等待"机制。
对比总结:阻塞 vs 非阻塞
| 项目 | 阻塞模式 | 非阻塞模式 |
|---|---|---|
| 是否等待 | 是,进程阻塞 | 否,立即返回 |
| 如何知道数据到来 | 内核唤醒进程 | 应用程序轮询 |
| CPU 利用 | 低 | 可能很高(轮询) |
| 实现复杂度 | 简单 | 中等(需循环处理) |
| 适用场景 | 单连接、简单服务器 | 多连接、高并发(需配合多路复用) |
**后续发展:**现代高性能服务器使用
select,poll,epoll等机制,实现 "事件驱动 + 非阻塞",既避免了阻塞,又避免了轮询浪费 CPU。
4.特点
- 不会阻塞线程,可继续执行其他逻辑。
- CPU 开销大:频繁轮询浪费计算资源。
- 通常不单独使用,而是配合 I/O 多路复用。
5.适用场景
- 需要精细控制 I/O 行为的底层库(如某些高性能网络框架的内部实现)。
3、信号驱动 I/O(Signal-driven I/O,SIGIO)
1.原理
-
应用程序通过
sigaction注册SIGIO信号处理函数,并启用 socket 的异步通知(如F_SETOWN+O_ASYNC)。
-
当数据就绪 (即内核缓冲区有数据可读)时,内核发送
SIGIO信号通知应用程序。 -
应用程序在信号处理函数中调用
read()进行数据拷贝。
注意:信号仅表示"可以开始拷贝",实际拷贝仍需同步完成。
2.钓鱼类比
你请了一个助手站在池塘边。你去干别的事。一旦鱼上钩,助手立刻朝你喊一声:"鱼上钩啦!" 你听到后,赶紧跑过去把鱼拉上来。
3.基于信号(Signal)的异步 I/O 模型流程

这张图展示了 基于信号(Signal)的异步 I/O 模型 ,具体是使用 SIGIO 信号来通知应用进程数据已就绪的机制。这是一种典型的 "异步通知 + 阻塞接收" 的混合模式,常用于实现高效、响应迅速的网络程序。
图中各部分说明
- 左侧:应用进程
- 用户程序注册信号处理函数,并在收到信号后执行
recvfrom()。
- 用户程序注册信号处理函数,并在收到信号后执行
- 右侧:内核
- 操作系统内核负责管理套接字状态和数据到达事件。
- 箭头表示控制流或事件触发方向
详细流程分步讲解
第一步:应用进程建立 SIGIO 信号处理程序
text
应用进程 → 系统调用 sigaction → 内核 → 返回
- 应用程序通过
sigaction()系统调用,为SIGIO信号注册一个 信号处理函数(signal handler); - 这个函数会在数据到达时被自动调用;
- 注册完成后,系统立即返回,进程继续正常执行,无需等待。
注意:
SIGIO是一个专门用于 I/O 就绪通知 的信号,通常与非阻塞套接字配合使用。
示例代码(伪代码):
c
void sigio_handler(int sig) {
recvfrom(sockfd, buf, len, 0, &addr, &addrlen);
}
// 注册信号处理函数
sigaction(SIGIO, &sa, NULL);
第二步:进程继续执行其他任务
text
进程继续执行
- 在注册完信号处理程序后,主程序不会暂停;
- 它可以继续做其他事情(如处理用户输入、计算等),不需要主动轮询或阻塞等待;
- 这体现了"异步"的核心思想:不依赖于同步等待。
第三步:数据报到达并准备就绪
text
内核 → 数据报准备好 → 递交 SIGIO → 信号处理程序
- 当网络接口收到数据包(例如 UDP 报文):
- 数据被存入内核的接收缓冲区;
- 协议栈确认目标套接字;
- 内核检测到有数据可读;
- 此时,内核会 向该进程发送
SIGIO信号,通知它:"数据来了!"
这是一个 异步事件通知,完全由内核驱动,不需要应用主动查询。
第四步:信号处理程序被调用
text
信号处理程序 → recvfrom → 系统调用 → 内核 → 拷贝数据报
- 操作系统捕获
SIGIO信号,并中断当前进程执行,跳转到之前注册的信号处理函数; - 在信号处理函数中,程序调用
recvfrom()来获取数据; - 由于此时数据已在内核缓冲区中,因此
recvfrom()可以立刻进入数据拷贝阶段。
关键点:信号处理函数中的
recvfrom()是阻塞式的,但只在真正需要时才执行。
第五步:将数据从内核拷贝到用户空间
text
拷贝数据报 → 拷贝完成 → 返回成功指示
- 内核将数据从其内部缓冲区 拷贝到用户空间 的缓冲区;
- 拷贝完成后,
recvfrom()返回成功; - 控制权回到信号处理函数。
注意:在数据拷贝期间,进程可能短暂阻塞 ,因为
recvfrom()是同步操作。
第六步:处理数据报
text
处理数据报
- 信号处理函数完成数据接收后,会返回;
- 主程序恢复执行;
- 应用程序可以在主循环或其他地方处理刚刚收到的数据。
关键概念强调
异步 I/O 的本质
- 事件驱动:不是"我问你有没有",而是"你来了告诉我";
- 低延迟响应:一旦数据到达,立即触发处理;
- 避免轮询浪费 CPU:不像轮询那样不断检查。
SIGIO 信号的作用
- 是一种 I/O 就绪通知机制;
- 只适用于 非阻塞套接字 (需先设置
O_NONBLOCK); - 通常用于 UDP 套接字,也可用于 TCP;
- 不推荐用于复杂逻辑,因为信号处理函数有严格限制(不能调用大多数系统调用)。
信号处理的局限性
| 优点 | 缺点 |
|---|---|
| 快速响应 | 信号处理函数不能太复杂 |
| 节省 CPU | 不能调用 malloc, printf, sleep 等安全函数 |
| 无轮询开销 | 多线程环境下容易出错(信号可能发给错误线程) |
实际意义
这种模型适合以下场景:
- 实时性要求高的网络服务(如聊天服务器、游戏服务器);
- 需要快速响应网络事件的程序;
- 早期 Unix 系统中常见的异步 I/O 方式。
但它也有明显缺点:
- 信号处理函数不能太长或太复杂;
- 可能出现 信号丢失 或 竞态条件;
- 现代系统更倾向于使用
epoll、kqueue等更强大的多路复用机制。
总结一句话
应用进程预先注册
SIGIO信号处理程序,当数据到达时,内核发送SIGIO信号唤醒信号处理函数,在其中调用recvfrom()接收数据,从而实现异步通知下的高效 I/O。
对比总结:三种 I/O 模型对比
| 模型 | 是否阻塞 | 是否轮询 | 是否异步通知 | 特点 |
|---|---|---|---|---|
| 阻塞 I/O | 是 | 否 | 否 | 简单直接,但效率低 |
| 非阻塞 I/O(轮询) | 否 | 是 | 否 | 高 CPU 占用,适合简单场景 |
| 信号驱动 I/O(SIGIO) | 是(仅在处理时) | 否 | 是 | 异步响应快,但信号处理有限制 |
后续发展:现代高性能服务器采用 I/O 多路复用(select/poll/epoll),结合了异步通知与批量处理的优点,成为主流方案。
4.特点
- 避免了轮询,减少了 CPU 浪费。
- 但信号处理机制复杂,易受中断上下文限制(如不能安全调用某些函数)。
- 在实际工程中较少使用,尤其在网络编程中已被 epoll 等机制取代。
5.适用场景
- 特定嵌入式或实时系统(对响应延迟敏感但并发不高)。
4、I/O 多路复用(I/O Multiplexing)------ select / poll / epoll
1.原理
- 应用程序通过
select、poll或epoll等系统调用,同时监控多个文件描述符 。

- 内核负责等待这些 fd 中任意一个变为就绪状态,然后返回。
- 应用程序再对就绪的 fd 执行
read()或write()(此步骤仍是阻塞的 ,但由于已知就绪,通常立即返回)。
核心优势:单线程可高效管理成千上万个连接。
2.钓鱼类比
你在池塘边架了 1000 个鱼竿,每个都连着一个小铃铛。你坐在椅子上闭目养神。只要任何一个鱼竿有鱼上钩,对应的铃铛就会响。你听到铃声后,只去处理那个响的鱼竿。
3.select系统调用工作流程

这张图展示了 select 系统调用 的工作流程,是实现 I/O 多路复用(I/O Multiplexing) 的经典方式之一。它允许一个进程同时监控多个套接字(文件描述符),等待其中任意一个变为"可读"状态,从而高效地处理多个网络连接。

图中各部分说明
- 左侧:应用进程
- 用户程序调用
select和recvfrom来管理多个套接字。
- 用户程序调用
- 右侧:内核
- 操作系统内核负责监听所有被监视的套接字,并在数据到达时通知应用。
- 箭头表示控制流或事件触发方向
详细流程分步讲解
第一步:应用进程调用 select
text
应用进程 → select → 系统调用 → 内核
- 应用程序调用
select()函数,传入一个或多个套接字(通常是文件描述符集合); - 它告诉内核:"请帮我监视这些套接字,看是否有数据可以读取。"
- 此时,
select()是一个 阻塞调用 ,意味着:- 如果当前没有任何套接字有数据,进程会暂停执行;
- 直到至少有一个套接字变为"可读"状态。
注意:
select可以同时监视多个套接字(如客户端连接、服务器监听端口等),这是其核心优势。
第二步:内核等待数据到来
text
内核 → 无数据报准备好 → 等待数据
- 内核开始检查所有被
select监视的套接字; - 如果 没有一个套接字有数据准备就绪 ,内核将进入 等待状态;
- 它不会占用 CPU,而是把控制权交给其他进程运行。
这个阶段可能持续较长时间,取决于网络状况和客户端行为。
第三步:某个套接字的数据报准备好
text
数据报准备好 → 返回可读条件 ← select
- 当某一个套接字(例如客户端 A 发送了数据)收到数据包:
- 数据被存入该套接字的接收缓冲区;
- 内核检测到这个变化;
- 它立即唤醒之前因
select而阻塞的进程; - 并返回一个指示:"以下套接字现在可读" (通常通过修改
fd_set集合来标记)。
**示例:**如果监听了 3 个 socket(sock1, sock2, sock3),只有 sock2 有数据,则
select返回并标记 sock2 为"可读"。
第四步:应用进程调用 recvfrom 接收数据
text
应用进程 → recvfrom → 系统调用 → 内核 → 拷贝数据报
- 应用程序根据
select返回的结果,知道哪个套接字可以读取; - 它对那个套接字调用
recvfrom(); - 由于数据已经在内核缓冲区中,
recvfrom()可以立刻进入数据拷贝阶段。
这一步是 同步阻塞的,但只发生在真正需要接收数据的时候。
第五步:将数据从内核拷贝到用户空间
text
拷贝数据报 → 拷贝完成 → 返回成功指示
- 内核将数据从其内部缓冲区 拷贝到用户空间 的缓冲区;
- 拷贝完成后,
recvfrom()返回成功; - 控制权回到应用程序。
在拷贝期间,进程会被短暂阻塞,直到数据完全复制完毕。
第六步:处理数据报
text
处理数据报
- 应用程序拿到数据后,进行解析、响应或其他业务逻辑处理;
- 处理完成后,可以再次调用
select,继续等待下一个事件。
关键概念强调
I/O 多路复用(I/O Multiplexing)
- 允许一个线程/进程同时管理多个 I/O 操作;
- 解决了"一个线程只能处理一个连接"的问题;
- 是构建高性能服务器的基础技术。
select 的特点
| 特性 | 说明 |
|---|---|
| 是否支持多文件描述符 | 支持(最多 1024 个) |
| 是否阻塞 | 默认阻塞,也可设置超时 |
| 是否可重用 | 可重复调用,形成循环 |
| 是否跨平台 | POSIX 标准,Linux/macOS/Windows 均支持 |
为什么比轮询好?
- 不需要频繁调用
recvfrom()查看是否有数据; - 不浪费 CPU 时间;
- 能同时处理多个连接。
实际意义
这种模型适用于以下场景:
- Web 服务器(同时监听多个客户端请求);
- 聊天室服务器(多个用户在线);
- 代理服务器、DNS 服务器等。
但它也有局限性:
- 文件描述符数量有限(通常 1024);
- 每次调用都要遍历所有文件描述符,性能随数量增长而下降;
- 现代系统更推荐使用
poll或epoll(Linux)来替代。
总结一句话
应用进程调用
select等待多个套接字中的任意一个变为可读;一旦有数据到达,select返回并告知具体是哪个套接字;然后应用调用recvfrom将数据从内核拷贝到用户空间,最后处理数据。
这是一种典型的 "事件驱动 + 批量监控" 模型,是实现高并发服务器的核心机制之一。
对比总结:四种 I/O 模型对比
| 模型 | 是否阻塞 | 是否多路复用 | 是否异步通知 | 适用场景 |
|---|---|---|---|---|
| 阻塞 I/O | 是 | 否 | 否 | 简单服务,单连接 |
| 非阻塞 I/O(轮询) | 否 | 否 | 否 | 小规模、低延迟需求 |
| 信号驱动 I/O(SIGIO) | 是(仅处理时) | 否 | 是 | 实时性强的应用 |
| I/O 多路复用(select) | 是(等待阶段) | 是 | 否 | 高并发服务器(传统方案) |
**后续发展:**现代 Linux 使用
epoll替代select,支持更多文件描述符且性能更高,成为主流选择。
4.特点
- 同步 I/O:数据拷贝仍由应用线程完成。
- 高效管理大量并发连接(尤其是 epoll 在 Linux 下性能极佳)。
- 是现代高性能服务器(如 Nginx、Redis)的基础。
5.三种实现对比:
| 模型 | 最大 fd 数 | 时间复杂度 | 是否需要遍历所有 fd |
|---|---|---|---|
| select | ~1024 | O(n) | 是 |
| poll | 无硬限制 | O(n) | 是 |
| epoll | 数十万 | O(1) | 否(仅返回就绪 fd) |
6.适用场景
- 高并发网络服务器(Web 服务器、聊天服务、游戏后端等)。
5、异步 I/O(Asynchronous I/O,AIO)
1.原理(POSIX AIO 或 Linux io_uring)
-
应用程序发起
aio_read()请求后,立即返回 ,不阻塞。
-
内核负责整个 I/O 过程:包括等待数据就绪 + 将数据拷贝到用户缓冲区。
-
拷贝完成后,通过回调函数 或信号通知应用程序 I/O 已完成。
关键区别(重点注意!!!):异步 I/O 的通知发生在"数据拷贝完成之后",而信号驱动 I/O 的通知发生在"数据就绪、可开始拷贝时"。
2.钓鱼类比
你雇了一个专业渔夫,告诉他:"帮我钓一条鱼,钓到后直接放我厨房桌上。" 然后你去做自己的事。渔夫钓到鱼、处理好、放到桌上后,给你发个微信:"搞定!" ------ 你全程无需动手。
3.异步 I/O(Asynchronous I/O)工作流程

这张图展示了 异步 I/O(Asynchronous I/O) 的工作流程,具体是使用 aio_read 系统调用实现的 真正的异步读取操作。这是现代操作系统中最高级、最高效的 I/O 模型之一,尤其适用于高性能服务器和实时系统。
图中各部分说明
- 左侧:应用进程
- 用户程序调用
aio_read发起异步读请求,并继续执行其他任务。
- 用户程序调用
- 右侧:内核
- 操作系统内核负责接收请求、等待数据到达并完成拷贝。
- 箭头表示控制流或事件触发方向
详细流程分步讲解
第一步:应用进程调用 aio_read
text
应用进程 → aio_read → 系统调用 → 内核
- 应用程序调用
aio_read()函数,发起一个 异步读取请求; - 它传入参数包括:
- 套接字描述符(或文件描述符);
- 用户空间缓冲区地址;
- 数据长度;
- 一个信号(signal)或回调函数,用于通知操作完成;
- 这是一个 非阻塞系统调用 ,意味着:
- 即使当前没有数据,也不会让进程挂起;
- 系统立即返回,不等待数据到达。
注意:
aio_read是 POSIX 标准中的异步 I/O 接口,Linux 支持(但需特定配置),FreeBSD、Solaris 等也支持。
第二步:进程继续执行其他任务
text
进程继续执行
- 在
aio_read返回后,应用进程 不会暂停; - 它可以立即去做其他事情(如处理用户输入、计算、响应其他请求等);
- 这体现了"异步"的核心思想:发起 I/O 后无需等待,可并发执行其他任务。
第三步:内核等待数据到来
text
内核 → 无数据报准备好 → 等待数据
- 内核收到异步读请求后,开始监听该套接字;
- 如果此时 还没有数据报准备好 ,内核会进入 等待状态;
- 它不会占用 CPU,而是把控制权交给其他进程运行。
这个阶段可能持续较长时间,取决于网络状况和对方发送速度。
第四步:数据报到达并准备就绪
text
数据报准备好 → 拷贝数据报 → 将数据从内核拷贝到用户空间
- 当网络接口收到数据包(例如 UDP 报文):
- 数据被存入内核的接收缓冲区;
- 协议栈解析后确认目标套接字;
- 内核检测到有数据可读;
- 开始将数据从内核缓冲区 拷贝到用户空间 的指定缓冲区。
**注意:**这个拷贝过程是在 内核线程或中断上下文中完成的,不需要应用参与。
第五步:拷贝完成后递交信号
text
拷贝完成 → 递交在 aio_read 中指定的信号 → 信号处理程序
- 数据拷贝完成后,内核会 向应用进程发送一个预设的信号 (如
SIGIO或自定义信号); - 这个信号是通过
aio_read调用时注册的; - 信号处理程序被触发,通知应用程序:"你的异步读取已完成!"
关键点:整个 I/O 操作完全由内核完成,应用只在最后才得知结果。
第六步:信号处理程序处理数据报
text
信号处理程序处理数据报
- 信号处理函数被调用,它可以从之前指定的用户缓冲区中读取数据;
- 然后进行后续处理(如解析、显示、转发等);
- 处理完成后,信号处理程序返回,主程序继续运行。
**注意:**信号处理函数不能太复杂,也不能调用大多数系统调用(如
malloc,printf等),否则可能导致问题。
关键概念强调
异步 I/O 的本质
- 发起 I/O 后立即返回,不阻塞进程;
- I/O 操作由内核独立完成,包括等待数据、拷贝数据;
- 完成时通过信号或回调通知应用;
- 实现了真正的 "非阻塞 + 高效" 的 I/O 模型。
aio_read 的优势
| 优点 | 说明 |
|---|---|
| 不阻塞主线程 | 主程序可以并发执行其他任务 |
| 高性能 | 内核自动处理所有步骤,减少上下文切换 |
| 适合高并发 | 可以同时发起多个异步 I/O 请求 |
| 低延迟 | 数据到达后立即开始拷贝,无需轮询 |
与前几种模型对比
| 模型 | 是否阻塞 | 是否多路复用 | 是否异步通知 | 是否真正异步 |
|---|---|---|---|---|
| 阻塞 I/O | 是 | 否 | 否 | 否 |
| 非阻塞 I/O(轮询) | 否 | 否 | 否 | 否 |
| 信号驱动 I/O(SIGIO) | 是(仅处理时) | 否 | 是 | 是(部分) |
| I/O 多路复用(select) | 是(等待阶段) | 是 | 否 | 否 |
| 异步 I/O(aio_read) | 否 | 是 | 是 | 是 |
"真正异步"指的是:I/O 的整个生命周期(等待+拷贝)都由内核完成,应用完全不参与。
实际意义
这种模型适用于以下场景:
- 高性能数据库服务器;
- 实时交易系统;
- 大规模并发 Web 服务;
- 科学计算、大数据处理等需要高效 I/O 的系统。
但它也有局限性:
- 并非所有系统都原生支持(如 Linux 早期版本支持有限);
- 使用复杂,容易出错;
- 信号处理函数限制多;
- 对于普通应用,
epoll或kqueue更常用且易用。
总结一句话
应用进程调用
aio_read发起异步读请求后立即返回,继续执行其他任务;内核负责等待数据到达并将其从内核拷贝到用户空间;完成后通过信号通知应用,由信号处理程序处理数据。
这是一种 真正的异步 I/O 模型,实现了"发起即走,完成即知"的高效机制,是构建极致性能系统的终极选择。
对比总结:五种 I/O 模型对比
| 模型 | 是否阻塞 | 是否多路复用 | 是否异步通知 | 是否真正异步 | 典型用途 |
|---|---|---|---|---|---|
| 阻塞 I/O | 是 | 否 | 否 | 否 | 简单程序 |
| 非阻塞 I/O(轮询) | 否 | 否 | 否 | 否 | 低频 I/O |
| 信号驱动 I/O(SIGIO) | 是(仅处理时) | 否 | 是 | 是(部分) | 实时系统 |
| I/O 多路复用(select/poll/epoll) | 是(等待阶段) | 是 | 否 | 否 | 高并发服务器 |
| 异步 I/O(aio_read) | 否 | 是 | 是 | 是 | 极致性能系统 |
建议:对于大多数开发人员,优先掌握
epoll;只有在追求极致性能时才考虑aio。
4.特点
- 真正的异步:应用程序完全不参与 I/O 的两个阶段。
- 编程模型复杂,传统 POSIX AIO 在 Linux 上实现不佳(依赖用户态线程模拟)。
- io_uring(Linux 5.1+) 是现代高性能异步 I/O 的代表,接近零拷贝、零上下文切换。
5.适用场景
- 极致性能要求的存储系统、数据库、高频交易系统。
- 新一代网络框架(如 Seastar、Tokio on io_uring)。
小结:核心思想与对比
| 模型 | 是否阻塞 | 通知时机 | 并发能力 | CPU 效率 | 编程复杂度 |
|---|---|---|---|---|---|
| 阻塞 I/O | 是 | 无(直接返回结果) | 低 | 中 | 低 |
| 非阻塞 I/O | 否 | 无(需轮询) | 低 | 低 | 中 |
| 信号驱动 I/O | 否 | 数据就绪时 | 中 | 高 | 高 |
| I/O 多路复用 | 否 | 数据就绪时(批量) | 高 | 高 | 中 |
| 异步 I/O | 否 | 数据拷贝完成时 | 极高 | 极高 | 高 |
注:多路复用本身不阻塞,但后续的
read()是同步的(不过因已就绪,通常不等待)。
核心洞见
任何 I/O 操作都包含"等待"和"拷贝"两个阶段。在实际系统中,"等待"所耗费的时间远大于"拷贝"。因此,提升 I/O 效率的关键在于:尽可能减少或隐藏"等待"时间。
- 阻塞 I/O:等待期间线程休眠 → 资源浪费(无法处理其他请求)。
- 非阻塞轮询:用 CPU 换时间 → 浪费计算资源。
- 多路复用 & 异步 I/O:让一个线程/进程高效处理成千上万 I/O → 最大化硬件利用率。
补充说明
-
同步 vs 异步:
- 同步 I/O:应用程序需主动参与数据拷贝(包括阻塞、非阻塞、多路复用、信号驱动)。
- 异步 I/O:应用程序发起请求后完全脱手,由内核全权负责,完成后通知。
-
Reactor vs Proactor 模式:
- **Reactor(反应器):**基于 I/O 多路复用,事件就绪时触发处理(如 Netty、Nginx)。
- **Proactor(前摄器):**基于异步 I/O,操作完成时触发处理(如 Windows IOCP、Linux io_uring)。
二、五种 I/O 模型再回顾:从阻塞到异步的本质区别
I/O = Input / Output(输入/输出)
在计算机系统中,I/O 指的是程序与外部设备(如磁盘、网卡、键盘等)之间的数据交换过程。
1、为什么 I/O 很"慢"?
问题:为什么访问外设比访问内存慢得多?
- CPU 执行指令的速度以 纳秒(ns) 计;
- 而磁盘读写需 毫秒(ms) 级,网络延迟也常达 几十微秒到毫秒;
- 速度差距可达百万倍!
根本原因:
I/O = 等待(Waiting) + 数据拷贝(Copying)
- "等待":CPU 发出请求后,必须等待外设(如网卡)准备好数据;
- "拷贝":数据从内核缓冲区复制到用户空间(或反之),涉及内存操作。
举例:
read()系统调用 = 等待数据到达 + 将数据从内核拷贝到用户缓冲区。
因此,高效 I/O 的核心目标是:在单位时间内,尽可能减少"等待"所占的时间比重。
2、类比:钓鱼模型 ------ 理解 I/O 的本质
想象五个人在钓鱼,代表五种 I/O 模型:
| 人物 | 行为 | 对应 I/O 模型 |
|---|---|---|
| 张三 | 坐在岸边,鱼竿放下后就一直盯着浮漂,直到鱼上钩才收竿 | 阻塞 I/O |
| 李四 | 不停地提起鱼竿看有没有鱼,没有就放回去,反复检查 | 非阻塞 I/O(轮询) |
| 王五 | 鱼竿装了铃铛,鱼一咬钩就响,他听到铃声再去收竿 | 信号驱动 I/O(SIGIO) |
| 赵六 | 同时放10根鱼竿,哪个铃响就处理哪个 | I/O 多路复用(select/poll/epoll) |
| 钱七 | 请了一个助手帮他钓鱼,鱼钓上来后助手通知他:"鱼好了!" | 异步 I/O(AIO) |
关键洞察:
- 张三、李四、王五、赵六 自己都参与了"收鱼"(即数据拷贝);
- 只有钱七 完全不参与钓鱼过程,只等结果通知。
3、五大 I/O 模型详解
阻塞 I/O(Blocking I/O)
- 行为 :调用
read()后,若无数据,进程挂起(阻塞),直到数据到达并完成拷贝。 - 特点 :
- 简单直观;
- 进程无法做其他事;
- "等待"时间占比高。
- 适用场景:单线程、低并发应用。
本质:同步 I/O(参与了"等 + 拷贝"全过程)。
非阻塞 I/O(Non-blocking I/O)
- 行为 :调用
read(),若无数据,立即返回错误(如EAGAIN),程序可去做别的事,稍后再试。 - 特点 :
- 不阻塞进程;
- 但需频繁轮询,浪费 CPU;
- "等待"被分散,但总开销可能更高。
- 效率误区 :看似"高效",实则因轮询导致 CPU 占用高,整体效率未必高。
本质 :同步 I/O(仍需主动调用
read完成拷贝)。
信号驱动 I/O(Signal-driven I/O)
- 行为 :
- 注册
SIGIO信号处理函数; - 主程序继续运行;
- 数据就绪时,内核发送
SIGIO; - 信号处理函数中调用
recvfrom完成数据拷贝。
- 注册
- 特点 :
- 避免轮询;
- 响应快;
- 但信号处理函数限制多(不能调用复杂函数)。
- 注意 :拷贝阶段仍是同步的!
本质:同步 I/O(应用仍参与了"拷贝"阶段)。
I/O 多路复用(I/O Multiplexing)
- 代表系统调用 :
select、poll、epoll - 行为 :
- 一个线程可同时监视多个文件描述符;
- 调用
select阻塞等待,任一 fd 就绪即返回; - 再对就绪的 fd 调用
read拷贝数据。
- 优势 :
- 单线程管理成千上万连接;
- 避免创建大量线程的开销;
- 是现代高性能服务器(如 Nginx、Redis)的基础。
- 局限 :仍需两次系统调用(
select+read)。
本质 :同步 I/O(最终仍需
read完成拷贝)。
异步 I/O(Asynchronous I/O, AIO)
- 代表接口 :POSIX
aio_read/ Linux io_uring - 行为 :
- 调用
aio_read,传入缓冲区和回调; - 立即返回,进程继续执行;
- 内核全权负责:等待数据 + 拷贝到用户空间;
- 完成后通过信号或回调通知应用。
- 调用
- 关键特征 :
- 应用不参与"等"也不参与"拷贝";
- I/O 全过程由内核独立完成;
- 真正实现"发起即走,完成即知"。
本质 :真正的异步 I/O(应用与 I/O 工作流完全解耦)。
4、核心辨析:同步 vs 异步 I/O
判断标准(黄金法则):
只要应用程序参与了 I/O 的"等待"或"拷贝"中的任意一个阶段,就是同步 I/O;
只有当应用程序既不等待、也不拷贝,仅发起请求并接收完成通知,才是异步 I/O。
| 模型 | 是否参与"等待"? | 是否参与"拷贝"? | 同步/异步 |
|---|---|---|---|
| 阻塞 I/O | 是 | 是 | 同步 |
| 非阻塞 I/O | 否(但轮询) | 是 | 同步 |
| 信号驱动 I/O | 否 | 是 | 同步 |
| I/O 多路复用 | 是(在 select 中) | 是 | 同步 |
| 异步 I/O (AIO) | 否 | 否 | 异步 |
重要结论 :
前四种模型本质上都是"同步 I/O" ,只是"等待"的方式不同(阻塞、轮询、信号、多路复用)。
只有第五种(AIO)是真正的异步 I/O。
5、如何衡量 I/O 效率?
- 高效 I/O = 单位时间内,有效工作时间占比高,等待时间占比低
- 钓鱼比喻中:
- 张三(阻塞):100% 时间在等;
- 李四(非阻塞):90% 时间在提竿检查(忙等);
- 赵六(多路复用):同时管多根竿,等的比重降低;
- 钱七(异步):完全不等,只处理结果 → 效率最高。
总结:一张表看懂五种 I/O 模型
| 模型 | 是否阻塞 | 是否多路复用 | 是否异步通知 | 是否真正异步 | 典型用途 |
|---|---|---|---|---|---|
| 阻塞 I/O | 是 | 否 | 否 | 否 | 简单脚本、教学示例 |
| 非阻塞 I/O | 否 | 否 | 否 | 否 | 早期嵌入式系统 |
| 信号驱动 I/O | (主流程) | 否 | 是 | 否 | 实时性要求高的场景 |
| I/O 多路复用 | (select 阶段) | 是 | 否 | 否 | Web 服务器(Nginx、Redis) |
| 异步 I/O (AIO) | 否 | 是 | 是 | 是 | 高性能数据库、金融系统 |
未来趋势 :Linux 的
io_uring正在推动真正的异步 I/O 成为主流,大幅降低系统调用开销。
最后强调
- "阻塞/非阻塞" 描述的是调用是否立即返回;
- "同步/异步" 描述的是应用是否参与 I/O 的核心过程(等+拷贝);
- 不要混淆这两个维度!
理解这五大模型,是掌握高性能网络编程、操作系统原理和系统架构设计的基石。