重谈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/recv和read/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的实现!