Linux高级IO之select

 (。・∀・)ノ゙嗨!你好这里是ky233的主页:这里是ky233的主页,欢迎光临~icon-default.png?t=N7T8https://blog.csdn.net/ky233?type=blog

点个关注不迷路⌯'▾'⌯

目录

一、五种IO模型 

1.IO效率的问题

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

3.非阻塞IO

4.信号驱动IO

5.IO多路转接

6.异步IO

7.小结

二、非阻塞IO

1.fcntl

三、I/O多路转接之select

1.快速认识select接口

2.fd_set

3.接口挑一个重点参数,细致分析

4.编写代码

5.select的优缺点


网络的本质就是:IO,是一次输入和输出

一、五种IO模型 

1.IO效率的问题

IO的效率是很低效的,毕竟对方在千里之外。

IO为什么低效,以读为例:

  1. 当我们read/recv的时候,如果底层缓冲区没有数据,read/recv会怎么办?-----阻塞
  2. 当我们read/recv的时候,如果底层缓冲区有数据,read/recv会怎么办?-----拷贝
  3. 所以一次IO=等+数据拷贝
  4. 所以read、recv、write、send等都是在等io就绪,然后发起拷贝

那么什么叫做低效的IO呢?

单位时间,大部分时间这些io类的接口都在等数据!

那么如何提高IO的效率呢?

想办法在单位时间等的比重变低,那么IO的效率就高

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

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

3.非阻塞IO

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

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

4.信号驱动IO

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

5.IO多路转接

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

6.异步IO

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

7.小结

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

二、非阻塞IO

1.fcntl

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

函数原型如下:

#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)
#include <iostream>
#include <cstring>
#include <ctime>
#include <cassert>
#include <cerrno>
#include <fcntl.h>
#include <unistd.h>

#include <sys/time.h>

bool SetNonBlock(int fd)
{
    int fl = fcntl(fd, F_GETFL); // 在底层获取当前fd对应的文件读写标志位
    if (fl < 0)
        return false;
    fcntl(fd, F_SETFL, fl | O_NONBLOCK); // 设置非阻塞
    return true;
}

int main()
{
   
     // 0
     SetNonBlock(0); //只要设置一次,后续就都是非阻塞了

     char buffer[1024];
     while (true)
     {
         sleep(1);
         errno = 0;
         // 非阻塞的时候,我们是以出错的形式返回,告知上层数据没有就绪:
         // a. 我们如何甄别是真的出错了
         // b. 还是仅仅是数据没有就绪呢?
         // 数据就绪了的话,我们就正常读取就行
         ssize_t s = read(0, buffer, sizeof(buffer) - 1); //出错,不仅仅是错误返回值,errno变量也会被设置,表明出错原因
         if (s > 0)
         {
             buffer[s-1] = 0;
             std::cout << "echo# " << buffer << " errno[---]: " << errno << " errstring: " << strerror(errno) << std::endl;
         }
         else
         {
             // 如果失败的errno值是11,就代表其实没错,只不过是底层数据没就绪
             //std::cout << "read \"error\" " << " errno: " << errno << " errstring: " << strerror(errno) << std::endl;

            if(errno == EWOULDBLOCK || errno == EAGAIN)
            {
              std::cout << "当前0号fd数据没有就绪, 请下一次再来试试吧" << std::endl;
                 continue;
            }
             else if(errno == EINTR)
             {
                 std::cout << "当前IO可能被信号中断,在试一试吧" << std::endl;
                 continue;
             }
             else
            {
                 //进行差错处理
            }
        }
    }
    return 0;

三、I/O多路转接之select

select其实就是帮助用户一次等待多个文件sock,当那些文件sock就绪了,只要通知用户,对应的sock文件就绪了,然后用户在调用如read、recv等接口进行数据读取!

1.快速认识select接口

int select(int nfds, fd_set *readfds, fd_set *writefds,
 fd_set *exceptfds, struct timeval *timeout);
  • 参数nfds是需要监视的最大的文件描述符值+1;
  • rdset,wrset,exset分别对应于需要检测的可读文件描述符的集合,可写文件描述符的集 合及异常文件描 述符的集合;
  • 参数timeout为结构timeval,用来设置select()的等待时间
  • 返回值为就绪的fd的个数
  • 至少有一个fd数据就绪or空间就绪,此时就可以进行返回

参数timeout取值:

  • 获取当前时间的时间戳
  • NULL:则表示select()没有timeout,select将一直被阻塞,直到某个文件描述符上发生了事件;
  • 0:仅检测描述符集合的状态,然后立即返回,并不等待外部事件的发生。
  • 特定的时间值:如果在指定的时间段里没有事件发生,select将超时返回

2.fd_set

这是一个位图结构,分别表示文件描述符文件描述符集

提供了一组操作fd_set的接口, 来比较方便的操作位图.

 void FD_CLR(int fd, fd_set *set); // 用来清除描述词组set中相关fd 的位
 int FD_ISSET(int fd, fd_set *set); // 用来测试描述词组set中相关fd 的位是否为真
 void FD_SET(int fd, fd_set *set); // 用来设置描述词组set中相关fd的位
 void FD_ZERO(fd_set *set); // 用来清除描述词组set的全部位

3.接口挑一个重点参数,细致分析

假设我们今天是一个读文件描述符集

readfds:

在输入时:用户告诉内核我的比特位中,比特位的位置,表示文件描述符值,比特位的内容表示,是否关心,如0100,这个表示,我们关心3这个文件描述符,124,这三个文件描述符我们并不关心

在输出时:内核告诉用户,我是OS,用户你让我关心的多个fd有结果了,比特位的位置依旧表示文件描述符值。比特位的内容表示是否就绪,如0100,表示我们的3号文件描述符已经就绪了,如果是0000,则表示3号文件描述符还未就绪,就绪之后就代表,用户可以直接读取3号而不被阻塞

注意,用户和OS都会修改同一个位图结构,这个参数被用一次之后,就需要被重新设定!

4.编写代码

详情讲解看代码

#ifndef __SELECT_SVR_H__
#define __SELECT_SVR_H__

#include <iostream>
#include <string>
#include <vector>
#include <sys/select.h>
#include <sys/time.h>
#include "Log.hpp"
#include "Sock.hpp"

#define BITS 8
#define NUM (sizeof(fd_set)*BITS)
#define FD_NONE -1

using namespace std;
// select 我们只完成读取,写入和异常不做处理 -- epoll(写完整)
class SelectServer
{
public:
    SelectServer(const uint16_t &port = 8080) : _port(port)
    {
        _listensock = Sock::Socket();
        Sock::Bind(_listensock, _port);
        Sock::Listen(_listensock);
        logMessage(DEBUG,"%s","create base socket success");
        for(int i = 0; i < NUM; i++) _fd_array[i] = FD_NONE;
        // 规定 : _fd_array[0] = _listensock;
        _fd_array[0] = _listensock;
    }

    void Start()
    {
        while (true)
        {
            // struct timeval timeout = {0, 0};
            // 如何看待listensock? 获取新连接,我们把它依旧看做成为IO,input事件,如果没有连接到来呢?阻塞
            // int sock = Sock::Accept(listensock, ...); //不能直接调用accept了
            // 将listensock添加到读文件描述符集中
            // FD_SET(_listensock, &rfds); 
            // int n = select(_listensock + 1, &rfds, nullptr, nullptr, &timeout);

            // 1. nfds: 随着我们获取的sock越来越多,随着我们添加到select的sock越来越多,注定了nfds每一次都可能要变化,我们需要对它动态计算
            // 2. rfds/writefds/exceptfds:都是输入输出型参数,输入输出不一定以一样的,所以注定了我们每一次都要对rfds进行重新添加
            // 3. timeout: 都是输入输出型参数,每一次都要进行重置,前提是你要的话
            // 1,2 => 注定了我们必须自己将合法的文件描述符需要单独全部保存起来 用来支持:1. 更新最大fd 2.更新位图结构

            DebugPrint();

            fd_set rfds;
            FD_ZERO(&rfds);
            int maxfd = _listensock;
            for(int i = 0; i < NUM; i++)
            {
                if(_fd_array[i] == FD_NONE) continue;
                FD_SET(_fd_array[i], &rfds);
                if(maxfd < _fd_array[i]) maxfd = _fd_array[i];
            }
            // rfds未来,一定会有两类sock,listensock,普通sock
            // 我们select中,就绪的fd会越来越多!
            int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr);
            switch (n)
            {
            case 0:
                // printf("hello select ...\n");
                logMessage(DEBUG, "%s", "time out...");
                break;
            case -1:
                logMessage(WARNING, "select error: %d : %s", errno, strerror(errno));
                break;
            default:
                // 成功的
                logMessage(DEBUG, "get a new link event..."); // 为什么会一直打印连接到来呢?连接已经建立完成,就绪了,但是你没有取走,select要一直通知你!
                HandlerEvent(rfds);
                break;
            }
        }
    }

    ~SelectServer()
    {
        if (_listensock >= 0)
            close(_listensock);
    }
private:
    void HandlerEvent(const fd_set &rfds) // fd_set 是一个集合,里面可能会存在多个sock
    {
        for(int i = 0; i < NUM; i++)
        {
            // 1. 去掉不合法的fd
            if(_fd_array[i] == FD_NONE) continue;
            // 2. 合法的就一定就绪了?不一定
            if(FD_ISSET(_fd_array[i], &rfds))
            {
                //指定的fd,读事件就绪
                // 读事件就绪:连接时间到来,accept
                if(_fd_array[i] == _listensock) Accepter();
                else Recver(i);
            }
        }
    }
    void Accepter()
    {
        string clientip;
        uint16_t clientport = 0;
        // listensock上面的读事件就绪了,表示可以读取了
        // 获取新连接了
        int sock = Sock::Accept(_listensock, &clientip, &clientport); // 这里在进行accept会不会阻塞?不会!
        if(sock < 0)
        {
            logMessage(WARNING, "accept error");
            return;
        }
        logMessage(DEBUG, "get a new line success : [%s:%d] : %d", clientip.c_str(), clientport, sock);
        // read / recv? 不能!为什么不能?我们不清楚该sock上面数据什么时候到来, recv、read就有可能先被阻塞,IO = 等+数据拷贝
        // 谁可能最清楚呢?select!
        // 得到新连接的时候,此时我们应该考虑的是,将新的sock托管给select,让select帮我们进行检测sock上是否有新的数据
        // 有了数据select,读事件就绪,select就会通知我,我们在进行读取,此时我们就不会被阻塞了
        // 要将sock添加 给 select, 其实我们只要将fd放入到数组中即可!
        int pos = 1;
        for(; pos < NUM; pos++){
            if(_fd_array[pos] == FD_NONE) break;
        }
        if(pos == NUM){
            logMessage(WARNING, "%s:%d", "select server already full,close: %d", sock);
            close(sock);
        }else{
            _fd_array[pos] = sock;
        }
    }
    void Recver(int pos)
    {
        // 读事件就绪:INPUT事件到来、recv,read
        logMessage(DEBUG, "message in, get IO event: %d", _fd_array[pos]);
        // 暂时先不做封装, 此时select已经帮我们进行了事件检测,fd上的数据一定是就绪的,即 本次 不会被阻塞
        // 这样读取有bug吗?有的,你怎么保证以读到了一个完整包文呢?
        char buffer[1024];
        int n = recv(_fd_array[pos], buffer, sizeof(buffer)-1, 0);
        if(n > 0){
            buffer[n] = 0;
            logMessage(DEBUG, "client[%d]# %s", _fd_array[pos], buffer);
        }
        else if(n == 0){
            logMessage(DEBUG, "client[%d] quit, me too...", _fd_array[pos]);
            // 1. 我们也要关闭不需要的fd
            close(_fd_array[pos]);
            // 2. 不要让select帮我关心当前的fd了
            _fd_array[pos] = FD_NONE;
        }
        else{
            logMessage(WARNING, "%d sock recv error, %d : %s", _fd_array[pos], errno, strerror(errno));
            // 1. 我们也要关闭不需要的fd
            close(_fd_array[pos]);
            // 2. 不要让select帮我关心当前的fd了
            _fd_array[pos] = FD_NONE;
        }
    }

    void DebugPrint()
    {
        cout << "_fd_array[]: ";
        for(int i = 0; i < NUM; i++)
        {
            if(_fd_array[i] == FD_NONE) continue;
            cout << _fd_array[i] << " ";
        }
        cout << endl;
    }
private:
    uint16_t _port;
    int _listensock;
    int _fd_array[NUM];
    // int _fd_write[NUM];
    // std::vector<int> arr;
};

#endif

5.select的优缺点

优点:任何一个多路转接都具有

  • 效率高,在单位时间内,等的比重大大减小
  • 省资源,应用场景:有大量的连接,但是只有少量的活跃

缺点:

  • 为了维护第三方数组,服务器会充满大量的遍历,时间复杂度时On的,在OS底层也要关心fd,也要进行遍历
  • 每一次都要对select输出参数重新设定
  • 能够同时管理的fd个数是有上限的
  • 因为几乎每一个参数都是输入输出型的,select一定会频繁的进行用户到内核,内核到用户的参数数据拷贝
  • 编码复杂

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

蓝桥杯C/C++实用知识总结

蓝桥杯C/C 文章目录 蓝桥杯C/C头文件实用函数及运算符求幂次移位运算符STL排序sort()函数依次读取数据STL全排列函数next_permutation()求数组最大/最小值初始化函数memset()GCD(最大公约数)和LCM&#xff08;最小公倍数&#xff09;C字符串函数 实用数据结构模板vector链表lis…

未来城市:探索数字孪生在智慧城市中的实际应用与价值

目录 一、引言 二、数字孪生与智慧城市的融合 三、数字孪生在智慧城市中的实际应用 1、智慧交通管理 2、智慧能源管理 3、智慧建筑管理 4、智慧城市管理 四、数字孪生在智慧城市中的价值 五、挑战与展望 六、结论 一、引言 随着科技的飞速发展&#xff0c;智慧城市已…

鸿蒙OpenHarmony HDF 驱动开发

目录 序一、概述二、HDF驱动框架三、驱动程序四、驱动配置坚持就有收获 序 最近忙于适配OpenHarmonyOS LiteOS-M 平台&#xff0c;已经成功实践适配平台GD32F407、STM32F407、STM32G474板卡&#xff0c;LiteOS适配已经算是有实际经验了。 但是&#xff0c;鸿蒙代码学习进度慢下…

超网、IP 聚合、IP 汇总分别是什么?三者有啥区别和联系?

一、超网 超网&#xff08;Supernet&#xff09;是一种网络地址聚合技术&#xff0c;它可以将多个连续的网络地址合并成一个更大的网络地址&#xff0c;从而减少路由表的数量和大小。超网技术可以将多个相邻的网络地址归并成一个更大的网络地址&#xff0c;这个更大的网络地址…

Lesson 6 Convolutional Neural Network(CNN)

听课&#xff08;李宏毅老师的&#xff09;笔记&#xff0c;方便梳理框架&#xff0c;以作复习之用。本节课主要讲了CNN的适用范围&#xff0c;整体架构与工作流程&#xff0c;CNN的应用&#xff0c;CNN的缺点以及解决方法。 1. CNN的输入与输出 CNN是专门为了图像而设计的一…

2.4_3 死锁的处理策略——避免死锁

文章目录 2.4_3 死锁的处理策略——避免死锁&#xff08;一&#xff09;什么是安全序列&#xff08;二&#xff09;安全序列、不安全状态、死锁的联系&#xff08;三&#xff09;银行家算法 总结 2.4_3 死锁的处理策略——避免死锁 银行家算法是“避免死锁”策略的最著名的一个…

【sgExcelGrid】自定义组件:简单模拟Excel表格拖拽、选中单元格、横行、纵列、拖拽圈选等操作

特性&#xff1a; 可以自定义拖拽过表格可以点击某个表格&#xff0c;拖拽右下角小正方形进行任意方向选取单元格支持选中某一行、列支持监听selectedGrids、selectedDatas事件获取选中项的DOM对象和数据数组支持props自定义显示label字段别名 sgExcelGrid源码 <template&g…

LLM长上下文外推方法

现在的LLM都集中在卷上下文长度了&#xff0c;最新的Claude3已经支持200K的上下文&#xff0c;见&#xff1a;cost-context。下面是一些提升LLM长度外推能力的方法总结&#xff1a; 数据工程 符尧大佬的最新工作&#xff1a;Data Engineering for Scaling Language Models to …

计算机网络——计算机网络的性能

计算机网络——计算机网络的性能 速率带宽吞吐量时延时延宽带积往返时间RTT利用率信道利用率网络利用率 我们今天来看看计算机网络的性能。 速率 速率这个很简单&#xff0c;就是数据的传送速率&#xff0c;也称为数据率&#xff0c;或者比特率&#xff0c;单位为bit/s&#…

C语言——强制类型转化

强制类型转化的作用 C语言中的强制类型转换是一种将一个数据类型转换为另一个数据类型的操作。它可以通过显式地指定要转换的数据类型来实现。强制类型转换可以用于以下几种情况&#xff1a; 改变变量的数据类型&#xff1a;当需要将一个变量的数据类型从一种类型转换为另一种…

【libwebrtc】基于m114

libwebrtc A C++ wrapper for binary release, mainly used for flutter-webrtc desktop (windows, linux, embedded).是 基于m114版本的webrtc 最新(20240309 ) 的是m122了。官方给出的构建过程 .gclient 文件 solutions = [{"name" : src,"url

域名交易系统已测试可正常使用免授权带后台

域名交易系统已测试可正常使用免授权带后台 下载地址&#xff1a;迅雷云盘

python处理geojson为本地shp文件

一.成果展示 二.环境 我是在Anaconda下的jupyter notebook完成代码的编写&#xff0c;下面是我对应的版本号&#xff0c;我建议大家在这个环境下编写&#xff0c;因为在下载gdal等包的时候会更方便。 二.参考网站 osgeo.osr module — GDAL documentation osgeo.ogr module …

链表基础知识详解

链表基础知识详解 一、链表是什么&#xff1f;1.链表的定义2.链表的组成3.链表的优缺点4.链表的特点 二、链表的基本操作1.链表的建立2.链表的删除3.链表的查找4.链表函数 一、链表是什么&#xff1f; 1.链表的定义 链表是一种物理存储单元上非连续、非顺序的存储结构&#xf…

SQLite3中的callback回调函数注意的细节

调用 sqlite3_exec(sqlite3*, const char *sql, sqlite_callback, void *data, char **errmsg)该例程提供了一个执行 SQL 命令的快捷方式&#xff0c; SQL 命令由 sql 参数提供&#xff0c;可以由多个 SQL 命令组成。 在这里&#xff0c; 第一个参数 sqlite3 是打开的数据库对…

Go语言数据结构(二)堆/优先队列

文章目录 1. container中定义的heap2. heap的使用示例3. 刷lc应用堆的示例 更多内容以及其他Go常用数据结构的实现在这里&#xff0c;感谢Star&#xff1a;https://github.com/acezsq/Data_Structure_Golang 1. container中定义的heap 在golang中的"container/heap"…

使用yarn创建vite+vue3electron多端运行

文章目录 第一步 使用yarn创建vite+vue3项目遇到创建报错看第二步 引入electron第三步 创建main.js在electron下面的main.js写入下面代码第四步 安装同时运行多条命令npm包&&修改package.json文件npm包增加一条electron运行脚本命令效果图第一步 使用yarn创建vite+vue3…

T-RAG = RAG + Fine-Tuning + Entity Detection

原文地址&#xff1a;T-RAG RAG Fine-Tuning Entity Detection T-RAG 方法的前提是将 RAG 架构与开源微调的 LLM 和实体树向量数据库相结合。重点是上下文检索。 2024 年 2 月 15 日 介绍 大型语言模型 (LLM) 越来越多地应用于各个领域&#xff0c;包括对私营企业文档的问答…

Pb量级超大容量光存储

近日&#xff0c;中国科学院上海光学精密机械研究所&#xff08;以下简称“上海光机所”&#xff09;与上海理工大学等科研单位合作&#xff0c;在超大容量三维超分辨光存储研究中取得突破性进展。研究团队利用国际首创的双光束调控聚集诱导发光超分辨光存储技术&#xff0c;实…

docker-compose这下会用了吗?

概要 默认的模板文件是 docker-compose.yml&#xff0c;其中定义的每个服务可以通过 image 指令指定镜像或 build 指令&#xff08;需要 Dockerfile&#xff09;来自动构建。 注意如果使用 build 指令&#xff0c;在 Dockerfile 中设置的选项(例如&#xff1a;CMD, EXPOSE, V…
最新文章