【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个参数,都是用来干什么的呢?
int nfds:输入性参数 ,表示等待的多个文件描述符最大值 加 1。比如等待1 2 5 6 99这几个文件描述符,那么就要传入100。注意不是文件描述符的个数!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等待连接
我们首先把之前的套接字基础的类拷贝过来:
class Socket:实现套接字的创建工作,并进入监听模式。class InetAddr:网络套接字基本信息类,用于进行网络套接字传参工作。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 ;}}}
但是有几个问题:
- 在上面的handler函数中,我们已经获取到了连接,那么下面敢不敢直接进行读取呢?
当然不能,因为建立连接并不代表会有请求传过来!所以还需要等待请求! - 那么怎么知道底层有没有就绪呢?
还是通过select进行等待,想办法将新的fd添加给select,进行统一管理! - 那么这样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的一些优缺点:
- 每次调用 select,都需要手动设置 fd 集合, 从接口使用角度来说也非常不便.
- 每次调用 select, 都需要把 fd 集合从用户态拷贝到内核态, 这个开销在 fd 很多时会很大。这个是多路转接IO无法避免的问题!
- 同时每次调用 select 都需要在内核遍历传递进来的所有 fd,这个开销在 fd 很多时很大。
- select 支持的文件描述符数量太小!虽然操作系统中文件描述符也有限制,但是这是操作系统的缺陷。同样select也是缺点
这里不断的要进行循环遍历数组,造成的性能开销是比较大的!所以就有了其他两种多路转接方案:poll与epoll
相关文章:
【Linux】从零开始使用多路转接IO --- select
碌碌无为,则余生太长; 欲有所为,则人生苦短。 --- 中岛敦 《山月记》--- 从零开始认识五种IO模型 1 前言2 认识多路转接select3 多路转接select等待连接4 完善代码5 总结 1 前言 上一篇文章我们讲解了五种IO模型的基本概念,并…...
ArcGIS Pro SDK (二十一)渲染
ArcGIS Pro SDK (二十一)渲染 文章目录 ArcGIS Pro SDK (二十一)渲染1 定义唯一值呈现器定义2 为最新观测值设置唯一值渲染器3 为先前的观测值设置唯一值渲染器4 设置简单的渲染器以绘制轨迹线5 检查先前的观测值和轨道线可见性6 使轨迹线和先前的观测点可见7 检索当前观测…...
FPGA在物联网边缘计算中的应用!!!
FPGA(现场可编程门阵列)在物联网边缘计算中的应用正变得越来越重要。边缘计算是一种分布式计算架构,它将数据的处理分散到网络的边缘,靠近数据源,而不是集中在数据中心处理。以下是FPGA在物联网边缘计算中的几个关键应…...
【解决】Linux环境中mysqlclient安装失败问题
问题描述 在Linux系统下安装myslclient报异常。系统为Centos 8 使用 pip install mysqlclient 报出下面的异常 error: subprocess-exited-with-error Getting requirements to build wheel did not run successfully.│ exit code: 1╰─> [30 lines of output]/bin/sh: pkg…...
✨ Midjourney中文版:创意启航,绘梦无界 ✨
Midjourney AI超强绘画 (原生态系统)用户端:Ai Loadinghttps://www.mjdiscord.com 项目详细介绍飞书文档:Docshttps://ivqklkndl4k.feishu.cn/docx/GRnMdCbcooWkwTx1RU4cZjGVnzb?fromfrom_copylk 🌐 无缝体验,中文定制…...
软件(1)
软件 常考软件 图像软件 Flash 一款二维动画处理软件 photoshop 图像处理界的“巨无霸” ACDSee ACDSee是常用的图片管理编辑软件,尽管也可以支持WAV格式的音频播放, 但目前主要是作为看图软件 音频软件 Winamp Winamp是数字媒体播放的先驱Audition Audi…...
linux perf 环境部署和基本测试(基于Ubuntu20.04)
1,linux 安装perf sudo apt-ge install linux-tools-common sudo apt-get install linux-tools-$(uname -r) linux-tools-generic -y 2 补充安装 sudo apt-get install python3-q-text-as-data 3,perf常用命令 larkubuntu:~$ perf usage: perf [--version] [--hel…...
【网络面试篇】HTTP(1)(笔记)——状态码、字段、GET、POST、缓存
目录 一、相关问题 1. HTTP请求常见的状态码和字段? (1)状态码 (2)字段 ① Host 字段 ② Content-length 字段 ③ Connection 字段 ④ Content-Type 字段 ⑤ Content-Encoding 字段 2. GET 和 POST 的区别&a…...
HTML 基础标签——分组标签 <div>、<span> 和基础语义容器
文章目录 1. `<div>` 标签特点用途示例2. `<span>` 标签特点用途示例3. `<fieldset>` 标签特点用途示例4. `<section>` 标签特点用途示例5. `<article>` 标签特点用途示例总结HTML中的分组(容器)标签用于结构化内容,将页面元素组织成逻辑区域…...
SS928V100 ISP常见问题列表
下载链接: https://download.csdn.net/download/quantum7/89948226 1 FAQ 1 1.1 ISP 1 1.1.1 如何解决整体锐度不足 1 1.1.2 如何解决图像发蒙问题,提高通透性 2 1.1.3 如何解决低照度清晰度差 2 1.1.4 如何解决图像清晰度与物体边缘白边和黑边问题…...
AI写诗:自动版大唐宫体诗
大唐学子,手拿一本小卷(类书),从中挑选过去他们(权威)认为好的词来拼接一首诗,此类诗词称作“宫体诗”,在初唐时期甚是流行。 写“宫体诗”的过程有木有那么一丝丝的熟悉?…...
Java复习31(PTA)
sdust-Java-字符串集合求并集 分数 15 全屏浏览 切换布局 作者 张峰 单位 山东科技大学 从键盘接收N个英文字符串(其中不同的字符串数量大于10),从头开始取5个不同的字符串放入一个集合S1,然后接着取5个不同的字符串放入另一个…...
【Linux系列】Linux 系统中的软连接管理
💝💝💝欢迎来到我的博客,很高兴能够在这里和您见面!希望您在这里可以感受到一份轻松愉快的氛围,不仅可以获得有趣的内容和知识,也可以畅所欲言、分享您的想法和见解。 推荐:kwan 的首页,持续学…...
@layer(级联层)
在css样式表(文件)中声明layer为样式添加级联层,其意义在于可以使用它重新定义样式的叠层关系. layer后声明的级联层里面的样式将覆盖前声明的级联层里面的相同属性.在级联层外声明的样式会覆盖级联层里面的相同属性样式,同一层级里面的样式冲突,依然按照优先级来计算. 在级联层…...
nginx代理websocket服务
一、nginx代理websocket服务 一)nginx代理ws服务 在nginx中,可以通过proxy_pass指令来代理WebSocket服务。 以下是一个示例配置: map $http_upgrade $connection_upgrade {default upgrade; close; }upstream ws_backend {server 127.0.0.1:…...
第二十七章 Vue异步更新之$nextTick
目录 一、概述 二、完整代码 2.1. main.js 2.2. App.vue 一、概述 需求:编辑标题, 弹出显示编辑框自动聚焦 1. 点击编辑,显示编辑框 2. 让编辑框,立刻获取焦点 我们常规的思路可能会编写如下代码来实现: 问题:…...
【51 Pandas+Pyecharts | 深圳市共享单车数据分析可视化】
文章目录 🏳️🌈 1. 导入模块🏳️🌈 2. Pandas数据处理2.1 读取数据2.2 查看数据信息2.3 处理起始时间、结束时间2.4 增加骑行时长区间列2.5 增加骑行里程区间列 🏳️🌈 3. Pyecharts数据可视化3.1 各…...
【Clikhouse 探秘】ClickHouse 物化视图:加速大数据分析的新利器
👉博主介绍: 博主从事应用安全和大数据领域,有8年研发经验,5年面试官经验,Java技术专家,WEB架构师,阿里云专家博主,华为云云享专家,51CTO 专家博主 ⛪️ 个人社区&#x…...
线程相关题(线程池、线程使用、核心线程数的设置)
目录 线程安全的什么情况用?情况下用线程安全的类? 线程池线程方面的优化 线程池调优主要涉及以下几个关键参数。 线程不安全原因? 1. 共享资源访问冲突 2. 缺乏原子操作 3. 内存可见性问题 4. 重排序问题 如何解决线程不安全的问题࿱…...
2181、合并零之间的节点
2181、[中等] 合并零之间的节点 1、问题描述: 给你一个链表的头节点 head ,该链表包含由 0 分隔开的一连串整数。链表的 开端 和 末尾 的节点都满足 Node.val 0 。 对于每两个相邻的 0 ,请你将它们之间的所有节点合并成一个节点ÿ…...
XCTF-web-easyupload
试了试php,php7,pht,phtml等,都没有用 尝试.user.ini 抓包修改将.user.ini修改为jpg图片 在上传一个123.jpg 用蚁剑连接,得到flag...
装饰模式(Decorator Pattern)重构java邮件发奖系统实战
前言 现在我们有个如下的需求,设计一个邮件发奖的小系统, 需求 1.数据验证 → 2. 敏感信息加密 → 3. 日志记录 → 4. 实际发送邮件 装饰器模式(Decorator Pattern)允许向一个现有的对象添加新的功能,同时又不改变其…...
进程地址空间(比特课总结)
一、进程地址空间 1. 环境变量 1 )⽤户级环境变量与系统级环境变量 全局属性:环境变量具有全局属性,会被⼦进程继承。例如当bash启动⼦进程时,环 境变量会⾃动传递给⼦进程。 本地变量限制:本地变量只在当前进程(ba…...
linux arm系统烧录
1、打开瑞芯微程序 2、按住linux arm 的 recover按键 插入电源 3、当瑞芯微检测到有设备 4、松开recover按键 5、选择升级固件 6、点击固件选择本地刷机的linux arm 镜像 7、点击升级 (忘了有没有这步了 估计有) 刷机程序 和 镜像 就不提供了。要刷的时…...
WordPress插件:AI多语言写作与智能配图、免费AI模型、SEO文章生成
厌倦手动写WordPress文章?AI自动生成,效率提升10倍! 支持多语言、自动配图、定时发布,让内容创作更轻松! AI内容生成 → 不想每天写文章?AI一键生成高质量内容!多语言支持 → 跨境电商必备&am…...
C++ 求圆面积的程序(Program to find area of a circle)
给定半径r,求圆的面积。圆的面积应精确到小数点后5位。 例子: 输入:r 5 输出:78.53982 解释:由于面积 PI * r * r 3.14159265358979323846 * 5 * 5 78.53982,因为我们只保留小数点后 5 位数字。 输…...
JDK 17 新特性
#JDK 17 新特性 /**************** 文本块 *****************/ python/scala中早就支持,不稀奇 String json “”" { “name”: “Java”, “version”: 17 } “”"; /**************** Switch 语句 -> 表达式 *****************/ 挺好的ÿ…...
vue3+vite项目中使用.env文件环境变量方法
vue3vite项目中使用.env文件环境变量方法 .env文件作用命名规则常用的配置项示例使用方法注意事项在vite.config.js文件中读取环境变量方法 .env文件作用 .env 文件用于定义环境变量,这些变量可以在项目中通过 import.meta.env 进行访问。Vite 会自动加载这些环境变…...
零基础在实践中学习网络安全-皮卡丘靶场(第九期-Unsafe Fileupload模块)(yakit方式)
本期内容并不是很难,相信大家会学的很愉快,当然对于有后端基础的朋友来说,本期内容更加容易了解,当然没有基础的也别担心,本期内容会详细解释有关内容 本期用到的软件:yakit(因为经过之前好多期…...
高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数
高效线程安全的单例模式:Python 中的懒加载与自定义初始化参数 在软件开发中,单例模式(Singleton Pattern)是一种常见的设计模式,确保一个类仅有一个实例,并提供一个全局访问点。在多线程环境下,实现单例模式时需要注意线程安全问题,以防止多个线程同时创建实例,导致…...
