[Linux网络基础——Lesson12.「五种 IO 模型与非阻塞 IO」]



目录
前言
在计算机技术体系中,I/O(输入/输出)是贯穿硬件与软件、连接数据产生与消费的核心环节。从日常使用的终端交互到大规模分布式系统的数据传输,I/O操作的效率直接决定了整个系统的响应速度与承载能力。而网络通信作为现代应用的核心场景,其卡顿、延迟、吞吐量不足等问题,追根溯源往往都指向I/O层面的设计缺陷。
许多开发者在接触网络编程或系统优化时,常常被阻塞、非阻塞、同步、异步等概念困扰,也难以理解不同I/O模型的适用场景与效率差异。本文正是为解决这一痛点而生,以"重新理解I/O"为起点,从本质层面拆解I/O的核心逻辑,再通过餐厅点餐等生活化案例类比五种经典I/O模型,最后结合关键问题引发思考并聚焦非阻塞I/O的实践实现。
无论你是初入编程领域的新手,还是需要进行系统性能优化的资深开发者,本文都将为你搭建起I/O知识的清晰框架,帮助你从根源上理解I/O问题、掌握高效I/O的实现思路,为后续的技术实践提供坚实的理论支撑。
一 、😀 重新理解 IO
1-1 🍕为什么说网络问题的本质是 I/O 问题?
🐦 从数据流动看网络通信
- 网络通信的核心:数据在客户端与服务器之间**双向流动**。
- 客户端发送请求 → 输出(Write)
- 服务器接收请求 → 输入(Read)
- 服务器返回响应 → 输出(Write)
- 客户端接收响应 → 输入(Read)
- 每个步骤均涉及 I/O 操作:数据通过网卡、内核缓冲区、用户程序传递,本质是 跨层数据搬运。
🐦 网络 I/O 的瓶颈
- 延迟(Latency):数据从一端到另一端的传输时间(如物理距离、路由跳数)。
- 带宽(Bandwidth):单位时间内可传输的数据量上限。
- 并发(Concurrency):同时处理的连接数影响资源分配效率。
总结:
网络性能优化的核心是减少 I/O 等待时间和提升 I/O 吞吐量。
1-2 🍔 如何理解 I/O 的本质?
🔬 IO 就是 input,output,参照物是计算机本身,是计算机系统内部和外部设备进行交互的过程。
IO = 等+拷贝(等是主要矛盾)
🐳 等待外部设备就绪,当外部设备准备好了以后,通过 CPU 的针脚发送中断信号告知操作系统。操作系统转入内核态,进行拷贝工作。
- 等待(Waiting):等待数据就绪(如网络数据到达内核缓冲区、磁盘数据加载到内存)。
- 拷贝(Copying):将数据从内核缓冲区复制到用户空间(或反向)。
高效IO
上面说的IO=等待+拷贝在大多数情况下,时间都浪费在等待上面,因为和等待相比,拷贝要花的时间比等待的时间少的多。
- 高效 IO 的核心:减少等待时间的浪费,而非单纯优化拷贝速度。
等待的分类
- 主动等待:进程阻塞直到数据就绪(如阻塞式
read())。- 被动等待:进程通过轮询或事件通知检查状态(如非阻塞 IO +
epoll)。示例:
- 网络请求:客户端等待服务器响应的 RTT(Round-Trip Time)属于被动等待。
- 数据库查询:从磁盘读取数据时,CPU 因 IO 阻塞而空闲属于主动等待。
1-3、🍟什么是高效的 I/O?
⚡🧙 任何通信场景,IO 通信效率一定是有上限的,毕竟 花盆里长不出参天大树(受硬件限制)
IO效率低 的原因主要有以下几点:
- 等待时间:IO操作通常涉及与外部设备的交互,这些设备的速度远低于CPU和内存。例如,硬盘的读写速度比内存慢几个数量级,网络传输的速度也受带宽和延迟的限制。因此,程序在等待IO操作完成时会浪费大量时间。
- 上下文切换:在阻塞IO中,操作系统需要将等待IO的进程挂起,并切换到其他进程执行。这种上下文切换会消耗额外的CPU资源,降低整体效率,
- 资源竞争:在高并发环境下,多个进程或线程可能同时请求IO操作,导致资源竞争和排队,进一步增加等待时间。
🐦 高效 I/O 的目标
- 最大化 CPU 利用率:减少进程因等待 I/O 而阻塞的时间。
- 最小化延迟:快速响应每个 I/O 请求。
- 最大化吞吐量:单位时间内处理更多 I/O 操作。
🐦 实现高效 I/O 的策略
策略 1:减少阻塞等待
- 非阻塞 I/O:轮询检查数据是否就绪,避免进程挂起。
- 代价:频繁轮询可能导致 CPU 空转。
- 多路复用(如
epoll):单线程监控多个 I/O 事件,仅处理就绪的描述符。
- 优势:适合高并发网络服务(如 Web 服务器)。
策略 2:批量处理 I/O
- 缓冲(Buffering):累积多个小数据包后一次性处理,减少系统调用次数。
- 示例:TCP 协议的 Nagle 算法合并小数据包。
策略 3:异步化与并行化
- 异步 I/O(如
io_uring):内核全程处理 I/O,完成后通知进程。- 多线程/进程:为每个连接分配独立执行单元(但需权衡上下文切换开销)
二:😁 五种 IO 模型
在了解相关知识之前,我们先来看个例子,方便我们对其的理解
2-1🍿 生动例子:餐厅点餐
角色定义
- 进程:顾客(发起 I/O 请求的主体)。
- 文件描述符:订单号(标识一个 I/O 请求)。
- 数据:顾客点的餐(需要处理的内容)。
1.1 阻塞 I/O
- 场景:
顾客下单后,一直坐在餐桌前等待,直到服务员端上菜才能做其他事(如玩手机)。- 关键点:
- 顾客(进程)在等待期间完全被阻塞。
- 订单号(文件描述符)对应唯一的请求。
1.2 非阻塞 I/O
- 场景:
顾客下单后,每隔 5 秒去厨房问一次“我的菜好了吗?”,期间可以喝水、聊天。- 关键点:
- 顾客(进程)需要主动轮询状态。
- 若厨房(内核)回答“没好”,顾客继续做其他事。
1.3 信号驱动 I/O
- 场景:
顾客下单后,留下手机号给服务员,继续聊天。厨房准备好菜时,服务员打电话通知顾客取餐。- 关键点:
- 数据就绪时内核(服务员)通过信号(电话)通知进程(顾客)。
- 顾客仍需自己从厨房端走菜(同步拷贝数据)。
1.4 多路转接 I/O
- 场景:
顾客同时点了咖啡和蛋糕,告诉大堂经理“两样都好了叫我”。经理一直监听多个订单,任一就绪时通知顾客。- 关键点:
- 一个进程(顾客)通过多路复用接口(经理)监控多个文件描述符(订单)。
- 仍需顾客自己取餐(同步拷贝数据)。
1.5 异步 I/O
- 场景:
顾客下单后,继续办公。厨房准备好菜后,服务员直接将菜端到顾客桌上,并说“您的菜齐了”。- 关键点:
- 数据准备和端菜(拷贝)全程由内核(服务员)完成。
- 顾客(进程)无需参与任何等待或操作。
2-2🧂 专业术语介绍
🐦阻塞 I/O (Blocking I/O)
- 进程发起 I/O 操作后,立即进入阻塞状态,(在内核将数据准备好之前,系统调用一直等待)直到内核将数据准备好并拷贝到用户空间后,进程才恢复执行。
- 所有的套接字默认是阻塞方式
- 同步 I/O:进程全程需要等待数据就绪和拷贝完成。
🐦非阻塞 I/O (Non-blocking I/O)
- 进程发起 I/O 操作后,内核立即返回一个状态值(未就绪),进程通过轮询(Polling) 反复检查数据是否就绪,期间可以执行其他任务。
- 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回
EWOULDBLOCK错误码.- 轮询:意指程序员循环的方式反复尝试读写文件描述符。这对 CPU 来说是较大的浪费, 一般只有特定场景下才使用.
- 同步 I/O:进程需要主动检查数据状态并完成拷贝。
🐦信号驱动 I/O (Signal-driven I/O)
- 进程发起 I/O 操作后,内核在数据就绪时发送信号(如
SIGIO) 通知进程,进程随后执行数据拷贝。- 同步 I/O:数据拷贝阶段仍需进程主动完成。
🐦多路转接 I/O (Multiplexing I/O)
- 进程通过
select、poll或epoll同时监控多个文件描述符,当任一描述符数据就绪时,内核通知进程进行处理。- 同步 I/O:数据就绪后仍需进程主动拷贝数据。
虽然从流程图上看起来和阻塞 IO 类似实际上最核心在于 IO 多路转接 能够同时等待多个文件描述符的就绪状态.
5.🐦异步 I/O (Asynchronous I/O)
- 进程发起 I/O 操作后,内核 全程负责数据准备和拷贝,完成后通过回调(如信号或回调函数)通知进程。
- 异步 I/O:进程无需参与数据准备或拷贝。
2-3🥓 总结表
- 同步 I/O:
- 阻塞 I/O、非阻塞 I/O、信号驱动 I/O、多路转接 I/O。
- 共同点:数据拷贝阶段需进程主动完成(即使通过信号或轮询触发)。
- 异步 I/O:
- 数据准备和拷贝全程由内核处理,进程无需参与。
I/O 模型 同步/异步 例子类比 进程角色 阻塞 I/O 同步 干等上菜 全程阻塞 非阻塞 I/O 同步 轮询询问厨房 主动轮询 信号驱动 I/O 同步 电话通知取餐 被动响应信号 多路转接 I/O 同步 经理监听多个订单 批量监听 异步 I/O 异步 服务员直接端菜到桌 完全无需参与
三、😂 思考
3-1🌭 阻塞 vs 非阻塞,非阻塞效率效率一定高吗?
答案:
不一定,非阻塞 I/O 的效率取决于具体场景。
- 非阻塞 I/O 的优势:
进程在等待数据就绪期间可以执行其他任务(避免完全阻塞),适合需要同时处理多任务的场景。
例子:餐厅顾客边等餐边聊天(非阻塞)比干等的顾客(阻塞)更高效。- 非阻塞 I/O 的劣势:
如果频繁轮询(如每秒检查 1000 次),会导致 CPU 资源浪费,甚至比阻塞 I/O 效率更低。
例子:顾客每隔 1 秒就去厨房问一次,导致自己无法专心聊天,服务员也被频繁打扰。结论:
- 低并发场景:阻塞 I/O 更简单高效(避免轮询开销)。
- 高并发场景:非阻塞 I/O + 多路复用(如
epoll)效率更高(避免大量线程阻塞)
3-2🥚 五种模型中,谁的 I/O 效率最高?
答案:
异步 I/O(如 Linux 的io_uring)理论效率最高,但实际中 多路复用 I/O(如epoll) 在同步模型中更常用。
- 异步 I/O:
- 优势:内核全程处理数据准备和拷贝,进程完全无需等待(服务员直接端菜到桌)。
- 限制:依赖操作系统和硬件的支持(如 Linux 的异步 I/O 实现复杂)。
- 多路复用 I/O(
epoll):
- 优势:单线程监控大量文件描述符,避免进程/线程频繁切换(大堂经理统一管理订单)。
- 场景:高并发网络服务器(如 Nginx、Redis)的核心模型。
总结:
- 异步 I/O 理论最优,但实际中多路复用 I/O 因兼容性和成熟度更常用
- 异步 I/O 的高效主要和其特点无关,还是得依靠程序员自己,而多路复用 I/O 可以用于处理大批网络数据,降低了等的比重
因此总的来说,我们更认为 多路复用的 I/O 效率更高
3-3🍳 同步通信 vs 异步通信
同步 和 异步 已关注的是消息通信机制.
- 所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回. 但是一旦调用返回,就得到返回值了;
- 换句话说,就是由调用者主动等待这个调用的结果;
- 异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果;
- 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.
另外:之前我们在讲 多进程多线程 的时候,也提到同步和互斥。
注意:这里的同步通信和进程之间的同步是完全不相干的概念.
- 进程/线程同步 也是进程/线程之间直接的制约关系
- 是为完成某种任务而建立的两个或多个线程,这个线程需要在某些位置上协调
他们的工作次序而等待、传递信息所产生的制约关系。尤其是在访问临界资源的时候.因此以后在看到 “同步” 这个词,一定要先搞清楚大背景是什么。这个同步是同步通信异步通信的同步, 还是同步与互斥的同步
四:😃 非阻塞 IO
4-1🧇 fcntl
一个文件描述符, 默认都是阻塞 IO.
函数原型如下.
NAME fcntl - manipulate file descriptor SYNOPSIS #include <unistd.h> #include <fcntl.h> int fcntl(int fd, int cmd, ... /* arg */ );cmd 是命令,是要操作的类型。主要的操作类型有:
- 获取,设置文件状态信息:cmd=F_GETFL,F_SETFL。
- 复制现有的描述符,cmd=F_DUPFD。
- 获取,设置异步IO所有权,cmd=F_GETOWN,F_SETOWN。
- 获取、设置记录锁,cmd=F_GETLK,F_SETLK,F_SETLKW。
🧊 我们此处只是用第三种功能,
获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞. 文件状态标志包括O_APPEND、O_NONBLOCK。
4-2🥞 实现函数 SetNonBlock
⚙️ 基于
fcntl, 我们实现一个SetNoBlock函数, 将文件描述符设置为非阻塞.#include <unistd.h> #include <fcntl.h> // 让文件描述符非阻塞 void SetNonBlock(int fd) { int f1 = fcntl(fd, F_GETFL); if(f1 < 0) { perror("fcntl"); return ; } fcntl(fd, F_SETFL, f1 | O_NONBLOCK); // O_NONBLOCK 让fd 以非阻塞的方式进行工作 }
- 使用 F_GETFL 将当前的文件描述符的属性取出来 (这是一个位图).
- 然后再使用 F_SETFL 将文件描述符设置回去. 设置回去的同时, 加上一个 O_NONBLOCK 参数.
4-3🧈 非阻塞方式读取标准输入
#include <iostream> #include <cstdio> #include <cerrno> #include <string> #include <unistd.h> #include <fcntl.h> // 让文件描述符非阻塞 void SetNonBlock(int fd) { int f1 = fcntl(fd, F_GETFL); if(f1 < 0) { perror("fcntl"); return ; } fcntl(fd, F_SETFL, f1 | O_NONBLOCK); // O_NONBLOCK 让fd 以非阻塞的方式进行工作 } int main() { std::string tips = "Please Enter# "; char buffer[1024]; SetNonBlock(0); while(true) { write(0, tips.c_str(), tips.size()); // 非阻塞,如果我们不做输入,数据不就绪,以出错形式返回!! // read 不是有读取失败(-1)吗?失败vs底层数据没就绪 -> 底层数据没就绪,不算失败 // 如果是 -1, 失败vs底层数据没就绪我们后续的做法是不同的! // read -> -1, 失败vs底层数据没就绪 -> 需要区分的必要性的! // errno 表示:更详细的出错原因, 最近一次调用,出错的时候的出错码 int n = read(0, buffer, sizeof(buffer)); if(n > 0) { buffer[n] = 0; std::cout << "echo# " << buffer << std::endl; } else if(n == 0) { std::cout << "read file end" << std::endl; break; } else { // EAGAIN 11 /* Try again */ // EWOULDBLOCK EAGAIN /* Operation would block */ if(errno == EAGAIN || errno == EWOULDBLOCK) { // 做其他事情呢? std::cout << "底层数据,没有就绪" << std::endl; sleep(1); continue; } else if (errno == EINTR) { std::cout << "被中断, 重新来" << std::endl; sleep(1); continue; } else { std::cout << "read error: " << n << ", errno: " << errno << std::endl; } } } return 0; }
🔥提炼与总结🔥
本文围绕I/O模型构建"本质认知-模型对比-关键思考-实践落地"知识闭环,核心总结如下:
本质认知:网络问题核心是I/O问题——通信本质是数据流动,I/O的等待(就绪、拷贝)是性能瓶颈;I/O实质是数据在用户/内核空间的传输,高效I/O核心是减等待、提传输,策略包括优化态切换、减少无效等待。
模型对比:以餐厅点餐类比五种模型,核心差异在"就绪通知方式"与"拷贝时机":阻塞I/O全程等待;非阻塞I/O主动查询;I/O多路复用集中监控;信号驱动I/O被动通知;异步I/O全程无阻塞(理论最优但复杂)。
关键认知:①非阻塞效率非绝对,需合理轮询否则耗资源;②无最优模型,需结合场景(如高并发用I/O多路复用);③同步/异步关注"是否等操作完成",阻塞/非阻塞关注"等待时是否挂起",分属不同维度。
实践落地:以非阻塞I/O为例,通过fcntl设非阻塞属性,给出SetNonBlock实现思路,结合标准输入读取案例演示应用。
核心逻辑:I/O优化关键在"减等待、优传输",需结合并发量、延迟需求、实现成本选模型与策略,方可提升系统性能。
结束语
以上是我对于【Linux网络编程】「五种 IO 模型与非阻塞 IO」的理解
感谢您的三连支持!!!


更多推荐








所有评论(0)