高级IO(五种IO模型介绍)

高级IO(五种IO模型介绍)

一、IO为什么慢?IO操作往往都是和外设交互,比如键盘、鼠标、打印机、磁盘。而最常见的就是内存与磁盘的交互,要知道磁盘是机械设备,需要机械运转来存取数据,所以比较慢。IO本质是:等+拷贝时间往往消耗在等上,等待键盘输入就绪、文件描述符就绪、硬件中断就绪… 外设就绪慢是相较于 CPU 来说的,它本身是很快,我们感知不到。

IO的拷贝很快的,慢大部分时间都花在等上,所以接下来我们就从“等”下手,去解决IO慢的问题。

IO 效率高?单位时间内,传输的数据量越大,IO 效率越高。

下面讲解的五种IO模型,我们从一个钓鱼例子引入:

张三:全神贯注的盯着鱼漂,本着鱼漂不动我不动的原则,尽管马蜂扑他脸上。(阻塞)李四:不会因为鱼不上钩就紧紧盯着什么也不干,而是边做其他事情,刷刷快手抖音,写写博客啥的。(非阻塞)王五(聪明人):鱼竿顶部加铃铛,他比李四好一些,不用时不时地去看鱼漂。(信号驱动)赵六(富人):拖来一卡车100支鱼竿,全部用来钓鱼,然后只用左右检查是否有鱼竿钓到鱼。(多路复用)

在这里插入图片描述田七(大老板):他知道他不是喜欢钓鱼,而是喜欢吃鱼。派小王去钓鱼,他干自己的事,到时候负责吃就行。(异步IO)一、阻塞IO 张三钓鱼的例子就是阻塞IO,它的特点是在内核将数据准备好之前,系统调⽤会⼀直等待。所有的套接字,默认都是阻塞⽅式。阻塞IO是最常见的IO模型。如下应用程序与内核的交互:

在这里插入图片描述二、非阻塞IO ⾮阻塞IO:如果内核还未将数据准备好,系统调⽤仍然会直接返回,并且返回EWOULDBLOCK错误码。

⾮阻塞IO往往需要程序员循环的⽅式反复尝试读写⽂件描述符,这个过程称为轮询。这对CPU来说是较⼤的浪费,⼀般只有特定场景下才使⽤。

在这里插入图片描述阻塞和⾮阻塞关注的是程序在等待调⽤结果(消息,返回值)时的状态。

阻塞调⽤是指调⽤结果返回之前,当前线程会被挂起,调⽤线程只有在得到结果之后才会返回。⾮阻塞调⽤指在不能⽴刻得到结果之前,该调⽤不会阻塞当前线程。文件默认情况下就是阻塞IO不用我们去修改,那么我们想要设置非阻塞IO要怎么做呢?

方法一:在文件open打开时添加O_NONBLOCK标准位即可。

如果在文件打开之后想要设非阻塞IO呢?方法二:使用函数fcntl,需要包含头文件unistd.h和fcntl.h代码语言:javascript复制int fcntl(int fd, int cmd, ... /* arg */ );参数:

fd:文件描述符(如 ```socket``、普通文件等)。cmd:控制命令(如 F_GETFL、F_SETFL等)。 F_GETL:获取当前文件描述符的标志,如 O_RDONLY、O_NONBLOCK等。F_SETFL:设置文件描述符的标志。arg:可选参数,取决于 cmd。返回值:

成功时返回值取决于 cmd,F_GETFL成功时返回状态标志符,F_SETFL成功时返回0,失败时都返回-1。失败时返回 -1,并设置 errno(如 EBADF 无效描述符)。获取原标志符:int fg = fcntl(fd,F_GETFL);

设置O_NONBLOCK标志位:fcntl(fd, F_SETFL, fg | O_NONBLOCK)

我们以read为例,当将文件设为非阻塞后,我们再去读取数据,即使数据没就绪,也不会把程序挂起,而是直接返回一个小于0的结果继续往下执行。所以数据并不一定一次read就读取到,需要我们反复去read,也就是轮询。

不过还有一个问题,read失败返回值小于0,因为数据未就绪返回值也是小于0,那么我们怎么区分呢?其实这两种结果都有不同的erron错误参数,到时候拿erron去匹配就行,即:

EWOULDBLOCK或EAGAIN:都表示数据未就绪。因为历史版本原因所以有两个版本,它们的值都是11,任意使用一个即可。EINTR:因信号中断导致的读书失败。其他:read读取错误。测试示例:

代码语言:javascript复制#include

#include

#include

using namespace std;

int main()

{

int fd = 0;

//获取标志符

int fn = fcntl(fd, F_GETFL);

//增加O_NONBLOCK并设置

fcntl(fd, F_SETFL, fn | O_NONBLOCK);

int count = 0;

while(1)

{

char buffer[1024];

int n = read(fd, buffer, sizeof(buffer)-1);

if(n > 0)

{

buffer[n] = '\0';

cout<

sleep(1);

}

else if(n < 0)

{

if(errno == EAGAIN || errno == EWOULDBLOCK)

{

cout<<"文件未就绪..."<

sleep(2);

}

else if(errno == EINTR)

{

cout<<"信号中断..."<

continue;

}

else

{

perror("read fail\n");

break;

}

}

else

{

break;

}

}

return 0;

}三、信号驱动IO信号驱动IO:内核将数据准备好的时候,使⽤SIGIO信号通知应⽤程序进⾏IO操作。

在这里插入图片描述

工作流程

注册信号处理程序:应用进程首先使用 sigaction系统调用,建立针对特定信号(通常是 SIGIO)的信号处理程序。在这一步,进程告知内核,当特定的 I/O 事件发生时,应该触发哪个信号处理函数。设置文件描述符:应用进程需要将相关的文件描述符(如套接字描述符)设置为信号驱动 I/O 模式,并指定该文件描述符的属主(通常使用fcntl 系统调用的 F_SETOWN命令设置属主为当前进程 )。这一步确保内核知道当 I/O 事件就绪时,应该向哪个进程发送信号。内核等待数据:完成上述设置后,应用进程可以继续执行其他任务,而内核开始等待数据到达。当数据报准备好(比如有数据到达套接字 )时,内核会向应用进程发送预先设置好的信号(如SIGIO )。信号处理:应用进程接收到SIGIO 信号后,会暂停当前正在执行的任务,转而执行之前注册的信号处理程序。在信号处理程序中,通常会调用诸如recvfrom 这样的函数来读取数据。在数据从内核拷贝到用户空间的应用缓冲区期间,进程会阻塞(这是信号处理函数内部执行具体 I/O 操作时的阻塞 ),直到数据拷贝完成。处理数据:数据读取完成后,信号处理程序执行结束,进程恢复之前被暂停的任务,或者开始对读取到的数据进行处理。四、IO多路复用 IO多路复用允许单个线程/进程同时监控多个文件描述符(如套接字、管道等)的 I/O 事件,并在任一描述符就绪时进行读写操作。它是高并发网络编程的核心技术之一。是真正意义上的高效 I/O 处理机制,它在单位时间内增加了传输效率,而其他四个只是在利用等的时间而已,并没有提高IO效率。

在这里插入图片描述核心思想

统一监控:通过系统调用(如select、poll、epoll)一次性注册多个文件描述符,由内核通知哪些描述符已就绪(可读/可写/异常)。避免阻塞轮询:无需为每个描述符创建独立线程,节省资源。单线程高并发:一个线程即可处理成千上万的连接(如 Nginx、Redis 的底层模型)。 关于IO多路复用的很重要,涉及select/poll/epoll,有繁多的细节和复杂的底层逻辑,统一放在下一章进行讲解。

五、异步IO在这里插入图片描述 核心思想是 “发起 I/O 请求后立即返回,由内核完成所有操作(包括数据拷贝),并通过回调或信号通知进程结果”。与同步 I/O 不同,进程无需等待 I/O 完成即可继续执行其他任务,真正实现 非阻塞 和 完全异步。

异步IO特性

非阻塞调用:调用 I/O 函数(如 aio_read)立即返回,不阻塞进程。内核全权处理:内核负责数据准备、拷贝到用户空间,全程无需进程参与。通知机制:操作完成后,内核通过回调、信号或事件通知进程。全程无等待:进程从发起请求到获取结果均无需阻塞,最大化利用 CPU。IO模型

控制方式

优势

劣势

阻塞 I/O

进程阻塞等待数据

编程简单

无法并发处理多 I/O

非阻塞 I/O

轮询检查数据是否就绪

可同时处理其他任务

轮询消耗 CPU

I/O多路复用

select/epoll

统一监控

高并发支持

信号驱动 I/O

内核信号通知

无轮询,延迟低

信号处理复杂,不适合高频场景

异步 I/O (AIO)

内核完成所有操作后回调

真正的异步,全程无阻塞

实现复杂,部分系统支持不完善

非常感谢您能耐心读完这篇文章。倘若您从中有所收获,还望多多支持呀!

相关推荐

《魔兽世界》tbc值钱材料大全
餐饮行业的利润一般是多少?详解净利润率与成本结构 | 帆软九数云
长沙惊现作弊麻将机 老板声称包“赢钱”
蜘蛛池需要多长时间,探索蛛网构建的奥秘,蜘蛛池需要多长时间消毒一次
赛扬G1620 赛扬G1620处理器深度评测:经典双核CPU的性能与适用场景分析
火线、地线与零线_火线零线地线怎么接?火线
浪琴手表可以退吗(退货政策详解)
数据安全防护如何防止数据泄露?
新华社:岫岩陨石坑被研究证实