【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% 占用空转。
相关推荐
A小辣椒1 天前
TShark:Wireshark CLI 功能
linux
A小辣椒1 天前
TShark:基础知识
linux
BingoGo1 天前
PHP 泛型之殇 泛型 RFC 提案被拒绝
后端·php
JaguarJack1 天前
PHP 泛型之殇 泛型 RFC 提案被拒绝
后端·php
AlfredZhao1 天前
OCI 明明分配了 200G 系统盘,为什么 df 只看到 30G?
linux·oci
AlfredZhao2 天前
vi 删除指定范围的行,不用再反复按 dd
linux·vi
用户3074596982072 天前
PHP 扩展——从入门到理解
php
用户9718356334662 天前
银河麒麟 KY10 申威(SW64) 安装 nginx-1.16.1-2.p01.ky10.sw_64.rpm 详细步骤
linux
猪脚踏浪2 天前
linux 拷贝文件或目录到指定的位置
linux
鹏仔先生3 天前
拷贝漫画APP下载页PHP程序,后台带免费AI写作
php