【Linux】从零开始使用多路转接IO --- select

在这里插入图片描述

碌碌无为,则余生太长;
欲有所为,则人生苦短。
--- 中岛敦 《山月记》---

从零开始认识五种IO模型

  • 1 前言
  • 2 认识多路转接select
  • 3 多路转接select等待连接
  • 4 完善代码
  • 5 总结

1 前言

上一篇文章我们讲解了五种IO模型的基本概念,并通过系统调用使用了非阻塞IO。
一般的服务器不会使用非阻塞IO,因为非阻塞IO非常耗费CPU资源,导致CPU发热效率下降!非阻塞IO只有在特定情况下才比较好用!

今天我们来学习多路转接select

我们知道IO = 等 + 拷贝。拷贝的前提是底层有数据,没有数据的时候就需要进行等待。为了提高效率可以等待多个文件描述符。多路转接就是等待文件描述符上的新事件,等到就可以通知程序员事件已经就绪,可以进行拷贝!

这个事件可以是:

  • 读事件就绪:OS底层有数据了
  • 写事件就绪:OS底层有空间了

今天我们要学习的就是多路转接select

2 认识多路转接select

我们先来看其作用与定位:

  • select的定位是:只在IO中只负责等待,不进行拷贝! 并且select可以等待多个文件描述符,有新事件就进行通知。

来看select系统调用:

SELECT(2)                                                                 Linux Programmer's Manual                                                                 SELECT(2)NAMEselect, pselect, FD_CLR, FD_ISSET, FD_SET, FD_ZERO - synchronous I/O multiplexingSYNOPSIS#include <sys/select.h>int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval *timeout);void FD_CLR(int fd, fd_set *set);int  FD_ISSET(int fd, fd_set *set);void FD_SET(int fd, fd_set *set);void FD_ZERO(fd_set *set);int pselect(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, const struct timespec *timeout,const sigset_t *sigmask);Feature Test Macro Requirements for glibc (see feature_test_macros(7)):pselect(): _POSIX_C_SOURCE >= 200112L

select函数中有5个参数,都是用来干什么的呢?

  1. int nfds:输入性参数 ,表示等待的多个文件描述符最大值 加 1。比如等待1 2 5 6 99 这几个文件描述符,那么就要传入100。注意不是文件描述符的个数!
  2. struct timeval *timeout:输入输出性参数 ,这是一个结构体表示微秒级别的时间戳,其中有两个参数分别表示秒和微秒。这个参数告诉select在这个时间戳内进行阻塞式select,超出时间就进行一次返回。如果时间以内等到了新事件,就返回,并把剩余时间返回。传入{0,0}就是非阻塞轮询了。传入nullptr表示一直阻塞等待事件
    在这里插入图片描述

那么现在我们知道了两个参数,我们探索一下返回值:

  • 大于0:有几个就绪了
  • 等于0:超时返回了
  • 小于0:select出错了

那么其他三个参数呢?首先fd_set代表文件描述符集,是用位图进行维护的!位图下标表示文件描述符,该比特位的内容表示对应信息!一共1024比特位,可以表示1024个文件描述符,下面我们就来了解一下这三个参数:
这三个参数都是fd_set,是输入输出参数,分别对应读事件,写事件,异常事件。通过这三个位图的设置,我们就可以对一个文件描述符的操作指明清楚。今天我们以读事件为例进行讲解:

  • 输入时:传入一个读事件文件描述符,就是告诉OS要帮我们关心fd_set集合中的所有fd的读事件。这里比特位的位置表示文件描述符的编号,比特位的内容表示是否关心fd的读事件!
  • 输出时:OS会返回一个读事件文件描述符,表示你让我关心的文件描述符集中哪些已经就绪了!这里比特位的位置表示文件描述符的编号,比特位的内容表示事件是否发生!

OK,现在我们了解了select的基本参数,下面我们就开始使用select进行编程

3 多路转接select等待连接

我们首先把之前的套接字基础的类拷贝过来:

  1. class Socket:实现套接字的创建工作,并进入监听模式。
  2. class InetAddr:网络套接字基本信息类,用于进行网络套接字传参工作。
  3. class Log:进行日志信息的打印,便于调试

然后我们就来设计Selectsever类:

  • 成员变量需要端口号,TcpSocket套接字类
  • 构造函数中进行端口号的初始化,并创建套接字,设置为监听模式
  • 循环函数中不能直接进行accept获取连接,因为底层不一定有数据,直接进行会阻塞式等待。所以我们可以把accept看做IO函数,将等的任务交给select函数。
  • select函数需要对监听套接字进行等待
#pragma once#include "Socket.hpp"
#include <sys/select.h>
#include "Log.hpp"using namespace socket_ns;
using namespace log_ns;class SelectServer
{
public:SelectServer(uint16_t port) : _port(port),_listensock(std::make_unique<TcpSocket>()){// 建立监听套接字_listensock->BuildListenSocket(_port);}~SelectServer(){};void Initserver(){}void Loop(){//进入服务while(true){//不能直接进行accept 因为底层不一定建立了连接,所以需要等待底层就绪//等待过程交给select//_listensock->Accepter();//创建fd_setfd_set rfds ;FD_ZERO(&rfds);//加入监听套接字文件描述符FD_SET( _listensock->GetSockfd() , &rfds);//创建timeoutstruct timeval timeout = {3 , 0};//进行selectint n = ::select(_listensock->GetSockfd() + 1 , &rfds ,  nullptr , nullptr , &timeout);switch (n){case 0://超时LOG(DEBUG , "timeout : %d.%d\n" , timeout.tv_sec , timeout.tv_usec);break;case -1://出错了LOG(ERROR, "select error\n");break;default://正常LOG(INFO, "have event ready: n = %d\n" , n);//执行任务HandlerEvent(rfds);break;}}}private:uint16_t _port;std::unique_ptr<Socket> _listensock;
};

我们运行程序来看等待效果:
在这里插入图片描述
可以正常的进行等待,当我们进行连接时:
在这里插入图片描述
select函数就能告诉我们有哪些文件描述符就绪,可以进行拷贝。这里可以得到一个现象:

  • 如果事件就绪,但是不处理,select就会一直通知我们,直到我们处理这个事件。

当我们知道底层就绪时,我们就可以进行"拷贝"了:

void HandlerEvent(fd_set& rfds){//判断是否是套接字就绪if(FD_ISSET(_listensock->GetSockfd() , &rfds)){//连接事件就绪//那么这里我们可以进行accept吗?InetAddr addr;int sockfd = _listensock->Accepter(&addr);//已经就绪 ,不会阻塞//这时会得到一个新连接if(sockfd > 0){LOG(DEBUG ,"get a new link , client info %s:%d\n" , addr.Ip().c_str() ,addr.Port());//TODO}else{return ;}}}

但是有几个问题:

  1. 在上面的handler函数中,我们已经获取到了连接,那么下面敢不敢直接进行读取呢?
    当然不能,因为建立连接并不代表会有请求传过来!所以还需要等待请求!
  2. 那么怎么知道底层有没有就绪呢?
    还是通过select进行等待,想办法将新的fd添加给select,进行统一管理!
  3. 那么这样select等待的fd不就越来越多,这要怎么进行维护呢?
    通过辅助数据结构进行维护!由于select接口的参数是输入输出性,无法保存文件描述符,所以必然需要额外的数据结构进行维护文件描述符!

4 完善代码

针对上面的三个问题,我们首先要做的就是想办法通过一个数据结构维护需要进行select的文件描述符。每次进入循环进行select时,就要通过这个数据结构初始化rfds!然后在通过对返回值的rfds与辅助数据结构中的文件描述符进行比对,对有新事件的文件描述符进行处理!

对于这个数据结构我们选择最简单的一维C风格数组即可!进行初始化时都设置为默认值-1

	const static int gnum = sizeof(fd_set) * 8;const static int gdefault = -1;//...void Initserver(){// 对数组进行初始化for (int i = 0; i < gnum; i++){fd_array[i] = gdefault;}// 加入监听套接字fd_array[0] = _listensock->GetSockfd();}//...// 辅助数组int fd_array[gnum];

通过这个数组,当我们进行循环时,每次就都需要通过这个数组进行初始化rfds

 void Loop(){// 进入服务while (true){// 创建fd_setfd_set rfds;FD_ZERO(&rfds);int max_fd = 0;// 首先根据fd_array将合法fd加入到rfdsfor (int i = 0; i < gnum; i++){if (fd_array[i] == gdefault)continue;// 加入合法的文件描述符FD_SET(fd_array[i], &rfds);// 维护一个文件描述符最大值if (fd_array[i] > max_fd)max_fd = fd_array[i];}// 创建timeoutstruct timeval timeout = {30, 0};// 进行selectint n = ::select(max_fd + 1, &rfds, nullptr, nullptr, &timeout);switch (n){case 0:// 超时LOG(DEBUG, "timeout : %d.%d\n", timeout.tv_sec, timeout.tv_usec);break;case -1:// 出错了LOG(ERROR, "select error\n");break;default:// 正常LOG(INFO, "have event ready: n = %d\n", n);// 处理事件HandlerEvent(rfds);PrintDebug();break;}}}

接下来我们来看handlerevent函数,进行select之后,如果有事件就绪,程序就会进入handlerevent函数。那么我们要如何判断是哪一个文件操作符的事件就绪了呢?

  • 直接遍历数组,进行FD_ISSET,通过对每一个合法fd进行判断,我们就能够知道是哪一个文件操作符有事件就绪!
  • 如果是listenfd就绪,说明有新连接,需要进行accepter获取新连接的fd,将其存入到文件描述符数组中!
  • 如果是普通fd就绪,我们进行读写操作即可,如果有连接退出了,要及时更新数组。
    void Accepter(){// 连接事件就绪InetAddr addr;int sockfd = _listensock->Accepter(&addr); // 已经就绪 ,不会阻塞// 这时会得到一个新连接if (sockfd > 0){LOG(DEBUG, "get a new link , client info %s:%d\n", addr.Ip().c_str(), addr.Port());// 将新获取的fd加入到数组中LOG(INFO, "get new fd :%d\n", sockfd);bool flag = false;for (int i = 0; i < gnum; i++){if (fd_array[i] == gdefault){flag = true;fd_array[i] = sockfd;break;}elsecontinue;}if (flag == false){LOG(WARNING, "fd_array have fill!\n");}}}void HandlerIO(int &fd){char buffer[1024];int n = ::recv(fd, buffer, sizeof(buffer) - 1, 0);if (n > 0){// 读取到了数据buffer[n] = 0;std::string echo_str = "[client say]#";echo_str += buffer;std::cout << echo_str << std::endl;// 返回一个报文std::string content = "<html><body><h1>hello bite</h1></body></html>";std::string ret_str = "HTTP/1.0 200 OK\r\n";ret_str += "Content-Type: text/html\r\n";ret_str += "Content-Length: " + std::to_string(content.size()) + "\r\n\r\n";ret_str += content;// echo_str += buffer;::send(fd, ret_str.c_str(), ret_str.size(), 0); // 临时方案}else if (n == 0){// 此时fd退出了LOG(INFO, "fd:%d quit!\n", fd);::close(fd);fd = gdefault;}else{LOG(ERROR, "recv error! errno:%d\n", errno);::close(fd);fd = gdefault;}}void HandlerEvent(fd_set &rfds){// 遍历fd_array判断是否有就绪的新事件for (int i = 0; i < gnum; i++){if (fd_array[i] == gdefault)continue;// 如果有新事件if (FD_ISSET(fd_array[i], &rfds)){// 进行判断是scokfd 还是普通fdif (fd_array[i] == _listensock->GetSockfd()){Accepter();}// 普通fd 进行正常读写else{HandlerIO(fd_array[i]);}}}}

这样就使用select完成了对连接的获取读取工作!来看效果:
在这里插入图片描述
可以看到,我们的数组中的有效fd随着客户端连接与中断会动态变化!

5 总结

根据上面的代码,我们可以总结出select的一些优缺点:

  1. 每次调用 select,都需要手动设置 fd 集合, 从接口使用角度来说也非常不便.
  2. 每次调用 select, 都需要把 fd 集合从用户态拷贝到内核态, 这个开销在 fd 很多时会很大。这个是多路转接IO无法避免的问题!
  3. 同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时很大。
  4. select 支持的文件描述符数量太小!虽然操作系统中文件描述符也有限制,但是这是操作系统的缺陷。同样select也是缺点

这里不断的要进行循环遍历数组,造成的性能开销是比较大的!所以就有了其他两种多路转接方案:poll与epoll

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

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

相关文章

【Java SE 】String 类 详解!

&#x1f525;博客主页&#x1f525;&#xff1a;【 坊钰_CSDN博客 】 欢迎各位点赞&#x1f44d;评论✍收藏⭐ 1. String 的地位 在Java 编程中&#xff0c;字符串的使用是非常频繁的&#xff0c;而字符串的使用有离不开 String类 &#xff0c;在开发和面试中String类也是非常…

专业130+总400+武汉理工大学855信号与系统考研经验电子信息与通信工程,真题,大纲,参考书。

已经顺利读研一段时间&#xff0c;回顾一下考研还是历历在目。应群里学弟要求&#xff0c;回忆总结一下自己考研经历&#xff0c;希望对大家复习有帮助。总分400&#xff0c;专业课855信号与系统130&#xff08;犯了低级错误&#xff0c;计算出现问题&#xff0c;大家专业好好准…

Self-Lengthen:阿里千问开源提升 LLM 长文本生成能力的训练框架

❤️ 如果你也关注大模型与 AI 的发展现状&#xff0c;且对大模型应用开发非常感兴趣&#xff0c;我会快速跟你分享最新的感兴趣的 AI 应用和热点信息&#xff0c;也会不定期分享自己的想法和开源实例&#xff0c;欢迎关注我哦&#xff01; &#x1f966; 微信公众号&#xff…

双向链表及如何使用GLib的GList实现双向链表

双向链表是一种比单向链表更为灵活的数据结构&#xff0c;与单向链表相比可以有更多的应用场景&#xff0c;本文讨论双向链表的基本概念及实现方法&#xff0c;并着重介绍使用GLib的GList实现单向链表的方法及步骤&#xff0c;本文给出了多个实际范例源代码&#xff0c;旨在帮助…

C++笔试题之实现一个定时器

一.定时器&#xff08;timer&#xff09;的需求 1.执行定时任务的时&#xff0c;主线程不阻塞&#xff0c;所以timer必须至少持有一个线程用于执行定时任务 2.考虑到timer线程资源的合理利用&#xff0c;一个timer需要能够管理多个定时任务&#xff0c;所以timer要支持增删任务…

【Java笔记】1-JDK/JRE/JVM是个啥?

JDK、JRE、JVM可以说是入门必须了解的三个词汇 先说全称 JDK&#xff1a;Java Development Kit&#xff0c;Java开发工具包 JRE&#xff1a;Java Runtime Environment&#xff0c;Java运行环境 JVM&#xff1a;Java Virtual Machine&#xff0c;Java虚拟机 再说关系 JVM⊆J…

c语言-进位计数制

文章目录 一、进位计数制是什么&#xff1f;二、c语言1.二进制转十进制2.十进制转二进制 一、进位计数制是什么&#xff1f; 进位计数制简称进制&#xff0c;是人类用于计算数量的基本规则。 可使用数字符号的数目称为基数或底数&#xff0c;基数个数为n个&#xff0c;即可称n…

HTML 基础标签——结构化标签<html>、<head>、<body>

文章目录 1. <html> 标签2. <head> 标签3. <body> 标签4. <div> 标签5. <span> 标签小结 在 HTML 文档中&#xff0c;使用特定的结构标签可以有效地组织和管理网页内容。这些标签不仅有助于浏览器正确解析和渲染页面&#xff0c;还能提高网页的可…

【算法赌场】区间合并

区间问题 区间问题的引入 数学上&#xff0c;用两个数字可以确定数轴上的一个区间&#xff0c;较小的数字叫做区间的左端点&#xff0c;也叫区间起点&#xff0c;较大的数字叫做区间的右端点&#xff0c;也叫区间终点。 在算法竞赛中&#xff0c;很多题目是以区间为单位去进行…

给定开始日期时间结束日期时间、间隔得到符合条件的序列pandas.timedelta_range()

【小白从小学Python、C、Java】 【考研初试复试毕业设计】 【Python基础AI数据分析】 给定开始日期时间 结束日期时间、间隔 得到符合条件的序列 pandas.timedelta_range() [太阳]选择题 以下代码执行后&#xff0c;delta中包含的时间差序列的个数是多少&#xff1f; import pa…

【AI工作流】FastGPT - 深入解析FastGPT工作流编排:从基础到高级应用的全面指南

文章目录 一、工作流编排概述二、FastGPT的节点类型1. 基础功能插件(1) 文本输出(2) 功能调用(3) 工具(4) 外部调用(5) 其他 2. 系统插件3. 团队插件 三、工作流中的流向结语 在当今快速发展的人工智能领域&#xff0c;工作流编排的能力已成为提升用户体验和应用效率的关键因素…

qt QAction详解

1、概述 QAction是Qt框架中的一个抽象类&#xff0c;用于表示用户界面中的一个动作&#xff08;action&#xff09;。这些动作可以绑定到菜单项、工具栏按钮或快捷键上&#xff0c;提供了一种灵活的方式来处理用户交互。QAction不仅包含了动作的名称、图标、提示信息等属性&am…

MRCTF2020:你传你ma呢

文件上传题先判断黑白名单过滤&#xff0c;先传个最简单的木马 这里上传不了php文件&#xff0c;猜测可能是对php文件进行了过滤&#xff0c;将文件改为任意后缀这里改为.abc 还是上传不成功&#xff0c;猜测可能对MIME也做了过滤&#xff0c;将Content-Type更改为image/jpeg再…

LeetCode (206单链表反转)

目录 题目描述: 代码: 第一种: 第二种: 第三种: 第四种: 第五种: 主函数: ListNode类: 题目描述: 给你单链表的头节点 head &#xff0c;请你反转链表&#xff0c;并返回反转后的链表。 示例 1&#xff1a; 输入&#xff1a;head [1,2,3,4,5] 输出&#xff1a;[5,4,3…

C# Modbus RTU通讯回顾

涉及技术&#xff1a; 1.使用NMdbus4 库 2.ushort[]转int 记得之前刚学习的时候&#xff0c;是ushort[] → Hex字符串→byte[] → 翻转byte[] →BitConverter.ToInt32()&#xff0c;饶了一大圈&#xff1b;实际上可以直接转&#xff1b;这里也有小细节&#xff1a;使用BitCo…

RHCE6

一、DNS域名解析服务器 DNS &#xff08; Domain Name System &#xff09;是互联网上的一项服务&#xff0c;它作为将域名和 IP 地址相互映射的一个分布式数据库&#xff0c;能够使人更方便的访问互联网。DNS 系统使用的是网络的查询&#xff0c;那么自然需要有监听的 port 。…

uni-app 下拉刷新、 上拉触底(列表信息)、 上滑加载(短视频) 一键搞定

一、下拉刷新 1. 首先找到pages.json中 给需要进行下拉刷新的页面设置可以下拉刷新 2. 然后在需要实现下拉刷新的script标签内添加 导入onPullDownRefresh import {onPullDownRefresh} from dcloudio/uni-app 下拉刷新触发的事件 onPullDownRefresh(()> {console.log(正…

QML旋转选择器组件Tumbler

1. 介绍 Tumbler是一个用于创建旋转选择器的组件。它提供了一种直观的方式来让用户从一组选项中进行选择&#xff0c;类似于转盘式数字密码锁。网上找的类似网图如下&#xff1a; 在QML里&#xff0c;这种组件一共有两个版本&#xff0c;分别在QtQuick.Extras 1.4(旧)和QtQuic…

车载无人机用来做什么?车载无人机技术详解

车载无人机是将车和无人机组合到一起的产品&#xff0c;它有效地结合了无人机的灵活性和指挥车的远距离移动性&#xff0c;大大扩展了无人机的使用范围。以下是对车载无人机技术的详细解析&#xff1a; 一、车载无人机的应用 1. 应急现场指挥&#xff1a; 车载无人机可迅速抵…

HarmonyOS NEXT 应用开发实战(九、知乎日报项目详情页实现详细介绍)

在本篇博文中&#xff0c;我们将探讨如何使用 HarmonyOS Next 框架开发一个知乎日报的详情页&#xff0c;逐步介绍所用到的组件及代码实现。知乎日报是个小巧完整的小项目&#xff0c;这是一个循序渐进的过程&#xff0c;适合初学者和有一定开发经验的工程师参考。 1. 项目背景…