参考博客:https://blog.csdn.net/sjsjnsjnn/article/details/128345976
一、五种IO模型
- 阻塞式I/O
- 非阻塞式I/O
- I/O复用(多路转接)
- 信号驱动式I/O
- 异步I/O
I/O我们并不陌生,简单的说就是输入输出;对于一个输入操作通常包括两个不同的阶段:
- 等待数据准备好
- 从内核向进程复制数据
对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达,然后被复制到内核的某个缓冲区;第二步就是把数据从内核缓冲区复制到应用进程的缓冲区。
二、阻塞式I/O
2.1 概念
最流行的I/O模型是阻塞式I/O模型,我们之前的博客中基本都采用的是阻塞式I/O(因为默认情况下所有的套接字都是阻塞的)。
阻塞式 IO 是一种 同步 IO 模型,当进程 / 线程发起 IO 操作(如
read
/write
系统调用)时,若数据未准备好(或无法立即完成操作),发起操作的进程 / 线程会被操作系统 “挂起”(阻塞),无法执行其他任务,直到 IO 操作完成(数据就绪、读写完毕等)才会解除阻塞,继续执行后续逻辑。简单说:“IO 没完成,进程 / 线程就干等,啥也做不了”。
2.2 典型流程(以网络 read
为例)
以从套接字读取数据(recv
/read
系统调用)为例,阻塞式 IO 的完整过程:
- 发起 IO 请求:进程调用
read
尝试读取数据。 - 内核等待数据:若内核缓冲区中没有可用数据(比如网络数据还没收到),内核会进入 “等待数据就绪” 状态。
- 进程阻塞:此时,发起
read
的进程会被操作系统标记为 “阻塞态”,从 CPU 调度队列中移除,无法执行任何代码。 - 数据就绪 & 拷贝:当内核拿到数据(如网络包到达),会将数据从 内核空间拷贝到用户空间。
- 解除阻塞,返回结果:数据拷贝完成后,操作系统唤醒进程,
read
系统调用返回,进程继续执行后续逻辑。
整个过程中,“等待数据就绪” 和 “数据拷贝” 两个阶段,进程都会处于阻塞状态(无法干其他事)。
2.3 特点与优缺点
优点
- 实现简单:无需复杂的轮询、事件监听逻辑,代码直观易写(比如简单的服务器 / 客户端模型,直接用
accept
/recv
/send
即可)。 - 逻辑清晰:适合对实时性要求不高、连接数少的场景,开发调试成本低。
缺点
- 资源浪费:进程 / 线程阻塞期间,会占用系统资源(如线程栈内存),且无法处理其他任务。高并发场景下,大量阻塞线程会导致系统资源被占满,性能急剧下降。
- 响应不及时:若 IO 操作耗时久(如磁盘读写慢、网络延迟高),阻塞会导致整个进程 / 线程 “卡壳”,无法响应其他请求。
2.4. 适用 & 不适用场景
适用场景
- 连接数少、数据量大:比如数据库备份程序(少连接、但需传输大量数据),用阻塞 IO 可简化代码,无需处理复杂的并发逻辑。
- 对实时性要求低:如后台脚本(日志归档、文件同步),阻塞等待的代价可接受。
- 开发调试阶段:快速实现功能原型时,阻塞 IO 代码简洁,便于验证逻辑。
不适用场景
- 高并发场景:如 Web 服务器需同时处理 thousands 连接,阻塞 IO 会导致线程爆炸,资源耗尽。
- 低延迟要求场景:如实时通信(IM、音视频),阻塞等待会导致消息延迟、卡顿。
三、非阻塞式I/O
3.1 概念
对于非阻塞式I/O模型,就是进程把一个套接字设置成非阻塞,本质上就是在通知内核:当所有请求的I/O操作非得把本进程投入睡眠才能完成时,请不要把本进程投入睡眠,而是返回一个错误。
非阻塞 IO 是 同步 IO 模型(注意和 “异步 IO” 区分),当进程 / 线程发起 IO 操作(如
read
/write
)时,若数据未准备好(或无法立即完成),系统调用会立即返回特定状态(如错误码EAGAIN
/EWOULDBLOCK
),不会阻塞进程 / 线程。进程可继续执行其他逻辑,或通过 “轮询”“事件通知” 等方式,后续再尝试 IO 操作。简单说:“IO 没就绪,操作立即返回;进程不阻塞,自己决定后续咋处理”。
3.2 典型流程(以网络 read
为例)
以从套接字读取数据(recv
/read
系统调用)为例,非阻塞 IO 的完整过程:
- 设置非阻塞模式:通过
fcntl
或ioctl
等函数,将文件描述符(如套接字)标记为 “非阻塞”。 - 发起 IO 请求:进程调用
read
尝试读取数据。 - 判断数据是否就绪:
- 若内核缓冲区有数据,则正常读取,
read
返回实际读取的字节数,进程继续处理数据。 - 若内核缓冲区无数据(IO 未就绪),
read
立即返回 -1,并设置errno
为EAGAIN
或EWOULDBLOCK
,表示 “操作暂时无法完成,建议稍后重试”。
- 若内核缓冲区有数据,则正常读取,
- 进程执行其他任务:因
read
未阻塞,进程可去处理其他逻辑(如响应其他请求、执行计算任务等)。 - 轮询 / 事件驱动重试:进程可通过 “定时轮询”(主动再次调用
read
)或 “事件通知”(如结合select
/poll
/epoll
),在数据就绪后重新发起read
操作。
3.3 特点与优缺点
优点
- 高并发支持:单进程 / 线程可同时处理多个 IO 操作(通过轮询或事件驱动),无需为每个 IO 单独开线程,减少线程切换开销,提升资源利用率。
- 实时响应:进程不会因某一个 IO 未就绪而 “卡壳”,可及时处理其他任务(如高并发服务器中,同时响应多个客户端请求)。
- 灵活性强:进程可自主控制重试时机(比如结合业务逻辑,决定多久后再次尝试 IO 操作)。
缺点
- 轮询开销:若单纯用 “忙轮询”(频繁调用
read
检查数据),会持续占用 CPU,导致资源浪费。需结合select
/poll
/epoll
等 “IO 多路复用” 机制优化。 - 编程复杂度高:需手动处理 “IO 未就绪” 的返回状态,编写重试逻辑、错误处理,代码比阻塞 IO 复杂。
- 部分场景效率低:若 IO 操作频繁且大部分时间未就绪,“轮询 + 重试” 可能比阻塞 IO 更耗时(需平衡重试间隔、事件通知机制)。
3.4 适用 & 不适用场景
适用场景
- 高并发网络编程:如 Web 服务器(Nginx 就大量用非阻塞 IO + 多路复用)、即时通讯(IM)、实时音视频,需同时处理成千上万个连接。
- 事件驱动架构:搭配
epoll
/kqueue
等机制,实现高效的 “事件循环”(如 Redis 单线程模型,靠非阻塞 IO + 事件驱动支撑高并发)。 - 需要 “边等 IO 边干活”:进程在等待数据时,还想处理其他任务(如后台任务调度、定时任务)。
不适用场景
- 简单低并发任务:如普通命令行工具、简单文件读写,用阻塞 IO 更简单,没必要引入非阻塞的复杂度。
- 无法有效减少轮询:若业务逻辑中,IO 就绪频率极低,但又必须频繁重试,非阻塞 IO 会因 “空转轮询” 浪费 CPU。
四、I/O复用(多路转接)
4.1 概念
有了I/O复用(多路转接),我们就可以调用select或poll或epoll,阻塞在这三个系统调用的某一个之上,而不是阻塞在真正的I/O系统调用上。
I/O 复用(I/O Multiplexing)是 Linux 中一种高效处理多连接的技术,也被称为 “多路转接”。它允许单个线程同时监控多个 I/O 事件源,当某个事件源就绪时,再进行相应处理。这种模型特别适合高并发场景,是现代高性能服务器(如 Nginx、Redis)的核心技术之一。
I/O 复用的本质是:使用一个线程 / 进程,通过系统调用(如select、poll、epoll)同时监听多个文件描述符(如套接字、管道)的 I/O 事件,当有事件就绪时,再执行对应的处理逻辑。
常见的 I/O 事件包括:
- 可读事件:文件描述符有数据可读(如 TCP 连接收到数据)。
- 可写事件:文件描述符可以写入数据(如 TCP 缓冲区可写入)。
- 异常事件:文件描述符发生错误(如连接断开)。
4.2 工作流程
以网络服务器为例,I/O 复用的典型流程:
- 创建监听套接字:绑定端口并监听连接请求。
- 注册监听事件:将监听套接字和所有客户端连接的套接字添加到复用器(如
select
/poll
/epoll
)。 - 等待事件就绪:线程调用复用器的系统调用(如
select()
),进入阻塞状态,等待任意文件描述符就绪。 - 处理就绪事件:
- 若监听套接字就绪 → 接受新连接并注册到复用器。
- 若客户端套接字就绪 → 读取 / 写入数据。
- 循环监听:继续等待下一批事件。
4.3 适用场景
- 高并发连接:如 Web 服务器(Nginx)、即时通讯(IM)、游戏服务器。
- 连接多但活跃少:例如 10 万连接,但同时活跃的只有 1000 个,epoll优势明显。
- 单线程 / 进程处理多连接:避免创建大量线程导致的上下文切换开销。
- 低延迟要求:通过事件驱动方式快速响应 IO 就绪事件。
五、信号驱动式I/O
5.1 概念
我们也可以用信号,让内核在描述符就绪时发送SIGIO信号通知我们。我们称这种模型为信号驱动式I/O模型。
信号驱动式 I/O(Signal-Driven I/O)是 Linux 中一种异步通知机制,允许进程在 I/O 操作就绪时通过信号接收通知,而不必主动轮询或阻塞等待。这种模型特别适合需要及时响应 I/O 事件但又不想阻塞主线程的场景。
信号驱动式 I/O 的本质是:进程预先向内核注册一个信号处理函数,当特定的 I/O 事件发生时,内核通过发送信号(通常是SIGIO)通知进程,进程在信号处理函数中执行相应的 I/O 操作。
5.2 工作流程
以网络套接字为例,信号驱动 I/O 的典型流程:
- 创建套接字并设置:创建套接字后,设置为非阻塞模式(通常配合使用)。
- 注册信号处理函数:使用
signal()
或sigaction()
注册SIGIO
信号的处理函数。 - 设置进程为 I/O 的属主:通过
fcntl()
设置文件描述符的属主进程,确保信号能正确发送到该进程。 - 启用异步通知:通过
fcntl()
设置FASYNC
标志,开启信号驱动模式。 - 继续执行其他任务:进程可以继续执行其他逻辑,无需阻塞等待 I/O。
- 接收信号并处理:当 I/O 事件就绪(如数据可读)时,内核发送
SIGIO
信号,进程在信号处理函数中执行 I/O 操作。
5.3 适用场景
- 需要及时响应 I/O 事件:如实时监控系统、网络设备驱动程序。
- 不希望阻塞主线程:主程序需要继续执行其他任务,I/O 事件通过信号异步通知。
- 连接数较少但需要异步处理:相比 I/O 复用,信号驱动 I/O 更适合连接数较少的场景。
- 硬件交互:与硬件设备(如串口、网卡)进行交互时,可通过信号驱动模式及时获取数据。
5.4 优缺点
优点
- 异步通知:无需主动轮询或阻塞等待,提高 CPU 利用率。
- 及时响应:I/O 事件发生时立即通过信号通知,延迟较低。
- 编程简单:相比 I/O 复用,信号驱动 I/O 的实现更直观,无需维护复杂的事件循环。
缺点
- 信号丢失风险:如果信号处理函数执行时间过长,可能会丢失后续信号。
- 信号处理限制:信号处理函数只能调用异步信号安全的函数,功能受限。
- 连接数限制:每个文件描述符都需要独立的信号处理,不适合大量连接的场景。
- 平台兼容性:在不同操作系统上实现可能有差异,Linux 和 BSD 系统支持较好。
六、异步I/O
6.1 概念
异步I/O的工作机制:告知内核启动某个操作,并让内核在整个操作(包括将数据从内核复制到我们自己的缓冲区)完成后通知我们。这种模型与信号驱动I/O模型的主要区别在于:信号驱动式I/O是有内核通知我们何时可以启动一个I/O操作,而异步I/O模型是由内核通知我们I/O操作何时完成。
异步 I/O(Asynchronous I/O)是 Linux 中最高效的 I/O 模型,它允许进程在发起 I/O 操作后无需等待完成,继续执行其他任务,内核会在 I/O 操作全部完成后通过回调或信号通知进程。这种模型特别适合需要处理大量并发 I/O 但对延迟要求极高的场景。
异步 I/O 的本质是:进程发起 I/O 操作后立即返回,内核在后台完成数据读写,操作完成后通过回调函数或信号通知进程。整个过程中,进程无需阻塞或主动检查 I/O 状态。
6.2 工作流程
以文件读取为例,异步 I/O 的典型流程:
- 准备 I/O 请求:进程创建异步 I/O 控制块(如
struct aiocb
),设置文件描述符、缓冲区、偏移量等参数。 - 提交请求:调用
aio_read()
或aio_write()
提交异步 I/O 请求,立即返回。 - 继续执行其他任务:进程无需等待,可继续执行其他逻辑。
- 内核处理 I/O:内核在后台将数据从磁盘读入用户缓冲区(或从用户缓冲区写入磁盘)。
- 完成通知:I/O 操作完成后,内核通过以下方式通知进程:
- 发送信号(如
SIGIO
或自定义信号)。 - 调用预先注册的回调函数(通过
aio_suspend()
等待)。
- 发送信号(如
- 处理结果:进程在信号处理函数或回调中检查 I/O 结果。
6.3 优缺点
优点
- 最高性能:I/O 操作完全由内核异步处理,进程无需等待,CPU 利用率最大化。
- 低延迟:I/O 完成后立即通知进程,延迟最小。
- 资源高效:无需为每个 I/O 操作创建线程 / 进程,减少内存和 CPU 开销。
- 真正的并行:计算任务和 I/O 操作可完全并行执行。
缺点
- 编程复杂:API 使用难度大,需要处理回调或信号,调试困难。
- 平台兼容性差:不同操作系统实现差异大,POSIX AIO 在某些系统上性能不佳。
- 文件系统限制:Linux Native AIO 只支持特定文件系统(如 XFS)和直接 I/O(O_DIRECT)。
- 缓冲区对齐要求:使用直接 I/O 时,缓冲区必须按页对齐,增加编程复杂度。
- 错误处理复杂:异步操作的错误处理和恢复机制更复杂。
适用场景
- 高性能存储系统:如数据库、文件系统,需要处理大量并发 I/O 请求。
- 实时数据处理:如流媒体服务器、金融交易系统,对延迟要求极高。
- 网络代理 / 转发:如高性能代理服务器、CDN 节点,需快速转发数据。
- 多任务并行处理:应用程序需要同时执行计算任务和 I/O 操作。
- 对资源利用率要求高:避免创建大量线程 / 进程处理 I/O,减少上下文切换开销。
七、五种I/O模型的比较
如下图所示,前4种模型主要区别在于第一阶段,因为他们第二阶段都是一样的:在数据从内核复制到调用者的缓冲区期间,进程阻塞于recvfrom调用。相反,异步I/O模型在这两个阶段都要处理,从而不同于其他4种模型。
I/O和异步I/O的比较:
- 同步I/O: 导致请求进程阻塞,直到I/O操作完成;
- 异步I/O: 不导致请求进程阻塞;
简单的讲:就是是否参与了I/O操作
- 前四种I/O模型——阻塞式I/O模型、非阻塞式I/O模型、I/O复用(多路转接)和信号驱动式I/O模型都是同步I/O,因为其中真正的I/O操作(recvfrom)将进程阻塞。只有异步I/O模型是异步I/O。
八、I/O复用典型使用在下列网络应用场合
- 当客户处理多个描述符时,必须使用I/O复用;
- 如果一个TCP服务器既要处理监听套接字,又要处理已连接的套接字,一般就要使用I/O复用;
- 如果一个服务器既要处理TCP,又要处理UDP,一般就要使用I/O复用。
- 如果一个服务器要处理多个协议或多个服务,一般就需要使用I/O复用。