【Linux】五种IO模型与非阻塞IO

作者:好评124日期:2026/9/13

本文主题内容

引言:网络通信中的很多性能问题,本质上并不是数据拷贝本身太慢,而是进程在等待数据到达。理解 IO 模型,就是理解操作系统如何通知进程数据已经就绪,以及数据最终由谁拷贝到用户空间。

一、理解 IO

1.1 一次 IO 的两个阶段

以网络读取为例,一次完整的 IO 通常包含两个阶段:

  1. 等待数据就绪:等待网卡接收数据,并由内核协议栈将数据放入 Socket 接收缓冲区。
  2. 将数据从内核空间拷贝到用户空间:read、recv 等系统调用把数据交给应用程序。

所以,一次 IO 的总时间可以简单理解为:

1IO 时间 = 等待时间 + 数据拷贝时间
2

对网络服务器来说,等待时间往往比真正的数据拷贝时间更长。不同 IO 模型的主要区别,也集中在等待阶段和通知方式上。

1.2 用户空间与内核空间

应用程序不能直接访问网卡,也不能随意读取内核缓冲区。数据到达主机后,先由内核接收和处理,再通过系统调用拷贝到用户空间。

IO 就绪表示内核已经具备完成某类 IO 操作的条件,并不等于业务数据已经处理完成。

二、阻塞 IO

2.1 阻塞读取

默认情况下,大多数 Socket 都是阻塞的。当进程调用 recv,而接收缓冲区中没有数据时,进程会进入睡眠状态,直到出现以下情况之一:

  • 有数据到达。
  • 对端关闭连接。
  • 发生错误。
  • 系统调用被信号中断。

例:

1char buffer[1024];
2ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);
3if (n > 0)
4{
5    //处理收到的数据
6}
7else if (n == 0)
8{
9    //对端关闭连接
10}
11else
12{
13    //读取失败
14}
15

阻塞 IO 的优点是代码简单,调用返回时通常已经得到结果;缺点是一个执行流在等待期间不能处理其他连接。

2.2 阻塞不等于低效

阻塞只是当前执行流暂时不能继续运行。如果服务器使用多个线程,每个线程阻塞等待一个连接,在连接数量不大时仍然能够正常工作。

不过,当连接数量非常大、活跃连接比例又很低时,为每个连接创建一个线程会带来明显的线程切换和内存开销。

三、非阻塞 IO

3.1 非阻塞读取

将 Socket 设置为非阻塞后,如果数据尚未就绪,recv 不会让进程睡眠,而是立即返回 -1,并将 errno 设置为 EAGAIN 或 EWOULDBLOCK。

例:

1char buffer[1024];
2ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);
3if (n > 0)
4{
5    //处理收到的数据
6}
7else if (n == 0)
8{
9    //对端关闭连接
10}
11else if (errno == EAGAIN || errno == EWOULDBLOCK)
12{
13    //当前没有数据,稍后再读
14}
15else if (errno == EINTR)
16{
17    //被信号中断,可以重试
18}
19else
20{
21    //发生真正的读取错误
22}
23

3.2 轮询的问题

如果应用程序不断调用 recv 检查数据是否就绪,就形成了忙轮询:

1while (true)
2{
3    ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);
4    if (n > 0)
5    {
6        break;
7    }
8    if (n < 0 && errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR)
9    {
10        break;
11    }
12}
13

这种方式虽然不会阻塞在一次 recv 上,却会持续占用 CPU。实际开发中,非阻塞 IO 通常要与 select、poll、epoll 等事件通知机制配合使用。

四、IO 多路转接

4.1 基本思想

IO 多路转接让一个执行流同时等待多个文件描述符。当其中任意描述符就绪时,select、poll 或 epoll 返回,应用程序再对就绪描述符执行 read、recv、write 等操作。

它的核心不是让单次 IO 更快,而是减少无效等待,让一个执行流能够管理大量连接。

1多个文件描述符 -> select、poll、epoll -> 返回就绪事件 -> 执行真正的 IO
2

4.2 多路转接仍属于同步 IO

select、poll、epoll 只负责告诉应用程序哪些描述符已经就绪。数据从内核缓冲区拷贝到用户缓冲区,仍然需要应用程序主动调用 recv 或 read 完成。

因此,IO 多路转接属于同步 IO。

五、信号驱动 IO

信号驱动 IO 允许进程先注册信号处理方式。当描述符就绪时,内核向进程发送 SIGIO 信号,进程再执行读取操作。

这种模型避免了持续轮询,但信号处理本身有较多限制:

  • 信号处理函数中只能安全调用异步信号安全函数。
  • 复杂业务逻辑不适合直接放在信号处理函数中。
  • 信号合并、并发状态和错误处理会增加程序复杂度。

所以,网络服务器更常使用 epoll 等多路转接机制。

六、异步 IO

异步 IO 中,应用程序提交 IO 请求后可以继续执行其他任务。内核不仅负责等待数据就绪,还负责把数据拷贝到用户指定的缓冲区,最后再通知应用程序操作已经完成。

1提交异步请求 -> 内核等待并完成数据拷贝 -> 通知应用程序处理结果
2

这和 epoll 的区别在于:

  • epoll 通知的是描述符已经就绪,应用程序还要主动完成 IO。
  • 异步 IO 通知的是 IO 操作已经完成。

七、阻塞、非阻塞、同步与异步

7.1 阻塞与非阻塞

阻塞和非阻塞描述的是:当调用暂时不能得到结果时,当前执行流是否停下来等待。

  • 阻塞:调用暂时不能完成时,执行流进入等待状态。
  • 非阻塞:调用暂时不能完成时,立即返回一个状态,执行流可以继续处理其他任务。

7.2 同步与异步

同步和异步描述的是:真正的数据拷贝由谁完成,以及完成后如何通知调用者。

  • 同步 IO:应用程序主动调用 read、recv 等函数完成数据拷贝。
  • 异步 IO:内核完成等待和数据拷贝,再通知应用程序。

注意:非阻塞 IO 不等于异步 IO,epoll 也不等于异步 IO。非阻塞 Socket 配合 epoll,依然属于同步 IO。

7.3 五种模型对比

IO 模型等待数据数据拷贝调用者是否可能等待常见使用方式
阻塞 IO内核等待应用程序发起简单客户端、线程式服务器
非阻塞 IO应用程序反复检查应用程序发起单次调用不等待通常配合事件通知
IO 多路转接select、poll、epoll 等待应用程序发起等待多个描述符高并发网络服务器
信号驱动 IO内核通过信号通知应用程序发起提交后可继续执行SIGIO
异步 IO内核等待内核完成提交后可继续执行aio、io_uring 等机制

八、使用 fcntl 设置非阻塞

8.1 获取和设置文件状态标志

fcntl 可以获取和修改文件描述符的状态标志:

1#include <fcntl.h>
2
3bool SetNonBlock(int fd)
4{
5    int flags = fcntl(fd, F_GETFL);
6    if (flags == -1)
7    {
8        return false;
9    }
10
11    if (fcntl(fd, F_SETFL, flags | O_NONBLOCK) == -1)
12    {
13        return false;
14    }
15
16    return true;
17}
18

设置新标志时要保留原有 flags,再通过按位或加入 O_NONBLOCK。直接把状态设置为 O_NONBLOCK,可能会意外清除其他标志。

8.2 非阻塞读的完整处理

非阻塞模式下,一次 recv 不一定读取到全部数据,所以常见做法是循环读取,直到返回 EAGAIN 或 EWOULDBLOCK。

例:

1bool ReadAll(int sockfd, std::string* input)
2{
3    char buffer[4096];
4
5    while (true)
6    {
7        ssize_t n = recv(sockfd, buffer, sizeof(buffer), 0);
8        if (n > 0)
9        {
10            input->append(buffer, n);
11            continue;
12        }
13
14        if (n == 0)
15        {
16            return false;
17        }
18
19        if (errno == EINTR)
20        {
21            continue;
22        }
23
24        if (errno == EAGAIN || errno == EWOULDBLOCK)
25        {
26            return true;
27        }
28
29        return false;
30    }
31}
32

在边缘触发的 epoll 中,必须一直读取到 EAGAIN,才能保证已经到达的数据不会被遗漏。

九、总结

  1. 一次 IO 通常包含等待数据就绪和把数据拷贝到用户空间两个阶段。
  2. 阻塞 IO 代码简单,但一个执行流等待时不能处理其他任务。
  3. 非阻塞 IO 会在数据未就绪时立即返回,单独忙轮询会浪费 CPU。
  4. select、poll、epoll 可以让一个执行流等待多个文件描述符,但它们仍属于同步 IO。
  5. 信号驱动 IO 在就绪时发送信号,异步 IO 则由内核完成等待和数据拷贝。
  6. 阻塞与非阻塞关注调用是否等待,同步与异步关注 IO 由谁完成。
  7. fcntl 可以为文件描述符添加 O_NONBLOCK,实际读取时要正确处理 EINTR、EAGAIN 和 EWOULDBLOCK。

【Linux】五种IO模型与非阻塞IO》 是转载文章,点击查看原文


相关推荐


【Python编程—从入门到实践】(Python条件判断完全入门:从布尔表达式到列表中的if实战)
承渊政道2026/9/5

🔥承渊政道:个人主页 ❄️个人专栏: 《C语言基础语法知识》 《数据结构与算法》 《C++知识内容》 《Linux系统知识》 《算法刷题指南》 《测评文章活动推广》 《大模型语言路线学习》 《MySQL数据库学习》 《Python知识内容》 《cpolar知识学习》 ✨逆境不吐心中苦,顺境不忘来时路!✨ 🎬 博主简介: 编程时经常需要检查一系列条件,并据此决定采取什么措施.在Python中,if语句让你能够检查程序的当前状态,并据此采取相应


GitHub 热榜项目 - 周榜(2026-08-22)
CoderJia_2026/8/28

GitHub 热榜项目 - 周榜(2026-08-22) 生成于:2026-08-22 统计摘要 共发现热门项目: 16 个 Token赞助:siliconflow 前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。 本期热点趋势总结 这周gtihub热榜,明显聚焦 AI 基础设施与本地化部署:Agent 记忆、上下文数据库、模型路由与多供应商兼容成为主线,配套出现小模型端侧推理、Mac / Apple Si


TDengine 第三方工具 — Telegraf、Kafka Connect、Flink、Spark
TDengine (老段)2026/8/20

分类:14.生态 | 篇章:04 第三方工具 免费详情 TDengine 通过 InfluxDB 兼容协议、JDBC、连接器等方式与主流数据生态对接。本文汇总 Telegraf、Kafka Connect、Flink、Spark、Logstash 等工具的集成方式。 集成方式速查 工具集成方式用途TelegrafInfluxDB output系统/IoT 采集collectdcollectd protocol服务器监控StatsDStatsD protocol应用指标Promethe


uni-app 项目目录结构全解:每个文件、每个文件夹的作用与配置详解
90后晨仔2026/8/7

📌 本文定位: 面向 iOS/Android/鸿蒙原生工程师,系统梳理 uni-app 项目的完整目录结构。基于 uni-app 官方文档 并大幅补充官方未覆盖的工程化细节、隐藏配置和实战经验。 一、标准项目目录全景图 使用 HBuilderX 或 CLI (npx degit dcloudio/uni-preset-vue#vite-ts my-project) 创建的标准项目结构如下: my-uni-app/ ├── pages/ # 📄 页面目


给 AI 装个“工具箱”:MCP 协议入门与 Node.js 实战
无情的西瓜皮2026/7/29

给 AI 装个"工具箱":MCP 协议入门与 Node.js 实战 你有没有想过一件事:AI 聊天模型很聪明,但它没办法直接读你的本地文件、查天气预报、更新 GitHub Issue,或者在数据库里跑一条 SQL。 这不是能力问题,是协议问题。大模型生在云端,活在自己的世界里。要让它们真正"动起来",你需要一个中间层——一个 AI 能理解和调用的标准接口。 MCP(Model Context Protocol)就是干这个的。它不是某个公司的私有方案,而是 Anthropic 推出来的开放协议,想


【Bug已解决】Forked thread token monitor over-accumulates usage after fork 解决方案
向哆哆2026/7/21

【Bug已解决】Forked thread token monitor over-accumulates usage after fork 解决方案 原始报错线索:Forked thread token monitor over-accumulates usage after fork(fork 出来的子进程里,那个统计 token 用量的监控线程,把用量算多了 / 重复累计)。 一、背景:fork 的语义陷阱 fork() 会几乎完整复制父进程的内存(写时复制,COW)。这意味着: 父进


【GitHub】Strix 深度解析:开源 AI 渗透测试工具的架构、原理与实战
怪侠说不说2026/7/13

当 AI 学会了黑客技能,安全测试的范式正在被彻底改写。 一、引言:安全测试的「自动驾驶」时代 传统的渗透测试(Pentest)面临着几个无解的痛点:周期长(动辄数周)、成本高(资深白帽人才稀缺)、误报多(静态扫描工具缺乏上下文理解)、覆盖窄(人为测试难以穷举攻击面)。一款名为 Strix 的开源项目正试图用 AI 多智能体协作的方式,把渗透测试带入"自动驾驶"时代。 Strix 在 GitHub 开源不到一年,已经斩获大量关注。它的核心理念非常直白:用 AI 代理(Agent)模


Opencode是怎么设计的
Worlds2026/7/5

一、前置基础:代码辅助工具的代际演进 在讲解具体架构前,先明确两个底层认知,帮你建立对 OpenCode 定位的正确理解: 两代代码辅助工具的本质区别代码 AI 工具经历了两个明显的代际演进,核心差异是「辅助补全」还是「自主执行」: 第一代:代码补全工具(如 GitHub Copilot),定位是「打字助手」,只能根据上下文生成片段代码,需要用户逐行确认、手动执行后续操作 第二代:代码智能体(Code Agent,如 OpenCode、Claude Code),定位是「任务执行者」,能够自


【C/C++】C 语言实现 WebSocket:握手、帧解析、掩码和回显
SilentSlot2026/6/27

【C/C++】C 语言实现 WebSocket:握手、帧解析、掩码和回显 1. WebSocket 为什么要先握手 WebSocket 不是一开始就直接发送二进制帧,它先通过 HTTP 发起升级请求。浏览器会发送类似这样的请求头: GET / HTTP/1.1 Host: 127.0.0.1:8080 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSoc


Makefile自动化编译实战项目
唐 城2026/6/18

有人说:一个人从1岁活到80岁很平凡,但如果从80岁倒着活,那么一半以上的人都可能不凡。 生活没有捷径,我们踩过的坑都成为了生活的经验,这些经验越早知道,你要走的弯路就会越少。  这是一份 Makefile 自动化编译实战项目资源包。这份指南从核心语法到企业级多目录架构,再到自动化依赖生成,带你彻底掌握 C/C++ 项目的构建自动化,告别手动敲 gcc 的低效时代。 📦 一、 项目目录结构规划 一个标准的工程化项目应具备清晰的目录划分,这是编写高级 Makefile 的基

首页编辑器站点地图

本站内容在 CC BY-SA 4.0 协议下发布

Copyright © 2026 聚合阅读