五种IO模型
目录
一、阻塞IO
二、非阻塞IO
三、信号驱动IO
四、多路转接
五、异步IO
六、同步和异步的区分
在前面的学习中,我们已经了解了网络通信的本质就是把数据拷贝到读写缓冲区中,然后由网卡接受/发送给对方,对方再从网卡中读取到数据的结果。但是我们在使用recv/send这样的接口的时候曾经说过,如果读写缓冲区没有就绪(读缓冲区没有数据/写缓冲区没有空间),程序会阻塞在该系统调用处。直到读写缓冲区中得到了资源,才会从阻塞状态重新工作。
这里我们可以提炼一个概念:IO=等资源就绪+拷贝。
所以如果一个服务器在运行的时候,有很多的客户端都和你建立了连接,但是却不发消息,让服务器阻塞在read等系统调用处,这样即使你的服务器采取了多线程多进程的模式,仍会很快就因为资源打满而卡死。但是你会发现,你服务器的性能降低的问题并不在于服务器本身的配置不够好,而是把大量的资源浪费在了IO的等待处!
所以为了提高服务器的性能,往往会采取把等待时间的比重降低的策略,来把更多资源用于处理数据。下面我们提到的5种IO模型就是围绕IO中等待是否高效进行的。
一、阻塞IO
阻塞IO就是我们最常使用的IO模型,在默认情况下所有的套接字都是采取的阻塞IO。阻塞IO即在操作系统将数据准备好之前,程序一直不往后执行。
显然,阻塞IO的效率非常低,因为他等待的占比太大了。
二、非阻塞IO
非阻塞IO顾名思义,就是在底层资源没有就绪的时候,不会傻傻的等待,而是直接返回-1,并且将错误码设置为EWOULDBLOCK/EAGAIN (这是全局变量errno)。然后在下一次查看资源是否就绪的期间内,可以做其他事情。这种方式通常会搭配循环使用,我们称之为轮询。
因为所有的套接字默认创造出来的时候就是阻塞等待的,那么如何将其修改为非阻塞等待呢?我们可以使用fcntl接口。
fcntl是File Control的缩写,是一个用于文件描述符的通用接口。它允许程序对文件描述符执行各种操作,包括获取和设置文件描述符的状态标志、文件锁定、文件描述符的复制等。在Unix和类Unix系统中,fcntl是一个非常重要的系统调用,广泛应用于文件操作、进程间通信、网络编程等领域。
fcntl函数的一般形式如下:
其中cmd可以有以下几种:
通常是先get获取文件描述符的状态标志,然后再set设置文件描述符的状态标志。
#include<fcntl.h>
#include<unistd.h>
#include<errno.h>
#include<cstdlib>
#include<iostream>
using namespace std;
int main() {
SetNonBlock(0);
while (1) {
char buffer[1024];
ssize_t s = read(0, buffer, sizeof(buffer) - 1);
if (s > 0) {
buffer[s] = 0;
cout << buffer << endl;
} else if (s == 0) {
cout << "读到文件结尾了" << endl;
break;
}
else
{
//1. 数据没用准备好 2. 真的出错了. 都以-1的返回值返回
// 数据没有准备好,不算出错. 需要区分这两种情况
if(errno == EWOULDBLOCK || errno == EAGAIN)
{
cout<<"os底层数据还没就绪"<<endl;
cout<<errno<<endl;
}
//被信号中断, 也不算read出错
else if(errno == EINTR)
{
cout<<"IO interrupted by signal"<<endl;
}
else
{
cout<<"read error"<<endl;
break;
}
}
sleep(1);
}
}
这种方式虽然相比较于阻塞IO,能把等待的时间利用起来了,但是由于需要频繁的轮询,往往会导致CPU不停的工作发热,由于硬件问题而使得性能下降,所以一般只会在特定的场景下使用。
三、信号驱动IO
信号驱动 IO: 内核将数据准备好的时候, 使用 SIGIO 信号通知应用程序进行 IO操作。即在上面非阻塞的情况下把原本的定时监测,改为了由操作系统发现就绪后来给你的进程发信号,提醒你数据已经就绪了,而你在等待期间就不再需要频繁记得去检测,就能更好的完成其他代码逻辑。
四、多路转接
IO 多路转接: 虽然从流程图上看起来和阻塞 IO 类似. 实际上最核心在于 IO 多路转接能够同时等待多个文件描述符的就绪状态。即同时有大量的套接字处于等待状态,而调用select则告诉操作系统我今天不想自己等待了,你去帮我等,只要有资源了,再来提醒我,这样就将许多等待的时间重叠了。
这种方式往往是效率最高的。
五、异步IO
异步 IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据)。
异步IO其实和之前4种方式都不太一样,前面无论是谁在帮你等,都是要自己处理数据的,但是异步IO则相当于甩手掌柜,只用告诉操作系统我要读写数据了,就走了。剩下的等待+拷贝操作都由操作系统自动完成。把所有事情做完了再通过信号告诉进程。
六、同步和异步的区分
同步和异步关注的是消息通信机制
(1)所谓同步,就是在发出一个调用时,在没有得到结果之前,该调用就不返回.但是一旦调用返回,就得到返回值了; 换句话说,就是由调用者主动等待这个调用的结果;
(2)异步则是相反,调用在发出之后,这个调用就直接返回了,所以没有返回结果; 换句话说,当一个异步过程调用发出后,调用者不会立刻得到结果; 而是在调用发出后,被调用者通过状态、通知来通知调用者,或通过回调函数处理这个调用.
异步和同步各有优势,同步更强调即时性,而异步通常适用于不需要立刻返回结果,可以在后台运行,所以往往异步的成文较低。我们在实际操作中要按照需求来选取。