当前位置: 首页 > news >正文

【Linux】Linux高级I/O

参考博客:https://blog.csdn.net/sjsjnsjnn/article/details/128345976

一、五种IO模型

  • 阻塞式I/O
  • 非阻塞式I/O
  • I/O复用(多路转接)
  • 信号驱动式I/O
  • 异步I/O

I/O我们并不陌生,简单的说就是输入输出;对于一个输入操作通常包括两个不同的阶段:

  1. 等待数据准备好
  2. 从内核向进程复制数据

对于一个套接字上的输入操作,第一步通常涉及等待数据从网络中到达,然后被复制到内核的某个缓冲区;第二步就是把数据从内核缓冲区复制到应用进程的缓冲区。

二、阻塞式I/O

2.1 概念

最流行的I/O模型是阻塞式I/O模型,我们之前的博客中基本都采用的是阻塞式I/O(因为默认情况下所有的套接字都是阻塞的)。

在这里插入图片描述

  • 阻塞式 IO 是一种 同步 IO 模型,当进程 / 线程发起 IO 操作(如 read/write 系统调用)时,若数据未准备好(或无法立即完成操作),发起操作的进程 / 线程会被操作系统 “挂起”(阻塞),无法执行其他任务,直到 IO 操作完成(数据就绪、读写完毕等)才会解除阻塞,继续执行后续逻辑。

  • 简单说:“IO 没完成,进程 / 线程就干等,啥也做不了”。

2.2 典型流程(以网络 read 为例)

以从套接字读取数据(recv/read 系统调用)为例,阻塞式 IO 的完整过程:

  1. 发起 IO 请求:进程调用 read 尝试读取数据。
  2. 内核等待数据:若内核缓冲区中没有可用数据(比如网络数据还没收到),内核会进入 “等待数据就绪” 状态。
  3. 进程阻塞:此时,发起 read 的进程会被操作系统标记为 “阻塞态”,从 CPU 调度队列中移除,无法执行任何代码。
  4. 数据就绪 & 拷贝:当内核拿到数据(如网络包到达),会将数据从 内核空间拷贝到用户空间
  5. 解除阻塞,返回结果:数据拷贝完成后,操作系统唤醒进程,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 的完整过程:

  1. 设置非阻塞模式:通过 fcntlioctl 等函数,将文件描述符(如套接字)标记为 “非阻塞”。
  2. 发起 IO 请求:进程调用 read 尝试读取数据。
  3. 判断数据是否就绪
    • 若内核缓冲区有数据,则正常读取,read 返回实际读取的字节数,进程继续处理数据。
    • 若内核缓冲区无数据(IO 未就绪),read 立即返回 -1,并设置 errnoEAGAINEWOULDBLOCK,表示 “操作暂时无法完成,建议稍后重试”。
  4. 进程执行其他任务:因 read 未阻塞,进程可去处理其他逻辑(如响应其他请求、执行计算任务等)。
  5. 轮询 / 事件驱动重试:进程可通过 “定时轮询”(主动再次调用 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 复用的典型流程:

  1. 创建监听套接字:绑定端口并监听连接请求。
  2. 注册监听事件:将监听套接字和所有客户端连接的套接字添加到复用器(如select/poll/epoll)。
  3. 等待事件就绪:线程调用复用器的系统调用(如select()),进入阻塞状态,等待任意文件描述符就绪。
  4. 处理就绪事件
    • 若监听套接字就绪 → 接受新连接并注册到复用器。
    • 若客户端套接字就绪 → 读取 / 写入数据。
  5. 循环监听:继续等待下一批事件。

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 的典型流程:

  1. 创建套接字并设置:创建套接字后,设置为非阻塞模式(通常配合使用)。
  2. 注册信号处理函数:使用signal()sigaction()注册SIGIO信号的处理函数。
  3. 设置进程为 I/O 的属主:通过fcntl()设置文件描述符的属主进程,确保信号能正确发送到该进程。
  4. 启用异步通知:通过fcntl()设置FASYNC标志,开启信号驱动模式。
  5. 继续执行其他任务:进程可以继续执行其他逻辑,无需阻塞等待 I/O。
  6. 接收信号并处理:当 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 的典型流程:

  1. 准备 I/O 请求:进程创建异步 I/O 控制块(如struct aiocb),设置文件描述符、缓冲区、偏移量等参数。
  2. 提交请求:调用aio_read()aio_write()提交异步 I/O 请求,立即返回。
  3. 继续执行其他任务:进程无需等待,可继续执行其他逻辑。
  4. 内核处理 I/O:内核在后台将数据从磁盘读入用户缓冲区(或从用户缓冲区写入磁盘)。
  5. 完成通知:I/O 操作完成后,内核通过以下方式通知进程:
    • 发送信号(如SIGIO或自定义信号)。
    • 调用预先注册的回调函数(通过aio_suspend()等待)。
  6. 处理结果:进程在信号处理函数或回调中检查 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复用。

更多资料:https://github.com/0voice

http://www.xdnf.cn/news/986941.html

相关文章:

  • 循环中的break和continue
  • Redis免费客户端工具推荐
  • Altair:用Python玩转声明式可视化(新手友好向)
  • C#委托代码记录
  • 推荐系统入门最佳实践:Slope One 算法详解与完整实现
  • 记录下blog的成长过程
  • 我的世界进阶模组开发教程——制作机械动力附属模组
  • MySQL存储引擎--深度解析
  • Go 语言 JWT 深度集成指南
  • 什么是哈希函数
  • C语言——深入解析字符串函数与其模拟实现
  • const auto 和 auto
  • Bash 脚本中的特殊变量
  • python使用SQLAlchemy 库操作本地的mysql数据库
  • python基本语法元素
  • python-docx 库教程
  • Oracle中10个索引优化
  • 美团NoCode中的Dev Mode 使用指南
  • 在windows中安装或卸载nginx
  • spring boot源码和lib分开打包
  • 遍历 unordered_map
  • GFS 分布式文件系统
  • UE_Event Any Damage和OnTake Any Damage
  • JAVA CAS 详解
  • Docker完整教程 - 从入门到SpringBoot实战
  • JSON5 模块的作用与区别
  • 图标异常问题
  • 【Linux】进程控制(下)---程序替换宝藏岛
  • 如何排查PHP-FPM进程CPU占用100%的间歇性问题 (2025)
  • Unity 服务器交互开发指南