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

重谈IO——五种IO模型及其分类

文章目录

  • 五种IO模型及其分类
    • 重谈IO的原因——高效IO模式
    • 五种IO模型及举例解释
    • 解释不同模型细节
    • IO模型执行流
    • 非阻塞IO的简单实现
      • send/recv使用选项实现非阻塞
      • open使用选项实现非阻塞
      • 使用fcntl(推荐)
      • 简单实验——阻塞和非阻塞

五种IO模型及其分类

早在讲解HTTP的时候,我们就已经说过:
我们上网的本质,其实就是和对方主机进行数据交换,本质就是IO!

但是,我们之前对IO的理解还是比较简单的,几乎就仅限于是双方进行通信。本篇文章开始,将正式地重新理解IO这个概念,以及给出IO的多种模型!

重谈IO的原因——高效IO模式

首先,我们需要引入一个非常重要的概念, 可以说是我们后序理解模型的指导思想

我们曾经做过一个实验:即让程序在指定时间内分别执行自增操作或者printf!

最终这个实验的结果就是:自增操作的次数是远远大于printf的操作的!
当时简单地输出了一个结论:IO是比较慢的。
Tips:这里的慢其实是相对于CPU来说的!

但是,我们只知道IO慢,但是没有搞清楚IO的本质是什么!即为什么IO会慢?


不管是直接读写文件也好,还是网络通信,在Linux系统下都是以文件描述符fd来作为载体进行Input/Output的!
这必然会有一个问题:拿到fd了,就一定有数据读吗?就一定能向fd对应的文件写吗?

当然是不一定了
比如从键盘(fd = 0)中读,如果我们始终不输入,或者不按下回车,那么系统根本读不到!
又或是网络通信的时候:
对方的接收缓冲区满了,这边就需要进行流量控制、或者是先不进行发送。
如果我方的写缓冲区满了,那也是没有办法往里面写的!

不管是文件的读写,还是网络通信来说,读写的本质是我们用户自行把内容读上来吗?
那肯定是不是:文件读写是把磁盘中文件的内容拷贝到用户缓冲区,网络通信的本质是操作系统自行选择合适的时机,把数据从一方的缓冲区传输到另一方的缓冲区。
👉这本质就是数据拷贝罢了

数据拷贝对于计算机来说是比较简单的事情,是很快的。
所以,真正导致IO慢的原因就是:IO需要等待条件就是,一个字——等!

read接口,它是用于Input的!过程:等待 + 拷贝
write接口,它是用于Output的!过程:等到 + 拷贝

所以,IO操作 = 等待 + 拷贝,IO的效率取决于等待!
👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇👇既然IO低效的原因是等待时间,所以,要想提高IO的效率,就得尽可能地减少等待的时间!

最后输出结论:
所谓高效的IO,其实就是单位时间内等待的时间小!也就是单位时间内等待时间的比重小!

五种IO模型及举例解释

有了上述的指导思想,后序我们学习五种IO模型的时候就比较轻松了。

接下来,我们将做两件事情:
1.引出五种IO模型(先不了解原理)
2.在了解五种IO模型之前,先举一个例子,进行通识的理解


上面说到:IO比较慢的原因,也解释了什么是高效的IO。那么,后序介绍的IO模型,必然是有好有坏,并且从这几个方面来出发,进行模型分析!

首先就来看看,五种IO模型分别叫什么名字:

1.阻塞IO
2.非阻塞IO
3.信号驱动IO
4.多路转接、多路复用
5.异步IO

光看名字肯定是不太好理解的,下面我们先通过一个简单的例子来进行上述模型的解释!


这里以钓鱼佬的例子来进行解释

前景:现在有五个钓鱼佬:A、B、C、D、E,它们先后进入一个钓鱼场去钓鱼。

钓鱼佬的例子就放在下面这张图里面:
在这里插入图片描述

上述我们以一个简单的例子,简单地理解了一下上面提出的五种IO模型的概念!

解释不同模型细节

细节1:阻塞和非阻塞的区别
A和B就阻塞和非阻塞的区别。阻塞和非阻塞的原因都是因为等待条件是否就绪!
这里钓鱼的例子就是鱼是否上钩。

IO条件不具备,所以阻塞方式会进行阻塞等待。反之发现IO条件不具备,就会出错返回,等待下一次再来进行检查。但是,非阻塞IO真的就效率高吗?

注意我们这里谈论的是IO的效率!IO = 等待 + 拷贝!
而非阻塞IO所谓的效率高,是指IO的效率吗?

非阻塞的情况下:
IO条件不具备就出错返回了。也没有办法进行IO操作!
所以,所谓的高效,是偷换概念的!不是IO效率高,而是单位时间内做事效率高!=

细节2:谁的钓鱼效率最高——本质就是哪一种IO模型的效率最高
答案:当然是D的效率高!也就是多路转接/复用的效率最高!

其实这是很容易想到的。但是我们要知道,为什么这种方式效率高?
我们还是以钓鱼的例子作为理解:不是说鱼竿多,就是效率高的!

因为ABCDF五个人去钓鱼,一共104根鱼竿,其中有100根是D的!
那么站在鱼的视角来看:它咬到D的鱼竿的钩子的概率是100/104。
👉这就导致了D鱼竿上钩的概率很大 👉D在单位时间内就等的比重就会减少了!

而IO效率高的本质就是:单位时间内的等待的比重小!

细节3:对于C来说,他有没有参与钓鱼的过程呢?
那当然是有的!只不过他相对于其他人来说:他并不需要进行条件是否就绪的检测过程!
他通过收到一个信号来判定,从而处理鱼竿。
所以,C参与的是真正钓鱼这个部分。

细节4:同步IO和异步IO
其实同步IO和异步IO这两个概念一直是比较有争议的。包括一些教材,一些大佬的认识。
但是,这里我们以自己的视角来看:
我们就认为 👉 只要有参与到IO过程的,都是同步IO,反之异步IO!

所以,上述的阻塞、非阻塞、信号驱动、多路转接/复用,都是同步IO!

本质上,这里的钓鱼者,在系统层面上就是一个个的进程!
所以,异步IO是什么情况呢?

异步IO就是:参与IO的进程只发起IO和IO流,但是他不关心底层IO的实现!
也就是说IO和该进程工作流是无关的!他只需要等待另一个工作流把结果返回给它!

但是需要注意的是:
这里的IO同步和线程的同步,并不是一个概念!这里需要特别注意!

IO模型执行流

阻塞IO

阻塞 IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式
阻塞IO也是最常用,最简单的IO模式!

在这里插入图片描述
其实就是读写系统调用的时候 ,内核会先检查读写条件是否就绪!
如果没有就绪,就会进行阻塞等待(进程状态切换为s)。
如果条件就绪(比如外部硬件就绪发送硬件中断),此时就会执行读写操作再返回成功指示。

非阻塞IO

非阻塞 IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK错误码.

在这里插入图片描述
其实就是读写系统调用多次的先进行询问条件是否就绪,如果未就绪就返回错误码。非阻塞IO 往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对 CPU 来说是较大的浪费, 一般只有特定场景下才使用。

其实轮询这个概念,我们在学习waitpid这个接口的时候就已经见过了!

信号驱动IO

信号驱动 IO:内核将数据准备好的时候, 使用SIGIO信号通知应用程序进行IO操作.

在这里插入图片描述
其实就是我们先进行捕捉信号,把对应的一种信号如SIGIO捕捉,自定义方法。
然后一旦IO条件准备就绪,就会接收到一个信号!

然后经过中断,由用户态切换到内核态,从而执行信号的处理方法。方法就是进行读写数据。
而信号返回来的时候,一定是IO条件就绪的!所以是一定能读写成功的。

这种方法其实用的比较少!虽然它看着不错,但是还是有些缺陷的。
比如信号的屏蔽:
在这里插入图片描述
后序也会有更好的办法能实现信号这种通知功能的!

IO多路转接,多路复用

IO 多路转接:
从流程图上看起来和阻塞IO类似。实际上最核心在于IO多路转接能同等待多个文件描述符的就绪状态

在这里插入图片描述
我们通常用的一些系统调用:
read/recv/recvfrom write/send/sendto,这些接口功能是检测条件就绪 + 拷贝操作

但是在IO多路转接的情况下:
有一些接口是可以专门做多个文件描述符fd的等待的!比如图中展示的select
一旦有一个条件就绪,就告诉用户,然后进行IO读取。

IO多路转接也是最常用的一种方式!我们后续会重点学习的。这里先了解即可。

异步IO

异步 IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)

在这里插入图片描述

其实就是通过类似于aio_read的接口,发起一个IO/IO流。让另一个执行流进行操作。
对于发起IO的进程来说,他就不再关心底层IO实现的等待、读取了。只关心最后得到的结果!

非阻塞IO的简单实现

我们曾经学过如何进行非阻塞式轮询的回收子进程。但是对于非阻塞的IO是不太理解的!
接下来我们将介绍一些方法,以及做一些实验理解如何实现非阻塞IO!

send/recv使用选项实现非阻塞

我们没有办法使用read/write,因为这两个默认是阻塞的!
但是我们在前面网络通信的实践中,使用过另外两个接口:send/recv

send/recvread/write是比较类似的,只不过多一个参数选项!

ssize_t recv(int sockfd, void *buf, size_t len, int flags);
ssize_t send(int sockfd, const void *buf, size_t len, int flags);

最后一个参数int flags就是用来进行选项控制的:
在这里插入图片描述
其中,选项种有一个叫MSG_DONTWAIT,就是非阻塞等待的选项!
使用这个后,send/recv就不会进行阻塞了!

open使用选项实现非阻塞

不过还有一种方式:

int open(const char *pathname, int flags);

打开文件的时候,也是可以传入选项的。
其中O_NONBLOCK or O_NDELAY就是表示,打开的文件不需要进行阻塞。

在这里插入图片描述

使用fcntl(推荐)

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

这个就是file control的意思。即控制文件!
如何控制? -> 通过传入的cmd进行控制。

复制一个现有的描述符(cmd=F_DUPFD)
获得/设置文件描述符标记(cmd=F_GETFD 或 F_SETFD)
获得/设置文件状态标记(cmd=F_GETFL 或 F_SETFL)
获得/设置异步 I/O 所有权(cmd=F_GETOWN 或 F_SETOWN)
获得/设置记录锁(cmd=F_GETLK,F_SETLK 或 F_SETLKW)

如果想要进行异步IO,我们需要这样做:

void SetNoBlock(int fd) {//1.先通过cmd(F_GETFL)获取到当前的同步/异步IO状态int fl = fcntl(fd, F_GETFL);//2.获取失败直接返回if (fl < 0) {perror("fcntl");return;}//3.成功后,重新进行设置,使用F_SETFL进行设置//设置的方法就是把需要的选项按位或起来,open的选项操作也是这样的!fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}

简单实验——阻塞和非阻塞

在了解了如何通过接口实现非阻塞IO的时候,我们就来做实验看看!

使用网络套接字来实现还是有点麻烦了。这里就简单地使用普通文件描述符做展示即可!

1.阻塞IO:

#include <iostream>
using namespace std;
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>//1.阻塞IOint main(){char buffer[1024] = {0};while(1){ssize_t n = read(0, buffer, sizeof(buffer)); //0就是stdin,即键盘if(n < 0){cout << "read error" << endl;exit(-1);}else{buffer[n] = 0;cout << buffer << endl;}}return 0;
}

在这里插入图片描述
这个阻塞IO很好实现,因为read本身就是默认阻塞的!

但是这里为什么打印出来的每次都要空一行呢?
因为我们是使用read系统调用!而不是语言库封装的一些读取函数!

read接口就是负责读取字节流的!所以,我们输入的字符中是有\r\n回车符的!
直接打印出来直接就被解析成空行了!但是语言库封装的函数会把这个忽略掉。

在这里插入图片描述
只需要把这个字符忽略掉即可,如上图所示!

2.非阻塞IO
很简单,直接拿刚刚展示的void SetNoBlock(int fd)进行设置非阻塞即可:

#include <iostream>
using namespace std;
#include <unistd.h>
#include <fcntl.h>
#include <sys/types.h>
#include <sys/stat.h>//2.非阻塞IOvoid SetNoBlock(int fd) {//1.先通过cmd(F_GETFL)获取到当前的同步/异步IO状态int fl = fcntl(fd, F_GETFL);//2.获取失败直接返回if (fl < 0) {perror("fcntl");return;}//3.成功后,重新进行设置,使用F_SETFL进行设置//设置的方法就是把需要的选项按位或起来,open的选项操作也是这样的!fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}int main(){//设置stdin非阻塞SetNoBlock(0);char buffer[1024] = {0};while(1){ssize_t n = read(0, buffer, sizeof(buffer)); //0就是stdin,即键盘if(n > 0){buffer[n - 1] = 0;cout << buffer << endl;}//按下ctrl + d,退出从标准输入的读取!else if(n == 0){break;}// n < 0else{//这里需要说的是:n < 0,一定代表读取出错吗?//如果在阻塞那里肯定是了!//但是现在时非阻塞! -> 有两种可能不是出错的: // 1.条件未就绪// 2.因为信号而退出读写//怎么判断?//我们可以发现,read返回值中,如果是出错了,会设置错误码到errno中!//所以我们通过查看错误码就知道是什么情况了!//1.条件未就绪:if(errno == EAGAIN || errno == EWOULDBLOCK){cout << "条件未就绪!" << endl;sleep(1);continue;}//2.被信号中断了else if(errno == EINTR){sleep(1);continue;}else{//这是真正的出错! 直接退出exit(-1);}}//这里打印一下 -> 如果说是非阻塞,这里是始终被打印的!cout << "非阻塞的打印" << endl;}return 0;
}

有一些细节直接写在代码注释上了,我们直接上结果:
在这里插入图片描述
至此,我们就实现了非阻塞IO的实现!

http://www.dtcms.com/a/393111.html

相关文章:

  • 数据库造神计划第十七天---索引(2)
  • 【开题答辩实录分享】以《车联网位置信息管理软件》为例进行答辩实录分享
  • (3)机器学习-模型介绍
  • 如何在 Ubuntu 20.04 LTS 上安装 MySQL 8
  • MuMu模拟器使用入门实践指南:从ADB连接到Frida动态分析
  • 条款5:优先选用auto, 而非显示类型声明
  • 强化学习原理(一)
  • 解读43页PPT经营分析与决策支持系统建设方案交流及解决经验
  • ubuntu24设置证书登录及问题排查
  • MySQL 备份与恢复完全指南:从理论到实战
  • 2011/12 JLPT听力原文 问题四
  • 实战free_s:在高并发缓存系统中落地“内存释放更安全——free_s函数深度解析与free全方位对比”
  • 异步通知实验
  • 用 C 语言模拟面向对象编程
  • 联邦学习论文分享:FedKTL
  • 智能体分类:从反应式到混合式的架构演进与实践
  • 【面板数据】上市公司企业ZF连接度数据集(1991-2024年)
  • 让codex像 cladue code一样 自动牛马
  • NeurIPS 2025 spotlight Autonomous Driving VLA World Model FSDrive
  • 多线程JUC
  • Qwen3技术之模型后训练
  • 服务端实现
  • 深入AQS源码:解密Condition的await与signal
  • ceph存储配置大全
  • 数据库造神计划第十六天---索引(1)
  • 【软件推荐】免费图片视频管理工具,让灵感库告别混乱
  • C语言入门教程 | 阶段二:循环语句详解(while、do...while、for)
  • GEO(Generative Engine Optimization)完全指南:从原理到实践
  • Msyql日期时间总结
  • IP地址入门基础