五种IO模型与非阻塞IO

五种IO模型与非阻塞IO


一、核心问题:基本I/O的瓶颈

文档开篇点明了所有I/O操作的本质:

  1. 1.等待:等待数据准备好(例如,等待网络数据包到达内核缓冲区)。
  2. 2.拷贝:将数据从内核缓冲区拷贝到用户空间(应用程序的内存)。

关键洞察 :在实际网络环境中,"等待"所消耗的时间远远大于"拷贝"的时间 。因此,提高I/O效率的核心不在于加快拷贝速度,而在于如何更高效地"等待"

五种I/O模型就是为了解决"如何等待"这个问题而提出的不同策略。


二、五种I/O模型详解(从低级到高级)

文档用"钓鱼"的比喻非常形象,我们来技术化地解释一下:

1. 阻塞I/O
  • 工作方式 :应用程序发起I/O调用(如 read)后,线程被挂起,一直阻塞,直到内核数据准备好并完成拷贝,函数才返回。
  • 优点:编程简单。
  • 缺点:一个线程只能处理一个连接,性能极差。为每个连接创建一个线程会消耗大量资源。
  • 类比:张三一直盯着鱼竿,鱼不上钩就不做任何别的事。
2. 非阻塞I/O
  • 工作方式 :应用程序发起I/O调用,如果内核数据没准备好,立即返回一个错误码 ,而不是阻塞线程。程序需要不断地轮询尝试,直到数据准备好。
  • 优点:线程不会阻塞,可以在等待一个连接时做别的事(比如处理其他连接)。
  • 缺点:轮询会消耗大量CPU资源,因为线程在不停地空转检查。
  • 类比:李四过几秒就拉一下鱼竿看看,没鱼就继续做别的事,但频繁检查很累人。
3. 信号驱动I/O
  • 工作方式 :应用程序先向内核注册一个信号处理函数,然后可以去干别的事。当内核数据准备好时,它会向应用程序发送一个信号(如 SIGIO),应用程序收到信号后再来执行I/O操作。
  • 优点:避免了轮询,CPU利用率高。
  • 缺点:信号处理本身比较复杂,并且在大量I/O操作时,信号可能会变得不可靠。
  • 类比:王五在鱼竿上装了个铃铛,鱼上钩铃会响,他听到铃声再来收竿。
4. I/O多路复用
  • 工作方式 :这是最重要的模型 。应用程序通过调用 select, poll, epoll等函数,同时监控多个文件描述符。这个函数调用是阻塞的,但当任何一个被监控的描述符数据准备好时,函数就会返回,应用程序再遍历准备好的描述符进行I/O操作。
  • 优点单线程就可以高效地管理成百上千个连接,这是高性能网络服务器(如Nginx, Redis)的基石。
  • 与阻塞I/O的区别:阻塞IIO是一个线程堵在一个连接上;多路复用是一个线程堵在"监控器"上,但可以同时等待多个连接。
  • 类比 :赵六一个人同时看管很多根鱼竿(epoll),只要任何一根有鱼上钩,他就去处理那一根。
5. 异步I/O
  • 工作方式 :应用程序发起一个I/O请求后,立即返回,内核会自动完成所有工作(包括等待数据和数据拷贝)。拷贝完成后,内核再通知应用程序。
  • 与信号驱动I/O的区别 :信号驱动I/O是内核通知我们"可以开始拷贝数据了 ",拷贝操作要我们自己来做。而异步I/O是内核通知我们"数据拷贝已经完成了"。
  • 类比:田七雇了一个帮手(内核),让帮手去钓鱼,钓好的鱼会直接送到家里。田七完全不用管钓鱼的过程。

三、关键概念辨析(非常重要!)

1. 同步 vs. 异步
  • 关注点消息通信机制。即结果是由谁主动提供的。
  • 同步 :调用者主动等待 结果。调用一个函数,在得到结果之前,调用不会返回。 •例子 :阻塞I/O、非阻塞I/O、I/O多路复用都是同步的。因为真正的I/O操作(read/write)还是由调用者线程自己完成的。
  • 异步 :调用发出后,这个调用就直接返回了,结果由被调用者通过通知或回调函数被动送达 。 •例子:只有异步I/O是真正的异步。
2. 阻塞 vs. 非阻塞
  • 关注点调用者在等待结果时的状态
  • 阻塞:调用结果返回前,当前线程会被挂起,不能执行其他任务。
  • 非阻塞:调用结果返回前,线程不会被挂起,可以执行其他任务。
组合关系
  • 同步阻塞:阻塞I/O。
  • 同步非阻塞 :非阻塞I/O、I/O多路复用(select本身是阻塞的,但它阻塞在多个连接上,相对于单个连接的处理线程来说,它是非阻塞的)。
  • 异步非阻塞:异步I/O。

总结

核心思想是:

  1. 1.基本I/O模型(阻塞I/O)效率低下,因为它让宝贵的线程资源在"等待"上白白浪费。
  2. 2.高级I/O模型的核心目标是减少线程在I/O等待上的耗时,用更智能的方式去"等待"。
  3. 3.I/O多路复用(如 epoll 是实践中最重要、最高效的模型,它能用最少的资源处理最多的并发连接。

首先,我们明确故事中的角色对应关系:

  • 唐僧 = 需要被处理的数据(比如网络数据包)
  • 蒸笼 = 操作系统内核(负责真正执行I/O操作的地方)
  • 小妖 = 应用程序/程序员(负责发起I/O调用和处理结果)
  • 蒸唐僧 = 一次I/O操作(比如从网络读取数据)

场景一:同步阻塞(最传统的方式)

故事:一个小妖把唐僧放进蒸笼,然后啥也不干,就搬个凳子坐在蒸笼前,眼睛死死盯着蒸笼。他心里想:"在唐僧蒸好(调用返回)之前,我就在这儿等着,哪儿也不去。" 他一直等到蒸笼冒出蒸汽(得到结果),然后打开蒸笼取出唐僧。

技术对应

  • 同步 :小妖主动等待结果(坐在蒸笼前等)。调用(开始蒸)没有返回,他在等待最终结果(蒸熟的唐僧)。
  • 阻塞 :在等待过程中,小妖被挂起,不能做任何其他事情(不能去巡山、不能去喝酒)。

总结 :这就是最基本的阻塞I/O。应用程序发起一个读取请求,线程就被挂起,直到数据完全准备好才返回。效率最低。


场景二:同步非阻塞(轮询方式)

故事 :小妖把唐僧放进蒸笼后,没有坐在那里干等。他对自己说:"我去干点别的(比如去巡山),但我每隔5分钟就跑回来看一眼蒸笼好了没。" 他每次跑回来,如果发现没好(EAGAIN错误),就继续去巡山;如果发现好了(调用成功返回),就取出唐僧。

技术对应

  • 同步 :小妖依然需要主动去查看结果(跑回来看)。结果的最终获取(取出唐僧)还是由他本人完成。
  • 非阻塞 :在"蒸唐僧"这个操作没有完成时,小妖没有被挂起,他可以自由地去干其他事情(巡山)。

总结 :这就是非阻塞I/O 。应用程序发起读取请求,如果数据没准备好,内核立即返回一个错误码(如EWOULDBLOCK),线程可以继续执行其他任务,但需要不断地轮询(polling)检查数据是否就绪。虽然不阻塞了,但轮询消耗CPU。


场景三:异步(最高效的方式)

故事 :小妖是一个"现代化"的妖怪。他发明了一个带闹钟的智能蒸笼。他把唐僧放进蒸笼,按下开始键,然后就直接去喝酒吃肉了,完全不管蒸笼。当唐僧蒸好的那一刻,智能蒸笼会自动发出"滴滴滴"的响声(通知 ),或者甚至自动派一个小机器人把蒸好的唐僧送到小妖面前(回调)。

技术对应

  • 异步 :小妖不需要主动等待或查看结果 。他发起调用(按下开始键)后,调用立即返回,他可以彻底干别的事。结果的送达是由被调用者(智能蒸笼)通过通知回调的方式"推送"过来的。

总结 :这就是异步I/O。应用程序发起一个读取请求后,内核会自行完成所有工作(包括等待数据和拷贝数据),完成后主动通知应用程序。应用程序在整个过程中都可以执行其他代码。


总结与对比表格

模式 通信机制(同步/异步) 等待状态(阻塞/非阻塞) "妖怪蒸唐僧"比喻
同步阻塞 同步:调用者主动等待结果 阻塞:调用者被挂起,不能干别的 干等式:坐在蒸笼前啥也不干,直到蒸好
同步非阻塞 同步:调用者主动轮询结果 非阻塞:调用者可以干别的 轮询式:边巡山边每隔5分钟跑回来看一眼
异步 异步:被调用者通知结果 非阻塞:调用者可以干别的 智能式:设置好闹钟后就去玩,闹钟响或直接送货上门

核心区别

  • 同步 vs 异步 :关注的是结果如何被返回。是调用者主动去取(同步),还是被调用者送上门(异步)?
  • 阻塞 vs 非阻塞 :关注的是等待结果时调用者的状态。是完全不能动(阻塞),还是可以自由活动(非阻塞)?

其他高级 IO

非阻塞 IO,纪录锁,系统 V 流机制,I/O 多路转接(也叫 I/O 多路复用),readv 和writev 函数以及存储映射 IO(mmap),这些统称为高级 IO.

非阻塞 IO

fcntl

一个文件描述符, 默认都是阻塞 IO.

函数原型如下.

C++ 复制代码
#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

传入的 cmd 的值不同, 后面追加的参数也不相同.

fcntl 函数有 5 种功能:

• 复制一个现有的描述符(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).

我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞.

实现函数 SetNoBlock

基于 fcntl, 我们实现一个 SetNoBlock 函数, 将文件描述符设置为非阻塞.

非阻塞代码如下:

C++ 复制代码
#include <iostream>
#include <unistd.h>
#include <cstdio>
#include <fcntl.h>
//0标准输入默认就是阻塞的
void SetNonBlock(int fd)
{
    int fl=fcntl(fd,F_GETFL);// 获取文件描述符当前标志
    if(fl<0)
    {
        perror("fcntl");
        return;
    }
    fcntl(fd,F_SETFL,fl | O_NONBLOCK);//O_NONBLOCK为非阻塞
}

int main()
{
    SetNonBlock(0);
    char buffer[1024];
    while(true)
    {
        //Linux中:ctrl+d:标识输入结束,read返回值是0,类似于读到文件结尾
        ssize_t n=read(0,buffer,sizeof(buffer));
        if(n>0)
        {
            buffer[n-1]=0;//将最后改为'/0'
            std::cout<<buffer<<std::endl;
        }
        else if(n<0)
        {
            //1.读取出错//2.底层没有数据准备好
            if(errno==EAGAIN||errno==EWOULDBLOCK)
            {
                std::cout<<"数据没有准备好。。"<<std::endl;
                sleep(1);
                continue;
            }
            else if(errno==EINTR)//系统中断
            {
                continue;
            }
            else
            {
                //真正的READ出错了
                
            }
        }
        else
        {
            break;
        }
        sleep(1);
        std::cout<<".:"<<n<<std::endl;//C++也有语言缓冲区
    }
    return 0;
}

四、技术要点总结

概念 说明 在本程序中的体现
阻塞I/O read会一直等待直到有数据 通过SetNonBlock(0)禁用
非阻塞I/O read立即返回,通过返回值判断 核心逻辑在n < 0的处理
轮询 循环检查是否有数据可读 while(true)+ sleep(1)
错误处理 区分真正错误和暂时无数据 检查errno的值

五、实际应用场景

这种模式是高性能网络编程的基础:

  • 单线程处理多连接:一个线程可以同时监控成百上千个网络连接
  • 事件驱动架构 :更高效的实现是使用epoll等系统调用,而不是忙等待
  • 避免线程阻塞:不需要为每个连接创建线程,节省系统资源

| 轮询 | 循环检查是否有数据可读 | while(true)+ sleep(1) |

| 错误处理 | 区分真正错误和暂时无数据 | 检查errno的值 |


五、实际应用场景

这种模式是高性能网络编程的基础:

  • 单线程处理多连接:一个线程可以同时监控成百上千个网络连接
  • 事件驱动架构 :更高效的实现是使用epoll等系统调用,而不是忙等待
  • 避免线程阻塞:不需要为每个连接创建线程,节省系统资源

这个简单的demo展示了非阻塞I/O的基本原理,是理解select/poll/epoll等高级I/O多路复用技术的重要基础。

相关推荐
拾零吖2 小时前
数据库 - SQL
数据库·sql
不会c嘎嘎2 小时前
MySQL -- 库的操作
数据库·mysql
陌上桑花开花2 小时前
DBeaver常用配置
数据库
百***87442 小时前
MySQL 查看有哪些表
数据库·mysql·oracle
曹牧2 小时前
Oracle:查询当前正在等待执行的SQL语句
linux·数据库·oracle
_Kafka_2 小时前
在 Oracle Data Guard 环境中,手工将备库(Standby)切换为主库(Primary)
数据库·oracle
百***24132 小时前
oracle使用PLSQL导出表数据
数据库·oracle
cqsztech2 小时前
ORACLE 11g 在线修改数据文件路径
数据库·oracle
为什么要做囚徒2 小时前
Oracle跨用户表授权+同义词创建的标准脚本模板
数据库·oracle