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

IO 中的阻塞、非阻塞、同步、异步及五种IO模型

在网络编程的世界里,IO 操作的效率和处理方式是影响系统性能的关键因素。而理解阻塞、非阻塞、同步、异步这四个核心概念,是掌握高性能网络编程的基础。本文将深入剖析这些概念,通过典型的 IO 操作过程,揭示它们的本质区别和应用场景。

目录

一次 IO 的两个典型阶段

一、数据准备阶段

1. 阻塞模式

2. 非阻塞模式

二、数据读写阶段

1. 同步模式

2. 异步模式

三、关键结论

四、与业务中并发的同步异步区分

五、总结:IO 中的阻塞、非阻塞、同步、异步

Linux 上的五种 IO 模型

一、阻塞 IO 模型(Blocking IO)

二、非阻塞 IO 模型(Non-blocking IO)

三、IO 复用模型(IO Multiplexing)

四、信号驱动 IO 模型(Signal-Driven IO)

五、异步 IO 模型(Asynchronous IO)

 六、五种 IO 模型对比

七、关键区别总结


一次 IO 的两个典型阶段

在探讨阻塞、非阻塞、同步、异步之前,我们需要先明确一个典型的网络 IO 操作所包含的两个阶段:数据准备(数据就绪)阶段和数据读写阶段。这两个阶段的处理方式不同,导致了不同的 IO 模型。

一、数据准备阶段

数据准备阶段是指系统 IO 操作检测数据是否就绪的过程。根据系统对 IO 操作就绪状态的处理方式,可分为阻塞和非阻塞两种模式。

1. 阻塞模式

在阻塞模式下,当调用如recv这样的 IO 接口时,如果目标套接字(sockfd)上没有数据到来,当前线程会被阻塞,进入等待状态,直到数据到达。下面是一个典型的阻塞 IO 调用示例:

int size = recv(sockfd, buf, 1024, 0);

在这个例子中,如果sockfd对应的内核 TCP 接收缓冲区中没有数据,recv函数会一直等待,不会返回,直到有数据到达或者连接关闭。这种方式的优点是代码逻辑简单,缺点是线程在等待过程中无法执行其他任务,会造成资源浪费,尤其在高并发场景下问题更为突出。

2. 非阻塞模式

非阻塞模式则不同,当设置套接字为非阻塞模式后,如果调用recv时目标套接字上没有数据,函数会立即返回,而不会阻塞当前线程。此时,需要通过返回值来判断数据是否就绪:

int size = recv(sockfd, buf, 1024, 0);
if (size == -1 && errno == EAGAIN) {// 数据尚未准备好,这是正常的非阻塞返回// 可以继续执行其他任务或再次尝试读取
} else if (size == 0) {// 对端关闭了连接
} else if (size > 0) {// 数据已就绪并成功读取
} else {// 发生其他错误,需要进行错误处理
}

在非阻塞模式下,通常需要在循环中不断检查返回值,直到数据就绪。这种方式虽然避免了线程阻塞,但如果数据长时间未就绪,会导致 CPU 空转,浪费 CPU 资源。因此,非阻塞 IO 通常需要配合多路复用技术(如 select、poll、epoll)一起使用,以提高效率。

二、数据读写阶段

数据读写阶段是指将数据从内核缓冲区传输到应用程序缓冲区,或者从应用程序缓冲区传输到内核缓冲区的过程。根据应用程序与内核的交互方式,可分为同步和异步两种模式。

1. 同步模式

在同步模式下,数据的读写操作由应用程序自己完成。当调用recv等同步 IO 接口时,如果数据已就绪,函数会将数据从内核的 TCP 缓冲区复制到应用程序提供的缓冲区中(这个过程由应用程序执行)。在这个数据拷贝过程中,代码会阻塞在recv函数处,直到数据拷贝完成才会返回。例如:

int size = recv(sockfd, buf, 1024, 0);

这里的recv就是一个典型的同步 IO 接口。即使在非阻塞模式下,只要数据就绪后进行读写操作时,应用程序仍需要等待数据传输完成,因此非阻塞 IO 在数据读写阶段仍然属于同步 IO。

2. 异步模式

异步模式则完全不同。在异步 IO 中,数据的读写操作由内核负责完成,应用程序只需向内核发起 IO 请求,并指定当操作完成时的通知方式,然后就可以继续执行其他业务逻辑。当内核完成数据的读写操作后,会通过事先约定的方式(如信号或回调函数)通知应用程序。例如:

#include <aio.h>// 定义异步IO控制块
struct aiocb aiocb;// 初始化aiocb结构
memset(&aiocb, 0, sizeof(struct aiocb));
aiocb.aio_fildes = sockfd;
aiocb.aio_buf = buf;
aiocb.aio_nbytes = 1024;
aiocb.aio_offset = 0;// 设置回调函数(当IO完成时调用)
aiocb.aio_sigevent.sigev_notify = SIGEV_CALLBACK;
aiocb.aio_sigevent.sigev_notify_function = my_callback_function;
aiocb.aio_sigevent.sigev_notify_attributes = NULL;// 发起异步读操作
int ret = aio_read(&aiocb);
if (ret != 0) {// 处理错误
}// 继续执行其他业务逻辑,无需等待IO完成

在这个例子中,aio_read函数会立即返回,不会阻塞当前线程。当数据从内核缓冲区复制到应用程序缓冲区完成后,内核会调用my_callback_function函数通知应用程序。这种方式使得应用程序在 IO 操作进行过程中可以继续执行其他任务,大大提高了并发处理能力。

三、关键结论

需要特别强调的是,在处理 IO 时,阻塞和非阻塞实际上都属于同步 IO 范畴,只有使用像aio_readaio_write这样的特殊 API 才是真正的异步 IO。这是因为,无论是阻塞 IO 还是非阻塞 IO,当数据就绪后进行读写操作时,应用程序都需要等待数据传输完成(即使是非阻塞 IO,也需要通过轮询不断检查状态),而真正的异步 IO 则是由内核完全接管数据传输,应用程序无需等待。

来自muduo作者陈硕:

  ***在处理IO时,阻塞和非阻塞都是同步IO,只有使用特殊的API才是异步IO***

四、与业务中并发的同步异步区分

在业务开发中,也经常会提到同步和异步的概念,但这与 IO 模型中的同步异步有所不同,需要加以区分:

  • 业务中的同步:是指操作 A 需要等待操作 B 完成后才能继续执行后续逻辑。例如,在调用一个远程 API 时,程序会等待 API 返回结果后再继续执行下一步。
  • 业务中的异步:是指操作 A 向操作 B 发起请求,并告知 B 自己感兴趣的事件以及事件发生时的通知方式,然后操作 A 就可以继续执行自己的业务逻辑。当操作 B 监听到相应事件发生后,会按照约定的方式通知操作 A,A 再进行相应的数据处理。例如,在消息队列系统中,生产者发送消息后不需要等待消费者处理结果,可以继续执行其他任务,消费者处理完消息后可以通过回调或消息通知生产者。

五、总结:IO 中的阻塞、非阻塞、同步、异步

综上所述,一个典型的网络 IO 接口调用可以分为 “数据就绪(数据准备)” 和 “数据读写” 两个阶段:

  • 在数据就绪阶段,根据是否阻塞当前线程,分为阻塞和非阻塞两种模式。
  • 在数据读写阶段,根据是由应用程序还是内核负责完成数据传输,分为同步和异步两种模式。

Linux 上的五种 IO 模型

在 Linux 系统中,根据数据准备和数据传输阶段的不同处理方式,可将 IO 模型分为五类。这些模型从简单到复杂,逐步提升系统在高并发场景下的处理能力。

下文图片来源:【Linux高级IO】五种IO模型_【linux】五种io模型之高性能io技术详解-CSDN博客

一、阻塞 IO 模型(Blocking IO)

阻塞 IO 是最基本的 IO 模型,其核心特点是在数据准备和数据传输阶段均会阻塞进程。以网络套接字为例:

// 创建套接字并连接服务器
int sockfd = socket(AF_INET, SOCK_STREAM, 0);
connect(sockfd, (struct sockaddr*)&server_addr, sizeof(server_addr));// 调用 recv 接收数据(默认阻塞模式)
char buffer[1024];
int n = recv(sockfd, buffer, 1024, 0);  // 进程在此处阻塞// 数据就绪并复制完成后继续执行
process_data(buffer, n);

 

工作流程

  1. 进程调用 recv 进入内核态
  2. 若数据未就绪(TCP 缓冲区为空),进程被挂起(进入睡眠状态)
  3. 数据到达后,内核将数据从网卡复制到内核缓冲区
  4. 内核将数据复制到用户空间缓冲区
  5. recv 返回,进程恢复执行

特点

  • 实现简单,代码逻辑清晰
  • 但同一时间每个进程只能处理一个 IO 请求
  • 在高并发场景下需要大量进程 / 线程,资源消耗大

二、非阻塞 IO 模型(Non-blocking IO)

非阻塞 IO 通过设置套接字为非阻塞模式,避免在数据准备阶段阻塞进程:

// 设置套接字为非阻塞模式
int flags = fcntl(sockfd, F_GETFL, 0);
fcntl(sockfd, F_SETFL, flags | O_NONBLOCK);// 循环尝试读取数据
while (1) {int n = recv(sockfd, buffer, 1024, 0);if (n == -1 && errno == EAGAIN) {// 数据未就绪,继续处理其他任务handle_other_tasks();} else if (n > 0) {// 数据就绪,处理数据process_data(buffer, n);break;} else {// 处理错误handle_error();break;}
}

 

工作流程

  1. 进程调用 recv 立即返回(无论数据是否就绪)
  2. 若数据未就绪,返回 EAGAIN (等同于EWOULDBLOCK)错误
  3. 进程可继续执行其他任务,定期轮询检查数据状态
  4. 数据就绪后,再次调用 recv 完成数据复制

特点

  • 避免进程阻塞,可在等待期间处理其他任务
  • 但频繁轮询会消耗大量 CPU 资源
  • 适用于 IO 就绪时间短的场景

三、IO 复用模型(IO Multiplexing)

IO 复用模型通过单个进程同时监视多个文件描述符(FD),提高并发处理能力。常见的实现有 selectpoll 和 epoll

// 使用 select 实现 IO 复用
fd_set readfds;
struct timeval timeout;// 初始化文件描述符集合
FD_ZERO(&readfds);
FD_SET(sockfd, &readfds);
FD_SET(other_fd, &readfds);// 设置超时时间
timeout.tv_sec = 5;
timeout.tv_usec = 0;// 调用 select 监视多个 FD
int activity = select(max_fd + 1, &readfds, NULL, NULL, &timeout);if (activity > 0) {// 检查哪些 FD 就绪if (FD_ISSET(sockfd, &readfds)) {// 处理套接字数据recv(sockfd, buffer, 1024, 0);}if (FD_ISSET(other_fd, &readfds)) {// 处理其他 FD}
}

 

工作流程

  1. 进程调用 select/poll/epoll_wait 进入阻塞状态
  2. 内核监视所有注册的 FD,任一 FD 就绪时唤醒进程
  3. 进程遍历 FD 集合,找出就绪的 FD 进行处理
  4. 对就绪的 FD 调用 recv 完成数据复制

特点

  • 单个进程可同时处理多个 IO 请求
  • 相比多进程 / 线程模型,资源消耗显著降低
  • epoll 在大规模 FD 场景下性能更优(时间复杂度 O (1))

四、信号驱动 IO 模型(Signal-Driven IO)

信号驱动 IO 使用异步通知机制,当数据就绪时通过信号通知进程:

// 安装信号处理函数
void sigio_handler(int signo) {// 处理数据就绪事件recv(sockfd, buffer, 1024, 0);
}// 设置信号处理
signal(SIGIO, sigio_handler);// 设置套接字为异步模式并绑定进程
fcntl(sockfd, F_SETOWN, getpid());
int flags = fcntl(sockfd, F_GETFL);
fcntl(sockfd, F_SETFL, flags | FASYNC);// 进程继续执行其他任务
while (1) {// 处理核心业务逻辑process_main_logic();// 无需主动检查 IO 状态
}

 

工作流程

  1. 进程通过 fcntl 设置套接字为异步模式并注册信号处理函数
  2. 内核在数据就绪时发送 SIGIO 信号给进程
  3. 进程在信号处理函数中调用 recv 完成数据复制

特点

  • 数据准备阶段非阻塞,进程可继续执行主逻辑
  • 相比轮询方式,减少了 CPU 消耗
  • 但信号处理函数可能干扰主程序执行流程

在第一阶段是异步的,在第二阶段是同步的;与非阻塞IO的区别在于它提供了消息通知机制,不需要用户进程不断轮询检查,减少了系统API调用次数,提高效率。

五、异步 IO 模型(Asynchronous IO)

真正的异步 IO 模型中,进程只需发起 IO 请求,内核完成整个数据传输过程后通知进程:

#include <aio.h>// 定义异步 IO 控制块
struct aiocb aiocb;// 初始化控制块
memset(&aiocb, 0, sizeof(aiocb));
aiocb.aio_fildes = sockfd;
aiocb.aio_buf = buffer;
aiocb.aio_nbytes = 1024;
aiocb.aio_offset = 0;// 设置完成回调
aiocb.aio_sigevent.sigev_notify = SIGEV_THREAD;
aiocb.aio_sigevent.sigev_notify_function = io_complete_handler;
aiocb.aio_sigevent.sigev_notify_attributes = NULL;// 发起异步读操作
aio_read(&aiocb);// 进程继续执行其他任务,无需等待
process_other_work();

 

工作流程: 

  1. 进程调用 aio_read 发起异步请求,立即返回
  2. 内核在后台完成数据准备和数据复制操作
  3. 数据完全传输到用户空间后,通过回调函数通知进程

特点

  • 整个 IO 过程(包括数据准备和传输)均非阻塞
  • 进程无需主动干预 IO 操作,效率最高
  • 需操作系统和应用程序共同支持(如 Linux 的 aio 系列函数)

 六、五种 IO 模型对比

IO 模型数据准备阶段数据传输阶段进程状态典型应用场景
阻塞 IO阻塞阻塞挂起等待简单单线程应用
非阻塞 IO非阻塞阻塞轮询检查实时性要求不高的小并发场景
IO 复用阻塞阻塞单进程监视多 FD高并发网络服务器
信号驱动 IO非阻塞阻塞信号回调实时性要求较高的场景
异步 IO非阻塞非阻塞完全无感知高性能数据库、流媒体服务器

七、关键区别总结

  1. 同步 vs 异步

    • 同步 IO(阻塞、非阻塞、IO 复用、信号驱动):进程需要主动参与数据传输过程
    • 异步 IO:内核完全负责数据传输,完成后通知进程
  2. 阻塞 vs 非阻塞

    • 阻塞:进程在数据准备或传输阶段被挂起
    • 非阻塞:进程可继续执行其他任务,通过轮询或回调处理 IO

相关文章:

  • 如何更新和清理 Go 依赖版本
  • flutter使用html_editor_enhanced: ^2.6.0后,编辑框无法获取焦点,无法操作
  • 4.8.4 利用Spark SQL实现分组排行榜
  • 2021年认证杯SPSSPRO杯数学建模D题(第二阶段)停车的策略全过程文档及程序
  • 手机如何压缩文件为 RAR 格式:详细教程与工具推荐
  • python:selenium爬取网站信息
  • 华为手机用的时间长了,提示手机电池性能下降,需要去换电池吗?平时要怎么用能让电池寿命长久一些?
  • 8卡910B4-32G测试Qwen2.5-VL-72B-instruct模型兼容性
  • 什么是数字化转型,如何系统性重构业务逻辑
  • SD-WAN 与传统网络方案组合应用:降本增效的政务网建设新策略
  • mac 下安装Rust Toolchain(Nightly)
  • CORS跨域资源共享解析
  • EFcore8和Sql Server 2014冲突
  • WebAssembly 及 HTML Streaming:重塑前端性能与用户体验
  • 【Doris基础】Apache Doris 基本架构深度解析:从存储到查询的完整技术演进
  • 无人机分布式协同算法解析!
  • 考研系列-操作系统:第二章、进程与线程
  • Screen 连接远程服务器(Ubuntu)
  • 视觉中国:镜头下的中国发展图景
  • BGP实验报告
  • 专做五金正品的网站/关键词优化系统
  • 在线做c 题的网站/快速排序优化
  • 怎么做网站倒计时/竞价外包推广
  • 太原网站建设51sole/推广产品最好的方式
  • 公司做网络推广哪个网站好/商丘seo推广
  • 网站如何不被收录/平台运营