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

news2025/1/5 21:58:07

  博客主页:花果山~程序猿-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; // 只关心可读事件

    // 假设这里有一个套接字,其文件描述符为 sockfd
    int 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 就绪通知方法。

结语

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/2143419.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

无人机助力智慧农田除草新模式,基于YOLOv10全系列【n/s/m/b/l/x】参数模型开发构建无人机航拍场景下的农田杂草检测识别系统

科技发展到今天&#xff0c;无人机喷洒药物已经不是一件新鲜事情了&#xff0c;在很多高危的工作领域中&#xff0c;比如高空电力设备除冰&#xff0c;电力设备部件传送更换等等&#xff0c;无人机都可以扮演非常出色的作用&#xff0c;前面回到老家一段时间&#xff0c;最近正…

算法题总结(一)——二分查找专题

二分查找 我们二分查找的本质就是每次能够通过中间值来进行分割&#xff0c;能够比较判断&#xff0c;查找到或者接近需要的数据&#xff0c;然后把一部分的数据丢弃掉。 原题 给定一个 n 个元素有序的&#xff08;升序&#xff09;整型数组 nums 和一个目标值 target &…

LabVIEW提高开发效率技巧----使用快捷键

在LabVIEW的开发过程中&#xff0c;熟练掌握和运用快捷键可以极大地提升工作效率&#xff0c;减少重复性操作所花费的时间。快捷键不仅可以加快编程速度&#xff0c;还能让开发者更加专注于逻辑实现和功能设计。细问问将详细介绍LabVIEW中的常用快捷键&#xff0c;特别是强大的…

101012分页属性

4k页面 P&#xff08;有效位&#xff09;&#xff1a;1有效&#xff0c;0无效 R/W&#xff08;读写位&#xff09;&#xff1a;1可读可写&#xff0c;0可读 U/S&#xff08;权限位&#xff09;&#xff1a;1(User)&#xff0c;0(System) A&#xff08;物理页访问位&#xff…

医学数据分析实训 项目五 聚类分析--蛋白质消费结构分析--车辆驾驶行为指标

文章目录 项目五&#xff1a;聚类分析实践目的实践平台实践内容任务一&#xff1a;蛋白质消费结构分析步骤 任务一&#xff1a;蛋白质消费结构分析数据预处理&#xff08;四&#xff09;模型建立及优化KMeans 任务二 车辆驾驶行为&#xff08;四&#xff09;模型建立及优化 项目…

并发带来的对象一致性问题

多线程操作带来数据不一致情况分析&#xff0c;简单demo。 public class Object_IS {private Student_Object so new Student_Object("张三", 123);public static void main(String[] args) throws InterruptedException {Object_IS os new Object_IS();os.test1(…

论文笔记:交替单模态适应的多模态表征学习

整理了CVPR2024 Multimodal Representation Learning by Alternating Unimodal Adaptation&#xff09;论文的阅读笔记 背景MLA框架实验Q1 与之前的方法相比&#xff0c;MLA能否克服模态懒惰并提高多模态学习性能?Q2 MLA在面临模式缺失的挑战时表现如何?Q3 所有模块是否可以有…

输电线路缺陷检测数据集(绝缘子自爆,破损,闪络,鸟巢,防震锤脱落五种缺陷)

输电线路数据集&#xff08;绝缘子自爆&#xff0c;破损&#xff0c;闪络&#xff0c;鸟巢&#xff0c;防震锤脱落五种缺陷&#xff09;包括&#xff1a; 1.绝缘子自爆 2.绝缘子破损绝、闪络 3.鸟巢 4.防震锤脱落 数据增强后的数量 对应数量&#xff1a;1828&#xff0c;1467&a…

类加载器详细介绍

类加载器我们要聊一个神秘而又重要的角色——Java类加载器。这家伙&#xff0c;就像是个超级英雄&#xff0c;总是在关键时刻挺身而出&#xff0c;为我们的Java程序提供强大的支持。我会尽量用简单易懂的方式来介绍它。 一 、类加载器介绍 1、类加载器是什么&#xff1f; 想象…

高频率快响应信号隔离变送器

隔离变送器相册: 隔离变送器图片----捷晟达科技​​​​ 隔离变送器---深圳捷晟达科技 隔离变送器---捷晟达科技 高频率快响应信号隔离变送器 定义&#xff1a; 高频率是指隔离变送器从输入到输出采样时的时间&#xff0c;该产品响应频率从10KHz~100KHz&#xff0c;产品精度高&…

【CSS】选择器(基础选择器、复合选择器、属性匹配选择器、结构伪类选择器、伪元素选择器)

选择器 引入方式基础选择器复合选择器属性匹配选择器结构伪类选择器伪元素选择器 引入方式 1&#xff1a;外联 <!-- css引入方式1&#xff1a;外联 外联与内嵌优先级相同&#xff0c;取决于加载顺序 --><link rel"stylesheet" type"text/css" h…

箭头检测系统源码分享

箭头检测检测系统源码分享 [一条龙教学YOLOV8标注好的数据集一键训练_70全套改进创新点发刊_Web前端展示] 1.研究背景与意义 项目参考AAAI Association for the Advancement of Artificial Intelligence 项目来源AACV Association for the Advancement of Computer Vision …

子查询优化

MySQL学习大纲 我的数据库学习大纲 1、什么是子查询&#xff1a; 1.MySQL 从 4.1 版本开始支持子查询&#xff0c;使用子查询可以进行 SELECT 语句的嵌套查询&#xff0c;即一个 SELECT 查询的结果作为另一个 SELECT 语句的条件。子查询可以一次性完成很多逻辑上需要多个步骤才…

二分+构造,CF 1063C - Dwarves, Hats and Extrasensory Abilities

目录 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 二、解题报告 1、思路分析 2、复杂度 3、代码详解 一、题目 1、题目描述 2、输入输出 2.1输入 2.2输出 3、原题链接 1063C - Dwarves, Hats and Extrasensory Abilities 二、解题报告 1、思路…

AtCoder ABC370 A-D题解

比赛链接:ABC370 AT 上 400 分寄。 Problem A: Code #include <bits/stdc.h> using namespace std; int main(){int L,R;cin>>L>>R;if(LR)cout<<"Invalid"<<endl;else if(L1)cout<<"YES"<<endl;elsecout<…

【数据结构】经典题

所以&#xff0c;语句 x; 的语句频度为&#xff1a;n(n1)(n2&#xff09;/6 选C 临时变量 t&#xff1a;只使用了一个额外的变量来存储交换的值。 没有使用额外的数组&#xff1a;所有的操作都是在原数组 a 上进行的。 因此&#xff0c;算法的空间复杂度是常数级别的&#xff0…

定位HardFault

一、HardFault定义 STM32出现HardFault_Handler硬件错误的原因主要有两个方面&#xff1a; 1、内存溢出或者访问越界。&#xff08;包括使用野指针&#xff09; 2、堆栈溢出。 二、定位HardFault步骤 1. 判断所使用堆栈&#xff1a; 发生异常之后可首先查看LR寄存器中的值…

十五,Spring Boot 整合连接数据库(详细配置)

十五&#xff0c;Spring Boot 整合连接数据库(详细配置) 文章目录 十五&#xff0c;Spring Boot 整合连接数据库(详细配置)最后&#xff1a; JDBC HikariDataSource(Spring Boot内置的数据库) HikariDataSource: 目前市面上非常优秀的数据源&#xff0c;是 Spring Boot2默认数…

gcc/g++的使用:

目录 (1). 程序的翻译过程 预处理&#xff1a; gcc -E 源文件 编译&#xff1a; gcc -S 源文件 汇编&#xff1a;gcc -c 源文件 连接&#xff1a; (2) 语言的自举(也叫 编译器的自举)&#xff1a; (3). 查看可执行程序在连接时依赖的库: ldd 可执行程序的名字 。 (4). …

C语言 | Leetcode C语言题解之第414题第三大的数

题目&#xff1a; 题解&#xff1a; int cmp(const void *a, const void *b) {return *(int*)a < *(int*)b; }int thirdMax(int* nums, int numsSize){qsort(nums, numsSize, sizeof(nums[0]), cmp);int diff 0;for (int i 1; i < numsSize; i) {if (nums[i] ! nums[i…