五种IO模型

1、通信的本质:

通过网络通信的学习,我们能够理解网络通信的本质是进程间通信,而进程间通信的本质就是IO。

IO也就是input和output。当读取条件不满足的时候,recv会阻塞。write写入数据时,会将数据拷贝到缓冲区中,当缓冲区满了之后,也会进行阻塞等待。

recv实际上干了几件事情呢?

  1. 拷贝

所以这也可以映射出IO的本质就是

IO = 等 + 拷贝

那什么是高效的IO呢?

单位时间内,等的比重越低,IO效率就越高!

什么是高效的IO代码?

减少IO的比重,或者把等待的时间利用起来。

针对高效的IO,我们来讲解一下五种IO模型!

2、五种IO模型

我们平时在钓鱼的时候,一般分为两步:

钓鱼 = 等 + 钓。这正好对应着我们的IO本质。

所以我们用钓鱼的例子来分别引入这五种IO模型。

有五名角色,他们采用钓鱼的方法分别对应的五种不同的IO模型。

  1. 张三:他将鱼竿放在水里之后,眼睛就一直盯着鱼竿,其他什么事都不干,只一动不动的盯着鱼竿,这就是阻塞IO
  2. 李四:他将鱼竿放在水里之后,一直在动,一会看看鱼有没有上钩,一会看看张三在干嘛(但张三一直不理他,一直阻塞式一心一意的盯着自己的鱼竿),一会又看看报纸,一会又刷刷抖音,非常的忙。这也就是非阻塞IO!
  3. 王五:他在鱼竿上绑了一个铃铛,当鱼上钩的时候,就会发出信号提醒王五有鱼上钩了,这就是信号驱动IO
  4. 赵六:他是当地的有钱人,他拉来了一卡车的鱼竿,有200根鱼竿同时进行钓鱼,这个赵六呢就来回在岸边检测轮询。这就是多路复用,多路转接式的IO
  5. 田七:他不想自己去钓鱼,他本质想吃鱼,所以他让别人(操作系统)去钓鱼,最后坐收渔翁之利。这就是异步IO

这里的湖水就是OS内部缓冲区,水桶是用户的缓冲区,鱼是数据,而鱼竿是一个sockfd,文件描述符。

阻塞 IO: 在内核将数据准备好之前, 系统调用会一直等待. 所有的套接字, 默认都是阻塞方式.

阻塞 IO 是最常见的 IO 模型.

非阻塞 IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回EWOULDBLOCK 错误码.

非阻塞 IO 往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为轮询. 这对 CPU 来说是较大的浪费, 一般只有特定场景下才使用.

信号驱动 IO: 内核将数据准备好的时候, 使用 SIGIO 信号通知应用程序进行 IO操作.

IO 多路转接: 虽然从流程图上看起来和阻塞 IO 类似. 实际上最核心在于 IO 多路转接能够同时等待多个文件描述符的就绪状态.

异步 IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).

小结

任何 IO 过程中, 都包含两个步骤. 第一是等待, 第二是拷贝. 而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让 IO 更高效, 最核心的办法就是让等待的时间尽量少.

什么是异步IO,什么是同步IO?

这个答案有争议,但是我们按照教材官方的解释就是只要参与IO,就是同步IO,参照上面的例子就是只要自己亲自参与钓鱼这个过程,就是同步IO,因此前面4种IO模型都是同步IO,只有最后田七没有自己参与IO,所以只有最后一种是异步IO

阻塞IO和非阻塞IO有什么区别呢?

区别就是等待的方式不一样。

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

  • 阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回.
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程.

3.非阻塞IO

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

那我们如何实现非阻塞IO呢?

我们使用一个函数fcntl(),函数原型如下.

#include <unistd.h>

#include <fcntl.h>

int fcntl(int fd, int cmd, ... /* arg */ );

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

fcntl 函数有 5 种功能:

  1. 复制一个现有的描述符(cmd=F_DUPFD).
  2. 获得/设置文件描述符标记(cmd=F_GETFD 或 F_SETFD).
  3. 获得/设置文件状态标记(cmd=F_GETFL 或 F_SETFL).
  4. 获得/设置异步 I/O 所有权(cmd=F_GETOWN或 F_SETOWN).
  5. 获得/设置记录锁(cmd=F_GETLK,F_SETLK 或 F_SETLKW).

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

实现函数 SetNoBlock

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

cpp 复制代码
#pragma once
#include <unistd.h>
#include <fcntl.h>
#include <iostream>

//int fcntl(int fd, int cmd, ... /* arg */);

void SetNonBlock(int fd)
{
   //首先获取原来标志位
   int fl = ::fcntl(fd , F_GETFL);
   if(fl < 0)
   {
       std::cout<< "fcntl error " << std::endl;
       return ;
   } 
   //设置非阻塞标志位
   int n = ::fcntl(fd , F_SETFL , fl | O_NONBLOCK);
   if(n < 0)
   {
       perror("fcntl error \n");
       return ;
   }
   return ;
}
  • 使用 F_GETFL 将当前的文件描述符的属性取出来(这是一个位图).
  • 然后再使用 F_SETFL 将文件描述符设置回去. 设置回去的同时, 加上一个O_NONBLOCK 参数,就将该文件描述符设置为非阻塞

我们将标准输入设置为非阻塞的时候,运行程序:

如果是非阻塞,底层数据没有就绪,IO接口会以出错的形式返回。

那么如何区分是真的出错了还是底层不就绪的非阻塞IO返回呢?仅仅通过返回值是无法区分的了,但是我们可以参考read函数中的errno全局错误码,将错误详细的输出,所以我们根据errno具体情况进行区分!

同样的recv,send...IO系列接口都是会设置errno全局错误码。

可以看到errno被设置为了11,那11代表什么呢?11就是EWOULDBLOCK错误:

#define EAGAIN 11 /* Try again */

...

#define EWOULDBLOCK EAGAIN /* Operation would block */

这就表示底层数据不就绪,可以try again,如果真的出错了,errno就会被设置成其他格式。

所以我们根据这一特性来重新编写我们的代码

cpp 复制代码
#include <iostream>
#include <unistd.h>
#include "Comm.hpp"

int main()
{
    char buffer[1024];
    SetNonBlock(0); // 将0设置为非阻塞
    while (true)
    {
        ssize_t s = ::read(0, buffer, sizeof(buffer) - 1);
        if (s > 0)
        {
            buffer[s] = 0;
            std::cout << "Echo# " << buffer << std::endl;
        }
        else
        {
            // 问题:我怎么知道是底层IO条件不就绪,还是读取错误了呢???
            //  底层IO条件就绪和读取错误采用的是同样返回值操作的
            if (errno == EWOULDBLOCK || errno == EAGAIN)
            {
                std::cout << "底层数据没有就绪, 下次在试试吧! 做做其他事情!" << std::endl;
                sleep(1);
                continue;
            }
            else if (errno == EINTR)
            {
                continue;
            }
            else
            {
                std::cout << "读取错误... s : " << s << " errno: " << errno << std::endl;
                sleep(1);
            }
        }
    }
}

运行结果:

这里必须线删除fd,因为如果先关闭fd的话,再删除fd,这里的fd就不是合法的了

循环读取的时候,为什么会出现阻塞呢

大概率会让滑动窗口变大,并行就会发送更多的数据

相关推荐
大树883 天前
金刚石散热越强,管路越先见顶
大数据·运维·服务器·人工智能·ai
摇滚侠3 天前
Linux CentOS7 rpm 安装 MySQL 5.7
linux·运维·mysql
霸道流氓气质3 天前
领域驱动设计(DDD)在 Spring Boot 微服务中的实践指南
运维·spring boot·微服务
小宇宙Zz3 天前
Maven依赖冲突
java·服务器·maven
Inhand陈工3 天前
基于台达PLC与映翰通IG502的智慧水产养殖精准投喂与远程运维解决方案
运维·人工智能·物联网·阿里云·信息与通信
网络研究院3 天前
2026年网络安全
网络·安全·法律·法规·趋势·发展
酣大智3 天前
ARP代理--工作原理
运维·网络·arp·arp代理
treesforest3 天前
AI安全系统如何识别异常访问?IP风险识别正在成为关键能力
网络·人工智能·tcp/ip·安全·web安全
shushangyun_3 天前
2026年快消品B2B系统推荐:支持终端门店订货、促销政策自动化的工具?
java·运维·网络·数据库·人工智能·spring·自动化
古城小栈3 天前
Unix 与 Linux 异同小叙
linux·服务器·unix