【Linux网络】五种IO模型与非阻塞IO

🎬 个人主页艾莉丝努力练剑
专栏传送门 :《C语言》《数据结构与算法》《C/C++干货分享&学习过程记录
Linux操作系统编程详解》《笔试/面试常见算法:从基础到进阶》《Python干货分享

⭐️为天地立心,为生民立命,为往圣继绝学,为万世开太平


🎬 艾莉丝的简介:


文章目录

  • 前言
  • [1 ~> IO 的相关概念](#1 ~> IO 的相关概念)
    • [1.1 IO 的本质](#1.1 IO 的本质)
    • [1.2 什么是高效的 IO](#1.2 什么是高效的 IO)
    • [1.3 如何提高 IO 效率](#1.3 如何提高 IO 效率)
    • [1.4 IO相关概念知识图谱](#1.4 IO相关概念知识图谱)
  • [2 ~> 五种 IO 模型](#2 ~> 五种 IO 模型)
    • [2.1 钓鱼故事:五种人物对应五种 IO 模型](#2.1 钓鱼故事:五种人物对应五种 IO 模型)
    • [2.2 场景角色与技术概念对应](#2.2 场景角色与技术概念对应)
    • [2.3 阻塞 IO 和非阻塞 IO](#2.3 阻塞 IO 和非阻塞 IO)
    • [2.4 同步 IO 和异步 IO](#2.4 同步 IO 和异步 IO)
      • [2.4.1 同步 IO 与异步 IO 正式定义](#2.4.1 同步 IO 与异步 IO 正式定义)
      • [2.4.2 信号驱动 IO](#2.4.2 信号驱动 IO)
      • [2.4.3 异步 IO](#2.4.3 异步 IO)
        • [POSIX AIO 异步读取 Demo 代码](#POSIX AIO 异步读取 Demo 代码)
        • 编译与运行说明
        • [异步 IO 原理与应用现状](#异步 IO 原理与应用现状)
      • [2.4.4 知识点总结与概念区分](#2.4.4 知识点总结与概念区分)
    • [2.5 五大 IO 模型标准原理详解](#2.5 五大 IO 模型标准原理详解)
      • [2.5.1 阻塞 IO](#2.5.1 阻塞 IO)
      • [2.5.2 非阻塞 IO](#2.5.2 非阻塞 IO)
      • [2.5.3 信号驱动 IO](#2.5.3 信号驱动 IO)
      • [2.5.4 多路转接 IO(IO 多路复用)](#2.5.4 多路转接 IO(IO 多路复用))
      • [2.5.5 异步 IO](#2.5.5 异步 IO)
      • [2.5.6 五种 IO 模型效率对比](#2.5.6 五种 IO 模型效率对比)
      • [2.5.7 五大 IO 模型整体小结](#2.5.7 五大 IO 模型整体小结)
    • [2.6 五种IO模型知识图谱](#2.6 五种IO模型知识图谱)
  • [3 ~> 非阻塞 IO](#3 ~> 非阻塞 IO)
    • [3.1 fcntl 系统调用基础](#3.1 fcntl 系统调用基础)
      • [3.1.1 函数原型](#3.1.1 函数原型)
      • [3.1.2 fcntl 五大核心功能](#3.1.2 fcntl 五大核心功能)
    • [3.2 文件状态标志位](#3.2 文件状态标志位)
    • [3.3 封装 SetNoBlock 工具函数](#3.3 封装 SetNoBlock 工具函数)
      • [3.3.1 函数代码](#3.3.1 函数代码)
    • [3.4 非阻塞 IO 演示思路](#3.4 非阻塞 IO 演示思路)
    • [3.5 非阻塞读取标准输入 完整代码](#3.5 非阻塞读取标准输入 完整代码)
    • [3.6 read 函数返回值详解](#3.6 read 函数返回值详解)
    • [3.7 补充:输入缓冲区问题解答](#3.7 补充:输入缓冲区问题解答)
    • [3.8 非阻塞IO知识图谱(不含代码部分)](#3.8 非阻塞IO知识图谱(不含代码部分))
    • [3.9 代码(艾莉丝的笔记截图 + 分析)](#3.9 代码(艾莉丝的笔记截图 + 分析))
      • [3.9.1 补充两个细节](#3.9.1 补充两个细节)
      • [3.9.2 测试](#3.9.2 测试)
    • [3.10 阻塞IO代码](#3.10 阻塞IO代码)
      • [3.10.1 阻塞IO的Demo代码](#3.10.1 阻塞IO的Demo代码)
      • [3.10.2 阻塞代码运行现象](#3.10.2 阻塞代码运行现象)
    • [3.11 非阻塞IO:工程实现代码](#3.11 非阻塞IO:工程实现代码)
      • [3.11.1 全量可直接编译代码(C++)](#3.11.1 全量可直接编译代码(C++))
      • [3.11.2 关键实现说明](#3.11.2 关键实现说明)
    • [3.12 非阻塞errno错误码](#3.12 非阻塞errno错误码)
      • [3.12.1 EAGAIN / EWOULDBLOCK 内核宏定义与含义](#3.12.1 EAGAIN / EWOULDBLOCK 内核宏定义与含义)
      • [3.12.2 错误分层判断必要性](#3.12.2 错误分层判断必要性)
    • [3.13 中断错误EINTR处理](#3.13 中断错误EINTR处理)
      • [3.13.1 EINTR触发场景](#3.13.1 EINTR触发场景)
      • [3.13.2 修复逻辑](#3.13.2 修复逻辑)
    • [3.14 非阻塞轮询业务拓展说明](#3.14 非阻塞轮询业务拓展说明)
    • [3.15 非阻塞IO综合全流程测试](#3.15 非阻塞IO综合全流程测试)
      • [3.15.1 编译命令](#3.15.1 编译命令)
      • [3.15.2 空载运行现象(无键盘输入)](#3.15.2 空载运行现象(无键盘输入))
      • [3.15.3 输入数据现象](#3.15.3 输入数据现象)
      • [3.15.4 Ctrl + D关闭输入流](#3.15.4 Ctrl + D关闭输入流)
      • [3.15.5 Ctrl + C信号中断测试](#3.15.5 Ctrl + C信号中断测试)
      • [3.15.6 性能缺陷补充](#3.15.6 性能缺陷补充)
    • [3.16 非阻塞IO代码相关知识图谱](#3.16 非阻塞IO代码相关知识图谱)
    • [3.17 非阻塞IO的完整代码](#3.17 非阻塞IO的完整代码)
    • [3.18 实操踩坑细节汇总](#3.18 实操踩坑细节汇总)
  • [4 ~> 全文总结](#4 ~> 全文总结)
    • [1. IO 本质与优化核心](#1. IO 本质与优化核心)
    • [2. 五种 IO 模型(钓鱼类比 + 分类)](#2. 五种 IO 模型(钓鱼类比 + 分类))
    • [3. 各模型核心特征](#3. 各模型核心特征)
    • [4. 同步 / 异步 IO 关键区分](#4. 同步 / 异步 IO 关键区分)
    • [5. 非阻塞 IO 核心技术:fcntl](#5. 非阻塞 IO 核心技术:fcntl)
    • [6. 代码与返回值重点](#6. 代码与返回值重点)
    • [7. 实操工程化知识点](#7. 实操工程化知识点)
      • [7.1 阻塞与非阻塞对照基准](#7.1 阻塞与非阻塞对照基准)
      • [7.2 errno错误码分层处理体系](#7.2 errno错误码分层处理体系)
      • [7.3 非阻塞业务拓展价值](#7.3 非阻塞业务拓展价值)
      • [7.4 实操高频踩坑清单](#7.4 实操高频踩坑清单)
    • [8. 学习路线总结](#8. 学习路线总结)
  • 结尾


前言

一、可视化思维导图

二、学习框架导入语

本文聚焦操作系统与 Linux 网络编程核心 IO 体系 ,从 IO 最底层的本质概念入手,拆解 IO 执行的两大核心步骤,解释 IO 运行低效的根本原因,并给出优化 IO 效率的核心思路。为了降低抽象概念的理解难度,全文采用钓鱼生活化案例 类比业界通用的五种经典 IO 模型,依次讲解阻塞 IO、非阻塞 IO、信号驱动 IO、IO 多路复用、异步 IO 的运行逻辑,同时精准界定同步 IO 与异步 IO的判定标准,搭配可直接编译运行的 C 语言代码完成信号驱动 IO、POSIX 异步 IO 的实操演示。

在此基础上,文章深入落地到工程实践中最常用的非阻塞 IO ,重点讲解实现非阻塞 IO 的核心系统调用fcntl,梳理其五大功能、文件状态标志位,封装通用的非阻塞设置函数,并编写完整代码演示非阻塞读取标准输入的流程,同时解读read函数在非阻塞场景下的返回值规则、常见误区与输入缓冲区相关问题。新增阻塞对照代码、完整工程化非阻塞Demo、errno错误码分层处理、EINTR信号中断修复、轮询业务拓展、全流程测试现象解析,覆盖实操中全部踩坑细节。全文遵循「理论讲解 + 案例类比 + 代码实操 + 踩坑细节 + 完整测试」的逻辑链条,层层递进串联整套 Linux IO 知识,是网络编程 IO 模块的完整复习资料,能够帮助学习者完整梳理 IO 体系的逻辑脉络。


1 ~> IO 的相关概念

1.1 IO 的本质

从系统与网络编程视角出发,IO 即 Input(输入)、Output(输出),日常调用readrecvwritesend等 IO 接口时,大部分耗时都消耗在等待数据阶段。

IO 的执行流程可以拆分为两个固定阶段,这也是 IO 最核心的本质:

  1. 输入(Input)流程 :第一步是等待数据就绪 ,第二步是将内核接收缓冲区的数据拷贝到用户空间
  2. 输出(Output)流程 :第一步是等待内核发送缓冲区有空闲空间 ,第二步是将用户空间的数据拷贝到内核发送缓冲区

由此得出核心结论:IO = 等待 + 拷贝 。 狭义认知中很多人仅把「数据拷贝」当做 IO,但传统意义上完整的 IO 流程必须包含等待与拷贝两个环节。由于网络传输、硬件交互存在延迟,等待环节是 IO 耗时的主要来源,也直接导致 IO 整体效率偏低。网卡、路由器等网络硬件负责长距离数据转发,是数据能够到达内核缓冲区的物理基础。

1.2 什么是高效的 IO

结合IO = 等待 + 拷贝的核心公式分析效率:

  • 拷贝属于纯硬件执行动作,硬件速率固定,几乎无法通过软件优化提升拷贝效率;
  • IO 低效的根本原因 :单位时间内,等待环节的时间占比过高

基于此定义高效 IO:单位时间内,等待环节的时间占比越低,IO 的整体效率就越高。优化 IO 的核心方向,就是想尽办法降低等待在整个 IO 流程中的时间占比。

1.3 如何提高 IO 效率

IO 的最终目的是实现设备 / 网络之间的数据通信,结合上文结论,提升 IO 效率的核心思路非常明确:在单位时间内,降低 IO 流程中等待环节的时间占比

这里用钓鱼做生活化类比(仅保留核心动作,忽略打窝、准备饵料等前置操作):钓鱼分为「等待鱼咬钩」+「提竿捞鱼」两个步骤,完全对应 IO 的「等待 + 拷贝」。

  1. 低效钓鱼:绝大部分时间都在单纯等待鱼咬钩,等待时间占比极高;
  2. 高效钓鱼:减少无效等待的占比,或是在等待的同时处理其他事务。

该类比("钓鱼例子")贯穿后续五种 IO 模型的讲解,帮助理解不同 IO 模型的设计思想。

1.4 IO相关概念知识图谱


2 ~> 五种 IO 模型

IO 效率不足会直接影响程序并发能力与产品体验,业界基于「等待 + 拷贝」的 IO 本质,演化出五种经典 IO 模型。本节全部依托钓鱼场景做类比讲解,再对应到操作系统进程、文件描述符等专业概念。

2.1 钓鱼故事:五种人物对应五种 IO 模型

以河边钓鱼为场景,五位钓鱼者的行为模式分别对应五种 IO 模型:

  1. 张三 --- 阻塞 IO 张三是新手钓鱼者,全程死死盯着鱼漂,鱼漂不动就原地等待、不做任何其他事情;只有鱼漂晃动(鱼咬钩)时,才会执行提竿捞鱼的动作。核心特征:被等待动作阻塞,等待期间无法执行其他事务
  2. 李四 --- 非阻塞 IO 李四有多年钓鱼经验,不会一直盯着鱼漂死等。他会间歇性查看鱼漂状态,在两次查看的间隙聊天、刷手机、处理其他琐事;一旦发现鱼漂晃动,立刻提竿捞鱼。核心特征:不会被等待动作阻塞,轮询检查状态,等待间隙处理其他工作
  3. 王五 --- 信号驱动 IO 王五经验更加老道,在鱼竿顶端悬挂铃铛,随后将鱼竿插在岸边,自己离开鱼竿做其他事。当鱼咬钩时鱼线晃动触发铃铛响(信号通知),王五收到声音信号后,再回来提竿捞鱼。核心特征:依靠信号通知事件就绪,无需主动轮询等待
  4. 赵六 --- IO 多路复用(多路转接) 赵六拥有大量鱼竿,一次性在岸边摆放多根鱼竿,来回巡视所有鱼竿的鱼漂状态。只要任意一根鱼竿的鱼漂晃动,就立刻对应捞鱼,甚至可以同时处理多条上钩的鱼。核心特征:统一监听多个目标,一次等待多个就绪事件
  5. 田七 --- 异步 IO 田七本身不喜欢钓鱼,仅需要最终的鱼。他不会参与钓鱼的任何环节,而是将鱼竿、水桶、通讯工具全部交给下属小王,全权委托小王完成「等待鱼咬钩 + 捞鱼」的全部流程;要求小王钓鱼完成后电话通知自己,田七全程处理自身业务。核心特征:仅发起任务,完全不参与等待、拷贝等任何 IO 流程,由第三方全权执行

2.2 场景角色与技术概念对应

将钓鱼场景的元素一一映射到 Linux IO 编程的专业概念,建立类比关联:

  • 钓鱼的人 = 操作系统中的进程
  • 鱼竿 = 文件描述符(fd),是程序访问文件、网络套接字的标识;
  • 鱼漂晃动 = IO 就绪事件(数据到达、缓冲区空闲等);
  • 鱼 = 待读写的业务数据
  • 池塘 / 河流 = 网络环境(网络数据传输链路)。

2.3 阻塞 IO 和非阻塞 IO

张三(阻塞 IO)与李四(非阻塞 IO)都需要完成「等待 + 捞鱼」两个步骤,二者纯 IO 执行效率没有区别 ,核心差异仅在于等待的方式不同

  1. 阻塞 IO:进程被等待动作阻塞,等待期间 CPU 资源被占用且无法执行其他业务;
  2. 非阻塞 IO:进程不会被阻塞,主动轮询检查事件状态,把原本无效的等待时间利用起来执行其他任务。

通俗举例理解:两名员工等待任务下发,一人全程紧盯工作面板不动(阻塞),一人每隔一段时间查看面板,间隙处理其他工作(非阻塞)。二者处理任务本身的速度一致,区别只是等待阶段的时间利用方式。非阻塞属于「时间管理优化」,而非提升 IO 本身的执行速度。

2.4 同步 IO 和异步 IO

同步 IO 与异步 IO 是五种 IO 模型的顶层分类,判定规则是区分二者的核心,结合钓鱼案例逐一分析。

2.4.1 同步 IO 与异步 IO 正式定义

结合IO = 等待 + 拷贝公式给出标准判定规则:

  1. 同步 IO :只要进程参与 IO 流程中的任意一个环节(等待数据 或 数据拷贝),就属于同步 IO。阻塞 IO、非阻塞 IO、信号驱动 IO、IO 多路复用均属于同步 IO;
  2. 异步 IO :进程完全不参与等待、拷贝任何环节,仅负责发起 IO 请求,全部流程由操作系统完成,完成后主动通知进程。仅田七对应的异步 IO 属于此类。

补充说明:进程运行在用户空间,操作系统内核运行在内核空间,IO 操作本质是进程与内核的协同工作 ,二者属于松耦合设计:进程无需感知底层硬件细节,内核也不关心进程的业务逻辑;但同步 IO 会造成进程与内核在执行时间上强绑定,这也是后续 IO 模型不断迭代优化的原因。

2.4.2 信号驱动 IO

信号驱动 IO 依托 Linux 系统信号实现,核心使用SIGIO信号:当文件描述符上的数据就绪时,内核会向进程发送SIGIO异步信号,进程捕获信号后再执行数据拷贝。

系统信号列表(Linux)

执行 kill -1 可查看系统所有信号,节选核心信号如下:

bash 复制代码
1) SIGHUP  2) SIGINT  3) SIGQUIT  4) SIGILL  5) SIGTRAP
6) SIGABRT 7) SIGBUS  8) SIGFPE   9) SIGKILL 10) SIGUSR1
11) SIGSEGV 12) SIGUSR2 13) SIGPIPE 14) SIGALRM 15) SIGTERM
16) SIGSTKFLT 17) SIGCHLD 18) SIGCONT 19) SIGSTOP 20) SIGTSTP
21) SIGTTIN 22) SIGTTOU 23) SIGURG  24) SIGXCPU 25) SIGXFSZ
26) SIGVTALRM 27) SIGPROF 28) SIGWINCH 29) SIGIO  30) SIGPWR
31) SIGSYS  34) SIGRTMIN ... 64) SIGRTMAX

其中 29) SIGIO 是信号驱动 IO 的核心信号。

信号驱动 IO C 语言 Demo 代码

基于标准输入(文件描述符STDIN_FILENO)实现信号驱动 IO,代码如下:

c 复制代码
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>
#include<signal.h>

// 全局文件描述符
int fd;

// SIGIO信号处理函数:数据就绪后执行读取
void io_handler(int sig){
    char buf[100];
    int n = read(fd, buf, sizeof(buf)-1);
    if(n > 0){
        buf[n] = '\0'; 
        printf("收到输入:%s",buf);
    }
}

int main(){
    // 绑定标准输入文件描述符
    fd = STDIN_FILENO;
    // 注册SIGIO信号的处理函数
    signal(SIGIO, io_handler);
    // 设置异步IO的归属进程
    fcntl(fd, F_SETOWN, getpid());
    // 开启文件描述符的异步标志
    fcntl(fd, F_SETFL, fcntl(fd, F_GETFL) | O_ASYNC);

    printf("请输入内容(程序会在后台等待信号)\n");
    // 休眠等待信号触发
    while(1)pause();
    return 0;
}

代码逻辑说明:程序为标准输入注册SIGIO信号处理函数,开启异步标记后进程休眠;当键盘输入数据(数据就绪),内核发送SIGIO信号,触发回调函数执行read读取数据。该模型中进程仍参与数据拷贝环节,因此属于同步 IO。

2.4.3 异步 IO

异步 IO 是唯一一类进程完全不参与等待、拷贝的模型,Linux 下使用POSIX AIO 标准异步 IO 系统调用实现,核心函数为aio_read

POSIX AIO 异步读取 Demo 代码
c 复制代码
#include<aio.h>
#include<unistd.h>
#include<fcntl.h>
#include<string.h>
#include<stdio.h>

int main(){
    char buf[100] = {0};
    // 定义异步IO控制块,初始化参数
    struct aiocb cb = {
        .aio_fildes = STDIN_FILENO,
        .aio_buf = buf,
        .aio_nbytes = sizeof(buf)
    };
    // 发起异步读请求(内核全权处理等待+拷贝)
    aio_read(&cb);
    // 轮询等待异步操作完成
    while (aio_error(&cb) == EINPROGRESS);
    // 获取读取结果并输出
    ssize_t ret = aio_return(&cb);
    write(STDOUT_FILENO, buf, ret);
    return 0;
}
编译与运行说明

POSIX AIO 依赖实时库librt,编译时需要链接该库,编译命令:

bash 复制代码
gcc demo.c -o demo -lrt
异步 IO 原理与应用现状
  1. 原理:调用aio_read后进程直接返回,内核独立完成「等待数据就绪 + 内核缓冲区拷贝到用户缓冲区」全流程;操作完成后通过状态标识通知进程,进程仅需要读取最终数据。对应钓鱼案例中的田七,全程委托内核(小王)执行所有流程。
  2. 应用现状:传统老项目中异步 IO 有一定使用场景,但目前工业界使用频率较低 。当下高并发场景更多使用协程替代原生异步 IO;异步 IO 仅保留在部分特殊业务场景中。

2.4.4 知识点总结与概念区分

学习重点

五种 IO 模型中,非阻塞 IO、IO 多路复用是核心学习内容,也是网络编程高并发开发的基础;掌握二者后可进一步学习协程技术。信号驱动 IO、原生异步 IO 了解原理即可。

易混淆概念区分

执行流同步(进程 / 线程同步)同步 IO 不是同一概念:

  1. 执行流同步:针对多进程、多线程的执行顺序、资源竞争问题,属于并发编程范畴;
  2. 同步 IO:针对 IO 流程中进程是否参与等待 / 拷贝环节,属于 IO 编程范畴。 二者应用场景、判定标准完全独立,不可混淆。

2.5 五大 IO 模型标准原理详解

结合操作系统内核与进程交互流程,逐一拆解五种 IO 模型的标准工作流程,所有套接字默认均为阻塞 IO模式。

2.5.1 阻塞 IO

  1. 流程:进程调用recvfrom/read等系统调用 → 内核检测数据,若无数据则进程阻塞,持续等待数据到达 → 内核数据就绪后,将数据从内核缓冲区拷贝至用户空间 → 系统调用返回,进程执行业务逻辑。
  2. 特点:recvfrom/read 同时承担等待数据数据拷贝两个工作;是最简单、最常用的 IO 模型,缺点是进程会被长时间阻塞。

2.5.2 非阻塞 IO

  1. 流程:进程调用recvfrom/read → 内核检测数据,若无数据**立即返回****EWOULDBLOCK**错误码,不会阻塞进程 → 进程编写循环逻辑,反复调用接口轮询状态 → 数据就绪后,内核完成数据拷贝,接口返回正常结果 → 进程处理数据。
  2. 特点:需要代码主动轮询,频繁的系统调用会大量消耗 CPU 资源 ,仅适用于少量特殊场景。recvfrom/read 同样兼顾等待与拷贝动作。

2.5.3 信号驱动 IO

  1. 流程:进程预先注册SIGIO信号处理函数,并为文件描述符开启异步信号属性 → 进程继续执行业务逻辑,内核在后台等待数据 → 数据就绪后,内核向进程发送SIGIO信号 → 进程触发信号回调函数,在回调中调用recvfrom/read完成数据拷贝 → 拷贝完成后继续执行业务。
  2. 特点:依靠信号实现事件通知,无需主动轮询;但数据拷贝环节仍需要进程参与,属于同步 IO。

2.5.4 多路转接 IO(IO 多路复用)

  1. 核心函数:select(多路复用经典接口)。
  2. 流程:进程调用select系统调用,一次性传入多个文件描述符 → 内核监听所有 fd,若全部无数据则进程阻塞在select上等待 → 任意一个 fd 数据就绪,select立即返回,告知进程就绪的 fd → 进程针对就绪 fd 调用recvfrom/read完成数据拷贝。
  3. 特点:
    1. select 仅负责等待多个 fd 的就绪事件 ,不做数据拷贝;拷贝动作仍由recvfrom/read完成;
    2. 单次等待可以监听多个文件描述符,是高并发网络编程的核心模型;
    3. 进程参与等待与拷贝,属于同步 IO。

2.5.5 异步 IO

  1. 流程:进程调用aio_read等异步 IO 接口,发起 IO 请求后立即返回,继续执行业务 → 内核独立完成「等待数据就绪 + 数据拷贝到用户空间」全流程 → 全部操作完成后,内核通过信号 / 状态标识通知进程 → 进程读取已拷贝完成的数据。
  2. 特点:进程全程不参与 IO 的任何环节,是真正意义上的异步 IO。

2.5.6 五种 IO 模型效率对比

综合实际运行效率:IO 多路复用(多路转接)效率最高。 原因:IO 多路复用可以单次等待多个文件描述符,大幅降低了「等待」在整体 IO 流程中的时间占比;对应钓鱼案例中赵六使用多根鱼竿,单位时间内捕获数据(鱼)的概率远高于单人单鱼竿的其他模型。

2.5.7 五大 IO 模型整体小结

  1. 所有 IO 流程都固定分为等待数据数据拷贝两个阶段,等待是 IO 低效的核心原因;
  2. 五种 IO 模型的本质差异,只是处理「等待环节」的方式不同
  3. 工程优化的核心思路:尽可能降低无效等待的时间占比,这也是 IO 模型持续迭代的底层逻辑。

2.6 五种IO模型知识图谱


3 ~> 非阻塞 IO

非阻塞 IO 是日常开发中高频使用的模型,Linux 下无法直接通过open永久设置非阻塞属性,主要依靠 fcntl 系统调用 动态修改已打开文件描述符的状态。本节讲解fcntl、文件状态标志位、非阻塞函数封装、代码演示与返回值规则。

3.1 fcntl 系统调用基础

fcntl 全称 File Control(文件控制),是 Linux/Unix 核心系统调用,作用是动态修改已经打开的文件描述符(fd) 的属性。当文件 / 套接字打开后,无法重新调用open修改属性时,fcntl是唯一的配置通道。

3.1.1 函数原型

c 复制代码
#include <unistd.h>
#include <fcntl.h>

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

参数说明:

  1. fd:目标文件描述符(文件、套接字、管道等);
  2. cmd:操作指令,决定fcntl执行的功能,是核心参数;
  3. 可变参数:根据cmd的不同,可选传入整数、结构体等参数,部分场景无需传参。

3.1.2 fcntl 五大核心功能

根据cmd参数的取值,fcntl分为五大类功能:

  1. 复制文件描述符cmd = F_DUPFD):等同于dup函数,生成一个新的文件描述符,二者指向同一个文件;
  2. 获取 / 设置文件描述符标记cmd = F_GETFD / F_SETFD):最常用标记为FD_CLOEXEC,设置后执行exec系列函数时自动关闭 fd,防止数据泄露;
  3. 获取 / 设置文件状态标记cmd = F_GETFL / F_SETFL):网络编程最常用,修改文件读写模式、非阻塞、同步 IO 等状态;
  4. 获取 / 设置异步 IO 所有权cmd = F_GETOWN / F_SETOWN):指定SIGIO信号发送的目标进程 / 进程组;
  5. 获取 / 设置文件记录锁cmd = F_GETLK / F_SETLK / F_SETLKW):多进程读写文件时加锁,分为共享读锁、独占写锁,防止数据错乱。

3.2 文件状态标志位

文件状态标志位通过位图形式存储在文件结构体struct filef_flags成员中,配合fcntl(F_GETFL/F_SETFL)读写,常用宏定义与含义如下:

宏定义 功能含义
O_APPEND 每次写入数据时,自动追加到文件末尾
O_NONBLOCK 非阻塞模式,读写操作不会阻塞进程(非阻塞 IO 核心标志)
O_SYNC 同步 IO,write调用必须等待物理磁盘写入完成才返回
O_DSYNC 仅同步文件数据,不同步文件元数据,功能近似 O_SYNC
O_RSYNC 针对读操作的同步标记
O_DIRECT 直接 IO,绕过系统缓存,最小化缓存开销
O_DIRECTORY 仅打开目录,路径非目录则调用失败
O_NOFOLLOW 不跟随符号链接,路径为软链接则调用失败
O_CLOEXEC 执行新程序时自动关闭当前文件描述符

其中 O_NONBLOCK 是实现非阻塞 IO 的核心标志。

3.3 封装 SetNoBlock 工具函数

基于fcntl的文件状态标记功能,封装通用函数,将任意文件描述符设置为非阻塞模式,该函数仅需调用一次,fd 会永久保持非阻塞状态

3.3.1 函数代码

c 复制代码
#include <unistd.h>
#include <fcntl.h>
#include <stdio.h>

// 将指定文件描述符设置为非阻塞模式
void SetNoBlock(int fd) {
    // 第一步:获取fd当前的状态标志
    int fl = fcntl(fd, F_GETFL);
    if(fl < 0){
        perror("fcntl getfl error");
        return;
    }
    // 第二步:在原有标志基础上,追加非阻塞标记 O_NONBLOCK
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

逻辑说明:先读取 fd 原有属性(保留读写权限、追加等原有标记),再通过按位或 叠加O_NONBLOCK非阻塞标记,最后写回 fd 属性。

3.4 非阻塞 IO 演示思路

选用标准输入(fd=0)标准输出(fd=1) 作为测试载体:

  1. 标准输入 (fd=0):默认无数据,按下键盘则数据就绪,符合「等待数据」的测试场景,适合演示非阻塞;
  2. 标准输出 (fd=1):内核输出缓冲区默认始终有空闲空间,输出条件永久满足,非阻塞效果不直观,一般不作为测试对象。

整体演示逻辑:将标准输入设为非阻塞,循环调用read读取数据,无数据时接口立即返回错误,程序不会阻塞。

3.5 非阻塞读取标准输入 完整代码

c 复制代码
#include<stdio.h>
#include<unistd.h>
#include<fcntl.h>

// 设置文件描述符为非阻塞
void SetNoBlock(int fd) {
    int fl = fcntl(fd, F_GETFL);
    if(fl < 0){
        perror("fcntl");
        return;
    }
    fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

int main(){
    // 将标准输入设置为非阻塞(仅设置一次)
    SetNoBlock(STDIN_FILENO);

    while(1){
        char buf[1024] = {0};
        // 读取标准输入
        ssize_t read_size = read(STDIN_FILENO, buf, sizeof(buf) - 1);

        if(read_size < 0){
            // 非阻塞模式下:无数据会触发该分支,并非真正故障
            perror("read");
            sleep(1);
            continue;
        }
        // 读取到有效数据,打印内容
        printf("input:%s\n",buf);
    }
    return 0;
}

3.6 read 函数返回值详解

read函数原型:

c 复制代码
#include <unistd.h>
ssize_t read(int fd, void *buf, size_t count);

结合阻塞 / 非阻塞 两种场景,read共有三类返回值,是代码编写的重点:

  1. 返回值 > 0:成功读取到有效数据,返回值为实际读取的字节数;
  2. 返回值 == 0:文件 / 数据流关闭(如管道断开、文件末尾、客户端断开网络连接);
  3. 返回值 < 0 :分为两种场景:
    1. 阻塞模式:返回负数代表真实 IO 错误(硬件故障、fd 非法等);
    2. 非阻塞模式:无数据就绪 是常态,会返回负数并设置errno=EWOULDBLOCK,这不属于程序异常,需要代码区分「真错误」和「数据未就绪」。

核心易错点 :非阻塞场景下,不能将read_size < 0直接判定为程序故障,必须结合错误码区分状态。

3.7 补充:输入缓冲区问题解答

标准输入存在系统输入缓冲区,该缓冲区分为两层:

  1. 内核层面:所有文件描述符共用的内核缓冲区,是 IO 流程中「等待 + 拷贝」的载体;
  2. C 语言标准 IO 流层面(stdio.h):额外封装的用户态缓冲区,和系统调用层面的 IO 缓冲区实现逻辑不同。

二者底层 IO 原理(等待 + 拷贝)完全一致,仅上层封装存在差异。标准输出缓冲区永久空闲,因此很难直观演示非阻塞输出的效果。

3.8 非阻塞IO知识图谱(不含代码部分)

3.9 代码(艾莉丝的笔记截图 + 分析)

下面艾莉丝会详细阐述的,这里先展示一下艾莉丝笔记。

由此可见:

3.9.1 补充两个细节

3.9.2 测试

3.10 阻塞IO代码

为直观区分阻塞与非阻塞运行差异,先给出原生阻塞读取标准输入完整代码,不做任何fcntl非阻塞设置。

3.10.1 阻塞IO的Demo代码

cpp 复制代码
#include <iostream>
#include <unistd.h>
using namespace std;

int main()
{
    char inbuffer[128];
    while(true)
    {
        inbuffer[0] = 0; // 缓冲区初始化清零
        ssize_t n = read(0, inbuffer, sizeof(inbuffer));
        if(n > 0)
        {
            inbuffer[n] = 0;
            cout << "标准输入内容:" << inbuffer << endl;
        }
        else if(n == 0)
        {
            cout << "输入流关闭,程序退出" << endl;
            break;
        }
        else
        {
            cout << "read调用出错,返回值n = " << n << endl;
        }
        sleep(1);
    }
    return 0;
}

3.10.2 阻塞代码运行现象

  1. 程序启动后无任何打印输出,直接卡死在read(0)调用,不会执行sleep(1)和循环打印;
  2. 只有键盘输入字符并按下回车后,read拿到缓冲区数据,才会打印内容并进入下一轮循环休眠;
  3. 按下Ctrl+Dread返回0,程序打印关闭提示并退出;
  4. 内核标准输入缓冲区会缓存整行输入,回车才会推送至内核缓冲区触发read返回。

3.11 非阻塞IO:工程实现代码

3.11.1 全量可直接编译代码(C++)

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>
using namespace std;

// 将指定文件描述符设置为非阻塞
void SetNonBlock(int fd)
{
    int flags = fcntl(fd, F_GETFL);
    if(flags < 0)
    {
        perror("fcntl F_GETFL 获取标志失败");
        return;
    }
    // 原有标志 | 非阻塞标记,写回文件状态
    fcntl(fd, F_SETFL, flags | O_NONBLOCK);
}

// 无数据就绪时执行其他业务逻辑
void DoOtherThing()
{
    cout << "当前无IO数据,执行其他业务逻辑" << endl;
}

int main()
{
    // 全局一次性设置标准输入为非阻塞,无需放入循环
    SetNonBlock(0);
    char inbuffer[128];
    while(true)
    {
        inbuffer[0] = 0;
        ssize_t n = read(0, inbuffer, sizeof(inbuffer));
        if(n > 0)
        {
            inbuffer[n] = 0;
            cout << "读取到输入数据:" << inbuffer << endl;
        }
        else if(n == 0)
        {
            cout << "标准输入流关闭,程序终止" << endl;
            break;
        }
        else
        {
            // 分层判断错误码,区分空轮询与真实故障
            if(errno == EAGAIN || errno == EWOULDBLOCK)
            {
                // 无数据就绪,合法场景,执行其他业务
                DoOtherThing();
                sleep(1);
                continue;
            }
            else if(errno == EINTR)
            {
                // 系统调用被信号打断,重试本次read
                continue;
            }
            else
            {
                // 真正不可恢复IO错误
                cout << "read发生致命错误,errno=" << errno << endl;
                break;
            }
        }
    }
    return 0;
}

3.11.2 关键实现说明

  1. SetNonBlock(0)放在while循环外部,仅执行一次,文件描述符属性永久生效;
  2. 引入<errno.h>全局错误码变量,用于区分非阻塞空轮询和真实IO故障;
  3. 拆分三层错误判断:无数据就绪、信号中断、致命错误,覆盖全部非阻塞异常场景;
  4. 封装DoOtherThing函数模拟等待间隙的业务处理,体现非阻塞核心价值。

3.12 非阻塞errno错误码

3.12.1 EAGAIN / EWOULDBLOCK 内核宏定义与含义

Linux内核头文件中两个宏定义值完全相等,语义一致:

c 复制代码
#define EAGAIN      11  /* Try again */
#define EWOULDBLOCK 11  /* Operation would block */

含义:当前文件描述符无可用数据,本次IO操作会阻塞进程,属于预期内正常现象,不是程序故障

  • EWOULDBLOCK:字面含义「操作将会阻塞」,更贴合非阻塞场景;
  • EAGAIN:字面含义「重试一次」,提示程序轮询重试read调用; 所有Linux发行版二者数值统一为11,代码中做或判断兼容两种写法。

3.12.2 错误分层判断必要性

若不区分errno == 11的场景,直接判定n<0为错误,会出现持续打印错误日志、误判程序崩溃,业务逻辑无法执行。必须优先捕获EAGAIN/EWOULDBLOCK分支,跳过错误告警,执行轮询业务。

3.13 中断错误EINTR处理

3.13.1 EINTR触发场景

read阻塞/非阻塞系统调用执行过程中,进程收到外部信号(SIGINT、SIGALRM、SIGCHLD等),内核会强行中断本次IO调用,read返回-1,同时errno=EINTR。 典型场景:

  1. 程序运行时按下Ctrl+C发送SIGINT;
  2. 定时器信号、子进程退出SIGCHLD打断IO;
  3. 自定义异步信号触发。

3.13.2 修复逻辑

EINTR属于临时中断,无数据丢失,只需continue跳过本次循环,重新执行read重试读取,不需要退出程序。代码中单独开辟else if(errno == EINTR)分支处理。

3.14 非阻塞轮询业务拓展说明

非阻塞IO核心优势:在无网络/终端数据时,CPU不会阻塞死等,可并行执行计算、日志、心跳检测、其他fd读写等业务。示例中DoOtherThing()函数就是业务载体,实际工程可填充:

  1. 定时心跳发包;
  2. 内存数据计算;
  3. 其他文件/套接字读写;
  4. 日志落盘、状态上报。

3.15 非阻塞IO综合全流程测试

3.15.1 编译命令

bash 复制代码
g++ TestNonBlock.cc -o TestNonBlock
./TestNonBlock

3.15.2 空载运行现象(无键盘输入)

程序每秒循环打印当前无IO数据,执行其他业务逻辑,不会卡死,持续占用少量CPU做轮询。

3.15.3 输入数据现象

键盘输入123abc回车后,立即打印读取到输入数据:123abc,下一轮恢复业务打印;

3.15.4 Ctrl + D关闭输入流

read返回0,打印关闭提示,程序正常退出;

3.15.5 Ctrl + C信号中断测试

运行中按下Ctrl + C触发SIGINT,代码捕获EINTR,不会崩溃,继续循环轮询。

3.15.6 性能缺陷补充

纯非阻塞单fd轮询会持续循环占用CPU,大量fd场景会造成CPU满载,因此高并发生产环境优先使用IO多路复用select/poll/epoll替代裸非阻塞轮询。

3.16 非阻塞IO代码相关知识图谱

3.17 非阻塞IO的完整代码

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <errno.h>

void SetNoBlock(int fd)
{
    int flags = fcntl(fd,F_GETFL);
    if(flags < 0)
    {
        perror("fcntl");
        return;
    }

    fcntl(fd,F_SETFL,flags | O_NONBLOCK);
}

void doOtherThing()
{
    std::cout << "read 0,0 fd data not ready!" << std::endl;
}

int main()
{
    // 一次设置信号,永久有效
    SetNoBlock(0);

    char inbuffer[128];
    while(true)
    {
        inbuffer[0] = 0;    // O(1) 初始化
        ssize_t n = read(0,inbuffer,sizeof(inbuffer));
        if(n > 0)
        {
            inbuffer[n] = 0;
            std::cout << "inbuffer: " << inbuffer << std::endl;
        }
        else if(n == 0)
        {
            // 告诉一声,我输入完毕了
            std::cout << "enter done,break" << std::endl;
            break;
        }
        else
        {
            // 1. 非阻塞模式下,只能以出错形式返回
            // 2. 底层没有数据,read返回,算不算出错?不算!!
            // 3. 如果返回值<0,你下来关系什么?因为什么原因出错的?错误码更详细的标识出错原因!
            // 4. errno == 11, EAGAIN || EWOULDBLOCK 不是真正的出错,只是表明fd没有就绪!
            if(errno == EAGAIN || errno == EWOULDBLOCK)
            {
                doOtherThing();
                sleep(1);

                std::cout << "read 0,0 fd data not ready!" << std::endl;
                continue; 
            }
            else if(errno == EINTR)
            {
                continue;
            }
            else{
                std::cout << "read error: n = " << n << " , errno = " << errno << std::endl;
            }
        }

        // 不要让read读太快了
        sleep(1);
    }

    return 0;
}

3.18 实操踩坑细节汇总

  1. 禁止将SetNonBlock放入while循环:重复叠加O_NONBLOCK标记无意义,浪费系统调用;
  2. 缓冲区必须每次循环清零:残留上一轮数据会造成字符串拼接错乱;
  3. read读取字节数n必须作为字符串结束符下标:inbuffer[n] = 0,否则打印乱码;
  4. sleep降低轮询频率:无sleep时循环极速刷屏,CPU占用100%;
  5. 错误码判断顺序不可颠倒:必须先判断EAGAIN/EWOULDBLOCK,再判断其他错误;
  6. 区分标准输入fd=0、标准输出fd=1、标准错误fd=2,输出fd几乎无法演示非阻塞;
  7. 回车是标准输入行缓冲触发条件,无回车输入不会送入内核缓冲区;
  8. EINTR中断必须单独处理,否则信号会直接打断程序业务循环。

4 ~> 全文总结

本文完整覆盖 Linux 网络编程 IO 体系的全部基础内容,从底层本质到模型分类、再到代码实操,核心要点梳理如下。

1. IO 本质与优化核心

  1. IO 的通用公式:IO = 等待数据 + 数据拷贝,该公式适用于所有输入、输出场景;
  2. IO 低效的根源:等待环节的时间占比过高,拷贝是硬件动作,无法通过软件大幅优化;
  3. 高效 IO 的判定标准:单位时间内,等待环节的时间占比越低,IO 效率越高;所有 IO 模型的迭代,本质都是优化「等待」的处理方式。

2. 五种 IO 模型(钓鱼类比 + 分类)

  1. 五大模型与人物对应:阻塞 IO (张三)、非阻塞 IO (李四)、信号驱动 IO (王五)、IO 多路复用 (赵六)、异步 IO (田七)
  2. 顶层分类(同步 / 异步 IO)
    1. 同步 IO :进程参与等待 / 拷贝任意环节,包含阻塞、非阻塞、信号驱动、IO 多路复用四类;
    2. 异步 IO:进程完全不参与 IO 流程,仅原生 POSIX 异步 IO 属于此类;
  3. 效率排序:IO 多路复用综合效率最高,是高并发网络编程首选模型;原生异步 IO 目前使用场景较少,逐步被协程替代。

3. 各模型核心特征

  1. 阻塞 IO:系统调用无数据则进程死等,默认套接字均为阻塞模式,实现最简单;
  2. 非阻塞 IO:无数据立即返回错误,需代码循环轮询,CPU 消耗高,适合特定场景;
  3. 信号驱动 IO :依靠SIGIO信号通知数据就绪,无需主动轮询,仍需进程完成拷贝;
  4. IO 多路复用 :通过select监听多个文件描述符,一次等待多个事件,是高并发核心;
  5. 异步 IO:内核全权执行所有 IO 流程,进程仅接收最终结果。

4. 同步 / 异步 IO 关键区分

判定唯一标准:进程是否参与 IO 两大环节(等待、拷贝)。该概念和「进程 / 线程执行流同步」是完全独立的两个知识点,不可混淆。

5. 非阻塞 IO 核心技术:fcntl

  1. fcntl是动态修改已打开 fd 属性的核心系统调用,五大功能中获取 / 设置文件状态标记是实现非阻塞的关键;
  2. 非阻塞核心标志:O_NONBLOCK,通过fcntl(F_GETFL)读取原有属性,fcntl(F_SETFL)叠加标记实现非阻塞;
  3. 工具函数SetNoBlock可通用化设置任意 fd 为非阻塞,仅需调用一次永久生效。

6. 代码与返回值重点

  1. 信号驱动 IO 代码:依赖SIGIO信号、F_SETOWNO_ASYNC标记,信号回调中执行数据读取;
  2. POSIX 异步 IO 代码:使用aio_read,编译必须链接-lrt实时库;
  3. read返回值规则:非阻塞场景下read_size < 0大概率是数据未就绪,并非程序错误,需要结合错误码做逻辑区分;
  4. 非阻塞 IO 演示优先使用标准输入 (fd=0),标准输出因缓冲区特性不适合测试。

7. 实操工程化知识点

7.1 阻塞与非阻塞对照基准

  1. 原生阻塞代码运行特征:read无数据时全程卡死,不会执行后续循环逻辑;
  2. 非阻塞改造核心改动:全局一次性调用SetNonBlock修改fd属性,循环不再阻塞。

7.2 errno错误码分层处理体系

  1. EAGAIN/EWOULDBLOCK(值11):无数据就绪,合法空轮询场景,执行业务后sleep重试;
  2. EINTR:系统调用被外部信号打断,直接continue重试read;
  3. 其余errno:真实不可恢复IO故障,终止循环退出。

7.3 非阻塞业务拓展价值

  1. 轮询间隙可并行执行其他计算、网络、定时任务,解决阻塞IO卡死无法处理其他逻辑的痛点;
  2. 纯裸非阻塞单fd适用轻量场景,多fd高并发必须使用IO多路复用降低CPU消耗。

7.4 实操高频踩坑清单

  1. 缓冲区未清零、未设置字符串结束符导致打印乱码;
  2. 未sleep造成循环极速轮询,CPU占用满载;
  3. 未区分错误码,将空轮询误判为程序崩溃;
  4. SetNonBlock放入循环重复执行,浪费系统调用;
  5. 未处理EINTR信号中断,外部信号直接打断业务流程;
  6. 混淆标准输入/输出文件描述符,选用fd=1做非阻塞测试无效果。

8. 学习路线总结

  1. 基础必学:IO本质 → 阻塞/非阻塞IO对照区分 → fcntl与O_NONBLOCK → errno错误分层处理 → IO多路复用(核心重点);
  2. 拓展学习:信号驱动IO、原生异步IO(了解原理即可);
  3. 进阶方向:掌握多路复用后,学习epoll、协程等高并发现代IO方案,规避裸非阻塞轮询CPU性能缺陷。

结尾

uu们,本文的内容到这里就全部结束了,艾莉丝在这里再次感谢您的阅读!

|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| ### 艾莉丝努力练剑 C/C++ & Linux 底层探索者 | 一个正在努力练剑的技术博主 *** ** * ** *** 👀 【关注】 跟随我一起深耕技术领域,见证每一次成长。 ❤️ 【点赞】 让优质内容被更多人看见,让知识传递更有力量。 ⭐ 【收藏】 把核心知识点存好,在需要时随时查、随时用。 💬 【评论】 分享你的经验或疑问,评论区一起交流避坑! 不要忘记给博主"一键四连"哦! "今日练剑达成!" "技术之路难免有困惑,但同行的人会让前进更有方向。" |

结语:希望对学习Linux相关内容的uu有所帮助,不要忘记给博主"一键四连"哦!

往期回顾

前一篇其实应该是【NAT、代理服务、内网穿透和内网打洞】,果然因为相关审核原因被强制下线了(😭):

兄弟们、大佬们,改了名字,又审核通过啦!!!

【Linux网络】NAT、内网穿透、内网打洞

🗡博主在这里放了一只小狗,大家看完了摸摸小狗放松一下吧!🗡 ૮₍ ˶ ˊ ᴥ ˋ˶₎ა

相关推荐
VidDown1 小时前
视频协议传输全解析:从 HTTP/HTTPS 到 HLS/DASH 的完整旅程
javascript·网络·http·https·编辑器·音视频·视频编解码
lihao lihao1 小时前
Linux线程同步与互斥
linux·数据结构·算法
盛码笔记1 小时前
计算机网络
网络
Dylan的码园1 小时前
python基础与快速入门
开发语言·python
zzz_23681 小时前
【Java基础】HashMap——为什么JDK 7扩容会死循环,JDK 8又是怎么修好的
java·开发语言
程序猿乐锅1 小时前
JavaSE 总复习:语法到多线程全梳理
java·开发语言
云器科技1 小时前
云器技术问答 Vol.2:揭秘通用增量计算
java·开发语言
云飞云共享云桌面1 小时前
集中算力・统一数据・高效协同:SolidWorks 云桌面方案详解
运维·服务器·人工智能·安全·3d·电脑·制造
爱喝水的鱼丶1 小时前
SAP-ABAP:SAP表与视图权限管控方案:表维护权限、视图访问权限配置实操
运维·数据库·性能优化·sap·abap·权限·表和视图