01、IO 模型引入实验
一、IO 模型的分类
假设这种情况:每次需要从硬盘读取并处理50MB数据。传统做法是先读取数据(耗时20秒),等读取完成后再开始处理数据(再耗时20秒),总耗时40秒。这时就会发现,读取数据就花掉一半时间,导致总耗时翻倍,效率很低。
有没有办法在等待读取数据的同时就开始处理数据呢?答案是肯定的!这时候IO编程模型就能派上用场了。
常见的IO模型可以这样分类:
- 阻塞IO:读数据时程序完全卡住等待
- 非阻塞IO:读数据时不断问"读好了吗?"直到完成
- 信号驱动IO:系统读好数据后主动通知程序
- IO多路复用:同时监控多个数据源的读写状态
- 异步IO:把读数据任务交给系统后完全不管,系统自动完成并通知
前四种方式(阻塞/非阻塞/信号驱动/多路复用)都属于"同步IO",都需要程序主动等待或轮询结果。而最后的异步IO是完全自动的,程序可以一边等待数据读取一边处理其他任务,真正实现并行操作。
这种设计就像餐馆点餐:
- 同步模式:点完菜必须坐在桌边等上菜才能吃饭
- 异步模式:点菜后可以先去喝咖啡,厨师做好会喊你过来吃
通过这种分工,就能把原本需要排队等待的时间充分利用起来。:
1.1、阻塞 IO
当一个程序需要读取数据(比如用read命令)时:
- 程序会先向系统发出请求,切换到系统核心层处理
- 如果数据还没准备好,程序就会暂停,进行等待,不再继续往下运行
- 系统等到数据完全准备好后
- 把数据从系统内存复制到程序自己的内存区域
- 最后通知程序继续运行,程序再对数据进行处理
以钓鱼为例:准备好鱼钩抛入水中后,你只能耐心等待鱼上钩。这期间必须专心盯着鱼竿,一旦有鱼咬钩,立刻拉竿。这就是阻塞IO的现实例子。
阻塞IO有优缺点。优点是能立刻处理结果(比如马上把鱼拉上来),缺点是等待结果时无法做其他事——你必须一直盯着鱼竿,直到鱼上钩才能行动。
C语言中的scanf()函数就是典型的阻塞IO。例如,当程序用scanf读取用户输入时,会一直等待直到输入完成,期间无法执行其他操作。这就是为什么程序运行到scanf时会"卡住",必须等用户输入完毕才能继续。
#include <stdio.h>
int main(void){
int i;
scanf("%d",&i);
printf("i = %d\n",i);
return 0;
}
2
3
4
5
6
7
scanf函数会等待用户从键盘输入数据。如果用户没有输入任何内容,程序会一直停在那等待,直到用户输入数据后才会继续运行,并把输入的内容显示在屏幕上。
1.2、非阻塞 IO
和阻塞IO不同,非阻塞IO的工作方式更直接。当程序请求IO操作时:
- 如果系统数据还没准备好,它会立刻返回"数据未就绪"的提示,让程序继续执行其他任务
- 一旦系统数据准备好了,它就会马上把数据返回给程序
简单来说,非阻塞IO像在超市自助收银机:你可以随时去查看结账进度,如果还没轮到你,系统会告诉你"请稍等";但如果你去的时候刚好可以结账,就会立即处理。而阻塞IO就像在收银台排队:你必须站在那里一直等到轮到自己为止。
非阻塞IO的好处是效率高,能同时处理更多任务。但缺点是需要不断检查结果是否准备好,可能会错过两次检查之间的完成结果。要提高实时性就得更频繁地检查,但这会增加CPU负担。
1.3、IO 多路复用
使用select函数实现IO多路复用可以这样简单理解:
当你需要同时管理多个网络连接或文件时,可以使用select函数。你只需要把所有要监控的连接告诉select,并设定一个等待时间。当调用select时:
系统会进入内核模式检查这些连接
如果有连接收到数据/准备好发送数据,select会立即返回结果
如果没有事件发生,系统会让程序暂停等待,直到:
- 某个连接产生新事件
- 等待时间到了
之后,你需要手动一个个查看所有被监控的连接,确认具体是哪些产生了事件(比如有数据可读或可写)。这样就能用一个程序同时处理多个输入输出请求了。
简单来说:就是把多个"观察对象"交给select,让它帮你监视哪些有动静,等有变化时再具体处理。
IO多路复用的优点是:一个进程或线程可以同时监控多个输入输出操作(比如网络连接),这样处理效率能提升很多。不过它并不是万能的——虽然能同时监控多个IO,但实际处理数据时还是得一个一个来。这种技术更适合需要处理大量分散的小数据场景,比如聊天软件(因为用户消息通常零散且不集中)。
但像select这种早期技术有个问题:最多只能处理1024个连接,而且每次都要逐一检查哪个连接有数据。连接数量多时效率会明显下降(这个问题后来被epoll等技术解决了)。
1.4、信号驱动
信号驱动的IO方式和信号机制有关。简单来说,当某些事件发生时,系统会向程序发送特定信号,而程序可以预先将信号和处理函数绑定。比如在Linux系统中,按下Ctrl+C键会向程序发送"终止信号"(SIGINT),默认情况下程序收到这个信号就会直接退出。
具体到IO操作时,可以这样做:
- 首先将SIGIO信号和自定义的处理函数绑定
- 然后开启某个文件/网络连接的信号通知功能 这样当数据到达时,系统会自动发送SIGIO信号,触发预设的处理函数。这时你就可以在信号处理函数里安全地读取数据了。
举个生活化的例子:就像手机设置消息提醒,你先设置好"收到新消息就震动"(绑定信号和动作),然后开启震动功能。当有新消息(数据到达)时,手机就会自动震动(触发信号处理),这时你就可以查看消息内容了。
1.5、异步 IO
aio_read是一个用于异步读取数据的函数。当程序调用它时,如果数据还没准备好,函数会立刻返回结果,不会让程序等待。当数据真正准备好后,系统会自动把数据从内存复制到程序指定的位置,然后触发一个预先设定的处理函数,让程序能及时对数据进行下一步操作。整个过程不会阻塞程序的其他任务。
举例:小马喜欢吃鱼但不想自己钓。于是他让助手帮忙钓鱼,自己马上离开去忙其他事。助手一钓到鱼就会:
- 把鱼放进篮子里
- 立刻喊小马回来取鱼
这样小马既能及时处理新鲜的鱼,又不用在岸边傻等。助手独立工作,小马随时回来取成果,双方都不耽误事儿。