目录
前言:
初识五种IO模型
初识高级IO
同步IO和异步IO
1.再谈五种IO模型
1.1.阻塞IO
1.2.非阻塞IO
1.3.信号驱动IO
1.4.IO多路复用
1.5.异步IO
前言:
在我们过去的学习中,我们对于IO的认识局限于input、output,也就是输入和输出,即对外设的访问,与计算机外部设备(如键盘、显示器、磁盘等)之间的数据交换。但是这在我们过去对IO接口的使用,例如:recv、send、write、scanf等函数时,我们能够感受到IO的本质就是在不同的缓冲区之间做数据的拷贝,并且在这个过程中会“等待”,比如recv函数需要数据写入到缓冲区中,scanf需要等待键盘写入……
IO的本质:等待数据的就绪 + 拷贝数据到相应的缓冲区。
初识五种IO模型
阻塞IO | 在阻塞IO模型中,当用户进程发起一个IO操作时,如果数据尚未准备好,用户进程将被阻塞,直到数据准备好后从内核空间复制到用户空间,IO操作才返回。 |
非阻塞IO | 非阻塞IO模型允许用户进程在发起IO操作后立即返回,而不会阻塞等待数据准备。然而,用户进程需要不断地轮询、重复请求来检查数据是否准备好。 |
IO多路复用 | IO多路复用模型通过引入select/poll/epoll等机制,允许用户进程同时监控多个文件描述符的状态变化,以决定哪些文件描述符可读、可写或有异常状态。 |
信号驱动IO | 信号驱动IO模型允许用户进程通过注册一个信号处理函数,当数据准备好时,由内核发送一个信号来通知用户进程进行IO操作。 |
异步IO | 异步IO模型允许用户进程发起一个IO操作后立即返回,并在数据准备好后由内核自动将数据从内核空间复制到用户空间,然后通知用户进程IO操作已完成。 |
我们在前言中知道了IO的本质就是等待数据、拷贝数据,我们也可以用生活中钓鱼的例子来抽象一下IO,也就是等待鱼上钩和钓鱼上来。那么以上的五种IO模型我们可以看成五个钓鱼佬,各自拥有着不同的钓鱼习惯!!!
空军一号 | 空军一号钓鱼时,一直看着鱼漂,其他什么事情都不做,直到鱼上钩,完成钓鱼,才去做别的事情 |
空军二号 | 空军二号钓鱼时,一会看一下鱼漂有没有动,一会跟旁边的其他钓鱼佬讲话,一会玩玩手机,循环往复的进行鱼漂的观察。 |
空军三号 | 空军三号是装备党,一次性带着100个鱼竿,哪个上钩了就收哪个杆 |
空军四号 | 空军四号很聪明,在鱼漂上绑了一个铃铛,如果鱼上钩,铃铛回响,那么空军四号在铃铛响之前就可以做自己的事情 |
空军五号 | 空军五号带着他的兄弟一起钓鱼,他提出去钓鱼但是他不参与,实际上他把鱼竿给了兄弟之后,他就去干别的事情了,等着吃鱼了。 |
初识高级IO
我们通过空军一号、到空军五号不同的钓鱼习惯,初步学习了五种IO模型,我们可以感受到这五种钓鱼习惯的不同,对应的钓鱼效率是不一样的,显然,空军四号能够高效的钓鱼、空军一号太过死板,钓鱼效率低下。同样的,五种IO模型也是如此,各自效率、复杂性和适用场景方面都有所差异。所以在实际应用中,需要根据具体需求和环境选择合适的IO模型!!!
那什么是高级IO呢?
高级IO技术涉及在应用程序与操作系统、硬件设备之间的高效数据交换。其目标是通过优化等待和拷贝过程,提高IO操作的效率。也就是在计算机系统中进行输入和输出操作时使用的一种更高级的接口和技术。
为什么需要高级IO呢?因为对于IO而言,必不可少的是对数据的获取进行等待,而这个等待一般会造成IO的效率低下,所以我们需要进行不同的等待策略,来实现高级IO
同步IO和异步IO
- 同步IO:只要参与了IO的基本过程,即等待和拷贝其一,就可以视为同步IO。
- 异步IO:不参与IO的过程,既不等待也不拷贝。
也就是说,阻塞IO、非阻塞IO、IO多路复用、信号驱动IO本质上都是同步IO,因为他们进行了IO的动作,而异步IO他把IO的行为转交给了别的模块进行IO,所以不为同步IO。
值得注意的是:这里的同步、异步IO的区分是以IO的概念作为出发点的,也就是 等待+拷贝 。
而我们在多线程的同步与互斥中提到的同步IO是对于IO这个行为进行区分的,比如读写的过程。
1.再谈五种IO模型
1.1.阻塞IO
阻塞IO:在内核将数据准备好之前,系统调用会一直等待数据。并且所有的套接字,默认都是阻塞方式。
如图我们也可以看到,recvfrom就是一条路走到黑,只有获取到数据才继续走后续的代码模块!!!
1.2.非阻塞IO
非阻塞IO:如果内核还没有将数据准备好,系统调用会直接返回,返回值为EWOULDBLOCK错误码。
非阻塞式IO需要我们轮询调用recvfrom,这会造成CPU的开销,但是收到EWOULDBLOCK错误码和到下一层循环的recvfrom之前,我们可以调用其他的代码模块!!!
非阻塞IO的代码实现
首先我们要知道,对于任何一个文件描述符,默认都是阻塞式IO。另外fcntl函数主要用于对文件描述符进行各种控制操作。
#include <iostream>
#include <unistd.h>
#include <fcntl.h>
void SetNoneBlock(const int fd)
{
// 获取当前文件描述符的属性
int f1 = fcntl(fd, F_GETFL);
if (f1 < 0)
{
std::cerr << "fcntl failed" << std::endl;
exit(0);
}
// 对f1进行选项的增加 ,设置为非阻塞的
fcntl(fd, F_SETFL, fl | O_NONBLOCK);
}
int main()
{
// 将键盘对应的文件描述符设置为非阻塞
SetNoneBlock(0);
while (1)
{
char buffer[1024];
ssize_t n = read(0, buffer, sizeof(buffer) - 1);
if (n > 0)
{
buffer[n] = 0;
std::cout << "echo# " << buffer << std::endl;
}
else if(n == 0)
{
std::cout<<"all data are received"<<std::endl;
}
else
{
// 处于非阻塞时,read的返回值也为-1,
// 这时会出现不知道是数据未就绪还是read错误的二义性问题
// 因此我们借助errno的设置
if (errno == EWOULDBLOCK || errno == EAGAIN)
{
std::cout << "none data, none block test..." << std::endl;
}
else if (errno == EINTR) // 表示IO被信号中断
{
std::cout << "read interrupted by singal, trt again..." << std::endl;
}
else
{
std::cout << "read error" << std::endl;
break;
}
}
sleep(2);
}
}
1.3.信号驱动IO
信号驱动IO:内核将数据准备好的时候,会发出SIGIO信号通知进程进行IO操作。
信号驱动IO的机制是通过告知内核,当数据到达缓存区后,发送信号给当前进程,接着在调用recvfrom,减少等待的时间。并且等待信号的这段时间,可以执行其他的代码模块!!!
1.4.IO多路复用
IO多路复用:将多个IO通道注册到一个事件管理器中,然后通过阻塞方式等待事件的发生。一旦有事件发生(如有数据可读或可写),线程就会被唤醒,然后可以针对具体的事件进行处理。
我们设置select、poll、epoll来监听大量的文件描述符,如果监听到数据就绪,就去调用recvfrom,这时数据就绪就不会造成recvfrom的等待,而是直接进行拷贝。
这时我们就能够理解高效在:select、poll、epoll可以实现单个线程(执行流)来监听大量的IO通道,进而实现高并发。
具体的讲解我们在下一篇博客具体学习!!!
1.5.异步IO
异步IO:发起IO,但是将IO的行为交由操作系统来进行处理,当数据未就绪时,由操作系统进行等待,当数据就绪时,让操作系统进行拷贝
通过aio_read机制将原本当前进程需要进行的IO行为,转交给操作系统进行。