当前位置: 首页 > news >正文

高级I/O知识分享【5种IO模型 || select || poll】

  博客主页:花果山~程序猿-CSDN博客

文章分栏:Linux_花果山~程序猿的博客-CSDN博客

关注我一起学习,一起进步,一起探索编程的无限可能吧!让我们一起努力,一起成长!

在这里插入图片描述

目录

一,前文

2,5种IO模型(钓鱼例子)

3. 5种IO模型图

4,进程同步&IO同步区分

那如何区分同步通信和异步通信?

5,阻塞与非阻塞区别

二,非阻塞IO

1.设置IO为非阻塞

2. I/O多路转接(select)

参数理解

用select优化listen_socket

select的优缺点

3.I/O多路转接(poll)


嗨!收到一张超美的图,愿你每天都能顺心!

一,前文

        我们曾经学习过类似IO的知识,例如接口:C语言的fwrite,fread,操作系统的read,write等对磁盘的IO,我们现在可以称他为——单机IO,本质上是内存到磁盘之间的读写操作,相比与网络IO,本地IO有着一些先天的优势(物理优势——近):

  1. 低延迟:由于数据交换不涉及网络传输,因此相对网络IO,单机IO的延迟较低。
  2. 简单直接:实现较为简单,通常使用操作系统提供的API(如C语言中的fopen, fread, fwrite等)直接进行操作。
  3. 可靠性高:相比网络环境,本地IO受外界干扰小,数据传输更可靠。

应用的场景多是对本地文件内容的读写。

        网络IO本质也是对外设进行读写(接口如:send,recv),但由于需要经过长延迟的网络环境与经历的繁琐步骤,网络IO的效率极其的低下,优化网络效率的问题被大家所关注。

网络IO的劣势:

  1. 高延迟:数据需要经过网络传输,可能受到网络延迟、丢包、拥塞等因素影响,因此延迟通常高于单机IO。
  2. 复杂性:需要处理网络连接建立、数据包封装/解封装、错误处理、流量控制等复杂问题。 

如何理解IO时间  =   等 +  数据拷贝

答:

本地IO:  当我们通过read打开某个文件,实质上是让操作系统帮我们向磁盘中获取数据,我们应用层只需要等待操作系统的缓冲区开始有数据,等到一定的时机,我们再拷贝到应用层准备好的缓冲区中。

网络IO:   网络版本其实也是类似外设从磁盘变成了网卡,依旧是操作系统为我们提取数据,应用程序等待操作系统从网络接收数据到内核的套接字缓冲区,然后再拷贝到应用层缓冲区中。

2,5种IO模型(钓鱼例子)

5种模型为:IO = 等 + 数据拷贝,这里用一个人在河边钓鱼为例子,“等”=等鱼上钩,“数据拷贝” =  将鱼钓上放在桶(应用层缓冲区)里。

  • 阻塞式 :这个人 ,就坐在哪儿,盯着鱼漂,啥事不干,就单纯在哪儿等鱼上钩,再调上。
  • 非阻塞轮询式:这个人,过一会来看看是否有上钩,上钩了再把鱼钓起来。
  • 信号驱动式:这个人将鱼钩上安装一个上钩提醒器,而他就做其他的事情,等到提醒了才过来把鱼钓起。
  • 多路复用,多路转接式:这个人安装100个鱼竿,坐在旁边,那个杆上钩了,把那个鱼钓起,因此这种方式效率最高
  • 异步IO式 :有两个人,老板自己嫌累,雇了个员工,员工在那里钓鱼,钓上来的鱼放在桶里,等到桶被装满,员工会提醒老板过来取鱼。

其中,除了异步IO,其余都是同步IO,那如何区分同步,异步IO只要(进程,线程)参与了等或者数据拷贝,都算同步IO,都没参加就是异步IO

通过了上面的IO模型下面我们来看看官方怎么解释的:(光看图理解是浅显的,这里就简单看看,后面会有代码实践

3. 5种IO模型图

阻塞 IO 是最常见的 IO 模型。
非阻塞IO: 如果内核还未将数据准备好, 系统调用仍然会直接返回, 并且返回 EWOULDBLOCK错误码
非阻塞IO往往需要程序员循环的方式反复尝试读写文件描述符, 这个过程称为 轮询. 这对 CPU来说是较大的浪费, 一般只有特定场景下才使用。

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

IO多路转接: 虽然从流程图上看起来和阻塞IO类似. 实际上最核心在于IO多路转接能够 同时等待多个文件
描述符的就绪状态.
异步IO: 由内核在数据拷贝完成时, 通知应用程序(而信号驱动是告诉应用程序何时可以开始拷贝数据).
小结
任何IO过程中, 都包含两个步骤. 第一是 等待, 第二是 拷贝。而且在实际的应用场景中, 等待消耗的时间往往都远远高于拷贝的时间. 让IO更高效, 最核心的办法就是让等待的时间尽量少

4,进程同步&IO同步区分

另外, 我们回忆 多进程多线程的时候, 也提到 同步。这里的 同步通信进程之间的同步是完全不想干的概念。
多进程多线程的同步:  在操作系统或程序内部, 多进程或多线程之间的同步是指控制多个进程或线程 访问共享资源的方式,以避免竞态条件和其他并发问题。常见的同步机制有:互斥锁,条件便利,信号量,原子操作等
同步通信: 同步通信是指两个或多个网络实体之间进行有序的信息交换,其中一方在发送消息后 必须等待另一方的响应才能继续执行。

那如何区分同步通信和异步通信

以钓鱼为例,“在河边等待鱼上钩"=等待,”将鱼吊起“=拷贝,只要进程参与任一一步都理解为同步IO;都不参加,直接来提桶拿鱼为异步IO

以后在看到 "同步" 这个词, 一定要先搞清楚大背景是什么。这个同步, 是同步通信异步通信的同步, 还是进程,线程同步与互斥的同步。

5,阻塞与非阻塞区别

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态。
  • 阻塞调用是指调用结果返回之前,当前线程会被挂起. 调用线程只有在得到结果之后才会返回。
  • 非阻塞调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

二,非阻塞IO

 首先我们先看看我们常见的阻塞式IO:

int main()
{while (1){char buff[1024];int ret = read(0, buff, sizeof buff - 1);if (ret){buff[ret] = 0;std::cout << "echo#:" << buff ;} else{std::cout << "....." << std::endl;}}return 0;
}

结论:I/O的文件描述符默认是阻塞式的。如果非阻塞式,需要我们通过fcntl设置。最常见的IO阻塞就是标准输入,会阻塞的等待我们输入内容。

1.设置I/O为非阻塞(fcntl)

#include <unistd.h>
#include <fcntl.h>
int fcntl(int fd, int cmd, ... /* arg */ );

(注:这个接口适用于所有的本地IO,网络IO所打开的文件描述符设置是否阻塞形式。有了这个我们就可以设置非阻塞IO,不用单独学习特定接口参数来设置非阻塞) 

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).
废话少说,先上手看看,写一下用于 设置文件描述符方式为非阻塞式的函数,如下:
int SetNoBlock(int fd)
{int ret = fcntl(fd, F_GETFL);  // 获取原来fd的读写标志位,就比如:open时设置的O_RDONLY,O_RDWR等等if (ret == -1)return -1;fcntl(fd, F_SETFL, ret | O_NONBLOCK);// fcntl 更换为设置读写标准位,并在原来标准位的基础上添加 非阻塞模式return 0;
}
我们此处只是用第三种功能, 获取/设置文件状态标记, 就可以将一个文件描述符设置为非阻塞。设置好非阻塞后,在循环内文件描述符会被轮询,如果没有资源就绪,会 设置错误码,如:EAGAIN,EWOULDBLOCK——资源未就绪。

因此,修改后的非阻塞IO案例,如下:

#include <iostream>
#include <unistd.h>
#include <fcntl.h>
#include <cerrno>
#include <string.h>int SetNoBlock(int fd)
{int ret = fcntl(fd, F_GETFL);  // 获取原来fd的读写标志位,就比如:open时设置的O_RDONLY,O_RDWR等等if (ret == -1)return -1;fcntl(fd, F_SETFL, ret | O_NONBLOCK);// fcntl 更换为设置读写标准位,并在原来标准位的基础上添加 非阻塞模式return 0;
}int main()
{SetNoBlock(0);  //文件描述符也只需要设置一次,如果还要设置标准位可以后面自己添加char buff[1024];while (1){sleep(1);errno = 0;int ret = read(0, buff, sizeof buff - 1);if ( ret > 0){buff[ret] = 0;std::cout << "echo# " << buff;continue;}else  if( ret == 0){std::cout << "echo# try again"  << std::endl;}else if (ret == -1){std::cout << "echo# "<< errno << ": "<< strerror(errno);// 由于文件描述符采用非阻塞式后,我们无法区分是错误导致,还是单纯是IO资源未就绪}// 针对无法区分非阻塞式IO错误,我们需要对错误码进行区分if (errno == EAGAIN || errno == EWOULDBLOCK) // RWOULDBLOCK 本质是 EAGAIN{std::cout << " IO资源未就绪, try again" << std::endl;continue; }else if (errno == EINTR) // read时被signal打断,返回时无法再调用read{continue;}else{break;}}return 0;
}

设置非阻塞后,socket的读事件未就绪,需要对错误值进行,判断。 

其他错误值可能为:
EBADF 文件描述词为无效的或该文件已关闭
EINTR 此调用被信号所中断
EINVAL 参数n 为负值。
ENOMEM 核心内存不足

2. I/O多路转接(select)

select接口 是一个用于监控多个文件描述符(file descriptors,简称fd)的系统调用,它可以检测多个文件描述符上是否有事件发生(如数据可读、可写或异常)。这对于需要同时处理多个客户端连接的服务器程序来说非常有用,比如Web服务器、聊天服务器等。(AI)

参数理解

1).  timeval结构体类型,里面存放着微秒数,如下:

输入意义:告诉此次select 阻塞 tv_sec + tv_usec 秒后select超时返回。

输出意义:如果在阻塞时间内,有fd事件就绪,则会设置剩余时间量

这里扩展一下,获取时间戳的方法,如:C语言的time_t  time(time_t *),系统接口gettimeofday

2). fd_set类型

select接口中有三个fd_set类型,意义:让系统分别注意,读事件,写事件,其他事件fd就绪。

fd_set本质是一个位图数据结构,其中位图下标值代表fd,一个位图最大能容纳1024个fd;

每个位的值为1,

  • 输入意义:告诉系统注意该fd的读事件(写事件,其他事件);
  • 输出意义:select会设置fd_set,输出时标记fd就绪的下标位为1;

设置fd_set类型,需要特定操作宏:

用select优化listen_socket

        首先,我们需要理解listen_socket在网络通信中的角色。listen_socket就像是餐馆门口的迎宾员,负责接收潜在顾客(客户端)的到来,并准备好迎接他们(监听连接请求)。accept操作则像是餐馆内的服务员,负责正式接待顾客并安排座位(接受连接请求并创建新的套接字来处理这个连接)。

listen_socket不仅要监听传入的连接请求,还要与客户进行TCP的三次握手,以确认连接的建立。只有当三次握手成功完成后,accept操作才能接收这个新连接,并为其提供服务(创建新的套接字来处理连接)。

当客户完成数据交换并准备断开连接时,需要进行TCP的四次挥手,以确保双方都知道连接将要关闭,并且释放占用的资源。

使用如下例子:

        MySocket tool;tool.listen_run(_server_socket);// 实现多路转接select,这里只有一个listen,不过演示一下即可fd_set fdset;while (1){    timeval time = {0, 0} //非阻塞FD_ZERO(&fdset);FD_SET(_server_socket, &fdset);int n = select(_server_socket + 1, &fdset, nullptr, nullptr, &time);switch (n){case 0:continue;break;case -1:Logmessage(ERROR, "select fail errno %d: %s", errno, std::to_string(errno).c_str());exit(-1);break;default:Logmessage(INFO, "select get a connect");// 获取成功}}

上面是select只监视listen_socket一个标识符的读事件就绪,那如何将已连接客户端(或者本地IO)描述符一起管理起来呢?

思路:

  1. 需要一个外部的int数组,自称它为socket数组集,用于存放select关注的所有描述符。
  2. 每次客户端连接成功后,将soket放入数组集中;当select监视到其中某socket读事件就绪并处理后,需要从数组集中消除。
  3. 3.有个该数组集,我们可以每次遍历数组集循环设置fd_set,同时方便FD_ISSET判断。

下面是用web服务器,对socket的select处理代码示例链接(仅看webserver.hpp):

IO_Model/webserver.hpp · 逆光/Linux - 码云 - 开源中国 (gitee.com)

select的优缺点

 优:

缺点:

1. 用户,OS层存在大量遍历:用户层,需要不断修改第三方数组;OS层,也需要不断遍历fd_set与修改

2. fd_set本身的1024上限。

3. 输入输出型参数,从用户到内核,从内核到用户存在频繁的数据拷贝。

4.编码复杂

3.I/O多路转接(poll)

        针对select的缺点,经过过优化后的poll能解决大部分问题。

fds 是一个 poll 函数监听的结构列表 . 每一个元素中 , 包含了三部分内容 : 文件描述符 , 监听的事件集合 , 返回的事件集合。
nfds 表示: fds 数组的长度。
timeout 表示: poll 函数的超时时间 , 单位是毫秒 (ms)。
events 如何设置事件集合
使用示例如下:
#include <stdio.h>
#include <sys/time.h>
#include <sys/poll.h>int main() {struct pollfd fds[2];int timeout;// 初始化第一个文件描述符为标准输入fds[0].fd = 0; // 标准输入fds[0].events = POLLIN; // 只关心可读事件// 假设这里有一个套接字,其文件描述符为 sockfdint sockfd = 3;fds[1].fd = sockfd;fds[1].events = POLLRDNORM; // 监听套接字是否有数据可读timeout = 5000; // 超时时间为5秒// 进行 poll 调用int ret = poll(fds, 2, timeout);if (ret > 0) {for (int i = 0; i < 2; i++) {if (fds[i].revents & POLLIN) {printf("文件描述符 %d 可读\n", fds[i].fd);fds[i].revents = 0; //poll不在关注该描述符}}} else if (ret == 0) {printf("超时\n");} else {perror("poll error");}return 0;
}

poll优点:

缺点:

1. 用户层,OS层任存在不少的遍历。用户层需要不断检测数组就绪;OS层不断检测fd就绪,本质上与select一样需要维护第三方数组。

2. 当有大量链接接入,但就绪事件较少,这样遍历fd数组量加大,效率线性下降。

3. 编码较select容易些。

没关系,epoll会解决,按照 man 手册的说法 : 是为处理大批量句柄而作了改进的 poll . epoll 几乎具备了之前所说的一切优点,被公认为Linux2.6 下性能最好的多路 I/O 就绪通知方法。

结语

   本小节就到这里了,感谢小伙伴的浏览,如果有什么建议,欢迎在评论区评论,如果给小伙伴带来一些收获,请动动你发财的小手点个免费的赞,你的点赞和关注永远是博主创作的动力源泉。

相关文章:

  • 北京网站建设多少钱?
  • 辽宁网页制作哪家好_网站建设
  • 高端品牌网站建设_汉中网站制作
  • 7000长文:一文读懂Agent,大模型的下一站
  • Netty+HTML5+Canvas 网络画画板实时在线画画
  • 数据结构-2.9.双链表
  • 科技引领未来生活——“光影漫游者”展览馆应用—轻空间
  • 每日学习一个数据结构-布隆过滤器Bloom Filter
  • 数据结构:二叉树(2)
  • Linux 清空redis缓存及查询key值
  • 修改 Visual Studio 的主题颜色、背景颜色、字体
  • 分布式计算技术是什么?在数据集成值得作用?
  • Java项目实战II基于Java+Spring Boot+MySQL的车辆管理系统(开发文档+源码+数据库)
  • Stable Cascade | ComfyUI API 工作流格式优化
  • 教你如何在微信小程序中轻松实现人脸识别功能
  • STM32——SPI
  • JVM基础篇学习笔记
  • Stable Diffusion不同部件拆分详解!
  • Computed property XXX was assigned to but it has no setter
  • Cumulo 的 ClojureScript 模块已经成型
  • Java 实战开发之spring、logback配置及chrome开发神器(六)
  • JS进阶 - JS 、JS-Web-API与DOM、BOM
  • k8s如何管理Pod
  • MySQL QA
  • Spring Cloud Feign的两种使用姿势
  • Webpack入门之遇到的那些坑,系列示例Demo
  • 从 Android Sample ApiDemos 中学习 android.animation API 的用法
  • 基于web的全景—— Pannellum小试
  • 力扣(LeetCode)21
  • 浅谈JavaScript的面向对象和它的封装、继承、多态
  • 如何使用 OAuth 2.0 将 LinkedIn 集成入 iOS 应用
  • -- 数据结构 顺序表 --Java
  • 正则与JS中的正则
  • 最近的计划
  • 阿里云IoT边缘计算助力企业零改造实现远程运维 ...
  • ​LeetCode解法汇总2808. 使循环数组所有元素相等的最少秒数
  • (09)Hive——CTE 公共表达式
  • (java)关于Thread的挂起和恢复
  • (k8s)Kubernetes本地存储接入
  • (Redis使用系列) Springboot 实现Redis消息的订阅与分布 四
  • (STM32笔记)九、RCC时钟树与时钟 第一部分
  • (七)MySQL是如何将LRU链表的使用性能优化到极致的?
  • (三)c52学习之旅-点亮LED灯
  • (十一)手动添加用户和文件的特殊权限
  • (数据大屏)(Hadoop)基于SSM框架的学院校友管理系统的设计与实现+文档
  • (循环依赖问题)学习spring的第九天
  • (一)、python程序--模拟电脑鼠走迷宫
  • (转) SpringBoot:使用spring-boot-devtools进行热部署以及不生效的问题解决
  • (转)chrome浏览器收藏夹(书签)的导出与导入
  • (转)LINQ之路
  • (轉貼) UML中文FAQ (OO) (UML)
  • .bat批处理(三):变量声明、设置、拼接、截取
  • .NET Framework .NET Core与 .NET 的区别
  • .NET 应用启用与禁用自动生成绑定重定向 (bindingRedirect),解决不同版本 dll 的依赖问题
  • .NET 中小心嵌套等待的 Task,它可能会耗尽你线程池的现有资源,出现类似死锁的情况
  • .Net(C#)自定义WinForm控件之小结篇
  • .NET/C# 使用反射注册事件
  • @Bean, @Component, @Configuration简析