【Linux】五种IO模型详解

一、 IO 的本质与五种 IO 模型

1.1 IO 的核心概念与本质

  • 概念解释IO (Input/Output) 在计算机科学中指数据在内存与外部设备(如磁盘、网络)之间的流动过程。在操作系统层面,IO = 等待 + 拷贝 。无论是传统的 read / write 还是网络的 recv / send / recvfrom / sendto,任何 IO 过程都包含这两个步骤:第一步是等待数据准备就绪,第二步是将数据从内核空间拷贝到用户空间(或反之)。
  • 笔记
  • 在实际应用场景中,等待消耗的时间往往远远高于拷贝的时间
  • 核心优化结论 :让 IO 更高效,最核心的办法就是让等待的时间尽量少
  • 对于文件描述符 (fd),一般默认情况下:读事件默认是不就绪的(接受缓冲区初始为空),而写事件默认是就绪的(发送缓冲区初始是空的,有空间可写)。
  • 报错处理:一旦出错返回 -1 并设置 errno,最关键的是要查明"为什么出错"。

1.2 阻塞 IO (Blocking IO)

  • 概念解释:最常见的 IO 模型。在内核将数据准备好之前,系统调用会一直挂起等待。所有的套接字 (Socket) 默认都是阻塞方式。
  • 笔记
  • 执行流程 :应用进程发起 recvfrom 系统调用 -> 内核无数据报准备好 -> 进程阻塞于 recvfrom 调用 -> 数据报准备好 -> 将数据从内核拷贝到用户空间 -> 返回成功指示 -> 处理数据报。
  • (类比:专心致志地守在水边钓鱼,不见鱼漂沉下去绝不离开。)

1.3 非阻塞 IO (Non-blocking IO) 与 轮询

  • 概念解释非阻塞 IO 是指如果内核还未将数据准备好,系统调用不会挂起,而是直接返回一个错误码(通常是 EWOULDBLOCKEAGAIN)。轮询 (Polling) 则是指程序员需要用循环的方式反复尝试读取或写入数据,直到成功为止。
  • 笔记
  • 执行流程 :进程反复调用 recvfrom -> 若无数据则立即返回 EWOULDBLOCK -> 循环调用 -> 直到数据准备好 -> 拷贝数据报 -> 返回成功。
  • 缺点:轮询对 CPU 资源是较大的浪费,一般只有特定场景下(结合特定的等待机制)才使用。

1.4 信号驱动 IO (Signal-driven IO)

  • 概念解释 :内核在数据准备好的时候,主动通过发送 SIGIO 信号通知应用程序,应用程序收到信号后再去发起系统调用进行数据拷贝。
  • 笔记
  • 执行流程 :建立 SIGIO 的信号处理程序(通过 sigaction) -> 进程继续执行主逻辑(不阻塞) -> 数据准备好 -> 内核递交 SIGIO 信号 -> 应用程序调用 recvfrom -> 拷贝数据期间进程阻塞 -> 完成后处理数据。

1.5 IO 多路转接 / 多路复用 (IO Multiplexing)

  • 概念解释 :通过一种机制(如 selectpollepoll),让单个进程能够同时等待多个文件描述符 (fd) 的就绪状态。虽然它在数据拷贝时也是阻塞的,但它的核心优势在于一次等待多个 fd
  • 笔记
  • 执行流程 :应用进程受阻于 select 调用 -> 等待多个套接口中的任意一个变为可读 -> 返回可读条件 -> 应用程序再调用 recvfrom 进行无阻塞的数据拷贝。

1.6 异步 IO (Asynchronous IO)

  • 概念解释 :由内核负责数据的拷贝工作。内核在数据拷贝完全完成时,才通知应用程序直接去使用数据。
  • 笔记
  • 执行流程 :调用 aio_read -> 立即返回,进程继续执行 -> 内核等待数据并主动将数据拷贝到用户空间 -> 拷贝完成后递交指定信号通知进程。
  • 与信号驱动的对比 :信号驱动是告诉应用程序"何时可以开始 拷贝数据"(拷贝过程仍由应用程序自己阻塞完成);异步 IO 是告诉应用程序"何时数据已经拷贝完成"。
  • (类比总结《妖怪蒸唐僧》:不同小妖看守蒸笼的方式对应不同的 IO 等待模型。)

二、 高级 IO 核心理论解析

2.1 同步通信 vs 异步通信

  • 概念解释 :关注的是消息通信机制(注意区分多线程中的同步与互斥概念)。
  • 笔记
  • 同步 (Synchronous) :发出调用时,在没有得到结果之前,该调用就不返回。一旦返回,就得到返回值。由调用者主动等待调用的结果。
  • 异步 (Asynchronous) :调用发出后直接返回,没有返回结果。被调用者在完成任务后,通过状态、通知或回调函数来通知调用者。

2.2 阻塞 vs 非阻塞

  • 概念解释 :关注的是程序在等待调用结果(消息、返回值)时的状态
  • 笔记
  • 阻塞 (Blocking):调用结果返回之前,当前线程会被挂起(休眠)。
  • 非阻塞 (Non-blocking):在不能立刻得到结果之前,该调用不会阻塞当前线程,直接返回错误标志。

【发散思考与解答】

问:同步/异步 与 阻塞/非阻塞 常常被混淆,它们到底有何组合关系?

:同步/异步是"拿结果的方式"(我主动等结果还是你送结果过来),阻塞/非阻塞是"等结果时的状态"(我是睡觉等还是边干活边等)。例如:

  • 同步阻塞 :最常见的传统 recv(主动去拿数据,没拿到就死等挂起)。
  • 同步非阻塞 :设置了非阻塞标志的 recv 轮询(主动去拿数据,没拿到直接返回错误,我不挂起,一会再来问)。
  • 异步非阻塞aio_read(告诉内核把数据准备好并送到缓冲区,我不挂起继续干活,内核弄好了通知我)。

三、 高级 IO 实践:非阻塞 IO (fcntl)

3.1 涉及的核心函数

  • 函数名fcntl
  • 函数原型
c 复制代码
#include <unistd.h>
#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */);

功能与参数说明

控制文件描述符的属性。cmd 决定了函数的具体行为,常用的有 5 种功能:

  1. 复制现有描述符 (cmd=F_DUPFD)。
  2. 获得/设置文件描述符标记 (cmd=F_GETFDF_SETFD)。
  3. 获得/设置文件状态标记 (cmd=F_GETFLF_SETFL) ------ 这是实现非阻塞的核心。
  4. 获得/设置异步 I/O 所有权 (cmd=F_GETOWNF_SETOWN)。
  5. 获得/设置记录锁 (cmd=F_GETLK, F_SETLKF_SETLKW)。

3.2 实现 SetNoBlock 非阻塞设置

  • 笔记
  • 一个文件描述符默认是阻塞 IO。
  • 实现逻辑:先使用 F_GETFL 取出当前属性(位图),再附加 O_NONBLOCK 参数并通过 F_SETFL 设置回去。
c 复制代码
void SetNoBlock(int fd) {
    int fl = fcntl(fd, F_GETFL); // 取出当前文件描述符属性
    if (fl < 0) {
        perror("fcntl");
        return;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 追加非阻塞标记并设置回内核
}
  • 应用层非阻塞轮询读取 fd=0 (标准输入) 时,如果没有输入,read 返回 <0,必须结合 sleep 进行轮询,避免 CPU 100% 占用空转。
相关推荐
ylscode15 小时前
联想驱动程序暗藏内核级杀器:BYOVD攻击链下的EDR防护溃堤实录
网络·安全·安全威胁分析
Rabitebla15 小时前
C++ 继承详解(下):默认成员函数、虚继承底层与设计取舍
c语言·开发语言·数据结构·c++·算法·leetcode
运维行者_20 小时前
Applications Manager中的Redis监控
大数据·服务器·数据库·人工智能·网络协议
祁白_1 天前
[0xV01D]_Night Traffic_writeUp
网络·安全·ctf·writeup
xingyuzhisuan1 天前
网络 Token 常见故障原理,基础排查科普
运维·服务器·网络·php
APIshop1 天前
Python 获取 1688 商品采集 API 接口 | 工厂货源自动化对接商品信息 | 无需选品
运维·python·自动化
z落落1 天前
C#String字符串
开发语言·c#·php
wljy11 天前
二、进制状态转换
linux·运维·服务器·c语言·c++
handler011 天前
【MySQL】常用命令总结(库与表增删查改)
运维·数据库·mysql·命令·总结