IO多路转接之select
IO多路转接之select
- 1. IO多路转接(复用)
 - 2. select
 - 2.1 函数原型
 - 2.2 细节描述
 
- 3. 并发处理
 - 3.1 处理流程
 - 3.2 通信代码
 
原文链接
1. IO多路转接(复用)
IO多路转接也称为IO多路复用,它是一种网络通信的手段(机制),通过这种方式可以同时监测多个文件描述符并且这个过程是阻塞的,一旦检测到有文件描述符就绪( 可以读数据或者可以写数据)程序的阻塞就会被解除,之后就可以基于这些(一个或多个)就绪的文件描述符进行通信了。通过这种方式在单线程/进程的场景下也可以在服务器端实现并发。常见的IO多路转接方式有:select、poll、epoll。
下面先对多线程/多进程并发和IO多路转接的并发处理流程进行对比(服务器端):
- 多线程/多进程并发 
- 主线程/父进程:调用 accept()监测客户端连接请求 
- 如果没有新的客户端的连接请求,当前线程/进程会阻塞
 - 如果有新的客户端连接请求解除阻塞,建立连接
 
 
 - 主线程/父进程:调用 accept()监测客户端连接请求 
 - 子线程/子进程:和建立连接的客户端通信 
- 调用 read() / recv() 接收客户端发送的通信数据,如果没有通信数据,当前线程/进程会阻塞,数据到达之后阻塞自动解除
 - 调用 write() / send() 给客户端发送数据,如果写缓冲区已满,当前线程/进程会阻塞,否则将待发送数据写入写缓冲区中
 
 - IO多路转接并发 
- 使用IO多路转接函数委托内核检测服务器端所有的文件描述符(通信和监听两类),这个检测过程会导致进程/线程的阻塞,如果检测到已就绪的文件描述符阻塞解除,并将这些已就绪的文件描述符传出
 - 根据类型对传出的所有已就绪文件描述符进行判断,并做出不同的处理 
- 监听的文件描述符:和客户端建立连接 
- 此时调用accept()是不会导致程序阻塞的,因为监听的文件描述符是已就绪的(有新请求)
 
 - 通信的文件描述符:调用通信函数和已建立连接的客户端通信 
- 调用 read() / recv() 不会阻塞程序,因为通信的文件描述符是就绪的,读缓冲区内已有数据
 - 调用 write() / send() 不会阻塞程序,因为通信的文件描述符是就绪的,写缓冲区不满,可以往里面写数据
 
 
 - 监听的文件描述符:和客户端建立连接 
 - 对这些文件描述符继续进行下一轮的检测(循环往复。。。)
 
 
与多进程和多线程技术相比,I/O多路复用技术的最大优势是系统开销小,系统不必创建进程/线程,也不必维护这些进程/线程,从而大大减小了系统的开销。
2. select
2.1 函数原型
使用select这种IO多路转接方式需要调用一个同名函数select,这个函数是跨平台的,Linux、Mac、Windows都是支持的。程序猿通过调用这个函数可以委托内核帮助我们检测若干个文件描述符的状态,其实就是检测这些文件描述符对应的读写缓冲区的状态:
- 读缓冲区:检测里边有没有数据,如果有数据该缓冲区对应的文件描述符就绪
 - 写缓冲区:检测写缓冲区是否可以写(有没有容量),如果有容量可以写,缓冲区对应的文件描述符就绪
 - 读写异常:检测读写缓冲区是否有异常,如果有该缓冲区对应的文件描述符就绪
 
委托检测的文件描述符被遍历检测完毕之后,已就绪的这些满足条件的文件描述符会通过select()的参数分3个集合传出,程序猿得到这几个集合之后就可以分情况依次处理了。
下面来看一下这个函数的函数原型:
#include <sys/select.h>
struct timeval {time_t      tv_sec;         /* seconds */suseconds_t tv_usec;        /* microseconds */
};int select(int nfds, fd_set *readfds, fd_set *writefds,fd_set *exceptfds, struct timeval * timeout);
 
- 函数参数: 
- nfds:委托内核检测的这三个集合中最大的文件描述符 + 1 
- 内核需要线性遍历这些集合中的文件描述符,这个值是循环结束的条件
 - 在Window中这个参数是无效的,指定为-1即可
 
 - readfds:文件描述符的集合, 内核只检测这个集合中文件描述符对应的读缓冲区 
- 传入传出参数,读集合一般情况下都是需要检测的,这样才知道通过哪个文件描述符接收数据
 
 - writefds:文件描述符的集合, 内核只检测这个集合中文件描述符对应的写缓冲区 
- 传入传出参数,如果不需要使用这个参数可以指定为NULL
 
 - exceptfds:文件描述符的集合, 内核检测集合中文件描述符是否有异常状态 
- 传入传出参数,如果不需要使用这个参数可以指定为NULL
 
 - timeout:超时时长,用来强制解除select()函数的阻塞的 
- NULL:函数检测不到就绪的文件描述符会一直阻塞。
 - 等待固定时长(秒):函数检测不到就绪的文件描述符,在指定时长之后强制解除阻塞,函数返回0
 - 不等待:函数不会阻塞,直接将该参数对应的结构体初始化为0即可。
 
 
 - nfds:委托内核检测的这三个集合中最大的文件描述符 + 1 
 - 函数返回值: 
- 大于0:成功,返回集合中已就绪的文件描述符的总个数
 - 等于-1:函数调用失败
 - 等于0:超时,没有检测到就绪的文件描述符
 
 
另外初始化fd_set类型的参数还需要使用相关的一些列操作函数,具体如下:
// 将文件描述符fd从set集合中删除 == 将fd对应的标志位设置为0        
void FD_CLR(int fd, fd_set *set);
// 判断文件描述符fd是否在set集合中 == 读一下fd对应的标志位到底是0还是1
int  FD_ISSET(int fd, fd_set *set);
// 将文件描述符fd添加到set集合中 == 将fd对应的标志位设置为1
void FD_SET(int fd, fd_set *set);
// 将set集合中, 所有文件文件描述符对应的标志位设置为0, 集合中没有添加任何文件描述符
void FD_ZERO(fd_set *set);
 
2.2 细节描述
在select()函数中第2、3、4个参数都是fd_set类型,它表示一个文件描述符的集合,类似于信号集 sigset_t,这个类型的数据有128个字节,也就是1024个标志位,和内核中文件描述符表中的文件描述符个数是一样的。
sizeof(fd_set) = 128 字节 * 8 = 1024 bit      // int [32]
 
这并不是巧合,而是故意为之。这块内存中的每一个bit 和 文件描述符表中的每一个文件描述符是一一对应的关系,这样就可以使用最小的存储空间将要表达的意思描述出来了。
下图中的fd_set中存储了要委托内核检测读缓冲区的文件描述符集合。
- 如果集合中的标志位为0代表不检测这个文件描述符状态
 - 如果集合中的标志位为1代表检测这个文件描述符状态
 

内核在遍历这个读集合的过程中,如果被检测的文件描述符对应的读缓冲区中没有数据,内核将修改这个文件描述符在读集合fd_set中对应的标志位,改为0,如果有数据那么这个标志位的值不变,还是1。
 
当select()函数解除阻塞之后,被内核修改过的读集合通过参数传出,此时集合中只要标志位的值为1,那么它对应的文件描述符肯定是就绪的,我们就可以基于这个文件描述符和客户端建立新连接或者通信了。
3. 并发处理
3.1 处理流程
如果在服务器基于select实现并发,其处理流程如下:
- 创建监听的套接字 lfd = socket();
 - 将监听的套接字和本地的IP和端口绑定 bind()
 - 给监听的套接字设置监听 listen()
 - 创建一个文件描述符集合 fd_set,用于存储需要检测读事件的所有的文件描述符 
- 通过 FD_ZERO() 初始化
 - 通过 FD_SET() 将监听的文件描述符放入检测的读集合中
 
 - 循环调用select(),周期性的对所有的文件描述符进行检测
 - select() 解除阻塞返回,得到内核传出的满足条件的就绪的文件描述符集合 
- 通过FD_ISSET() 判断集合中的标志位是否为 1 
- 如果这个文件描述符是监听的文件描述符,调用 accept() 和客户端建立连接 
- 将得到的新的通信的文件描述符,通过FD_SET() 放入到检测集合中
 
 - 如果这个文件描述符是通信的文件描述符,调用通信函数和客户端通信 
- 如果客户端和服务器断开了连接,使用FD_CLR()将这个文件描述符从检测集合中删除
 - 如果没有断开连接,正常通信即可
 
 
 - 如果这个文件描述符是监听的文件描述符,调用 accept() 和客户端建立连接 
 
 - 通过FD_ISSET() 判断集合中的标志位是否为 1 
 - 重复第6步

 
3.2 通信代码
select_server.cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>int main()
{// 1. 创建监听的fdint lfd = socket(AF_INET, SOCK_STREAM, 0);// 2. 绑定struct sockaddr_in addr;addr.sin_family = AF_INET;addr.sin_port = htons(9999);addr.sin_addr.s_addr = INADDR_ANY;bind(lfd, (struct sockaddr*)&addr, sizeof(addr));// 3. 设置监听listen(lfd, 128);// 将监听的fd的状态检测委托给内核检测int maxfd = lfd;// 初始化检测的读集合fd_set rdset;fd_set rdtemp;// 清零FD_ZERO(&rdset);// 将监听的lfd设置到检测的读集合中FD_SET(lfd, &rdset);// 通过select委托内核检测读集合中的文件描述符状态, 检测read缓冲区有没有数据// 如果有数据, select解除阻塞返回// 应该让内核持续检测while(1){// 默认阻塞// rdset 中是委托内核检测的所有的文件描述符rdtemp = rdset;// 这里timeout参数一定要设置为结构体,要么就设为NULL,NULL就是默认阻塞struct timeval timeout;timeout.tv_sec = 0;timeout.tv_usec = 0;int num = select(maxfd+1, &rdtemp, NULL, NULL, &timeout);// rdset中的数据被内核改写了, 只保留了发生变化的文件描述的标志位上的1, 没变化的改为0// 只要rdset中的fd对应的标志位为1 -> 缓冲区有数据了// 判断有无新连接// 有新连接if(FD_ISSET(lfd, &rdtemp)){// 接受连接请求, 这个调用不阻塞struct sockaddr_in cliaddr;int cliLen = sizeof(cliaddr);int cfd = accept(lfd, (struct sockaddr*)&cliaddr, (socklen_t*)&cliLen);printf("%d\n",cfd);// 得到了有效的文件描述符// 通信的文件描述符添加到读集合// 在下一轮select检测的时候, 就能得到缓冲区的状态FD_SET(cfd, &rdset);// 重置最大的文件描述符maxfd = cfd > maxfd ? cfd : maxfd;}// 没有新连接, 通信for(int i=0; i<maxfd+1; ++i){// 判断从监听的文件描述符之后到maxfd这个范围内的文件描述符是否读缓冲区有数据,i != lfd 是因为 lfd是监听的socketif(i != lfd && FD_ISSET(i, &rdtemp)){// 接收数据char buf[10] = {0};// 一次只能接收10个字节, 客户端一次发送100个字节// 一次是接收不完的, 文件描述符对应的读缓冲区中还有数据// 下一轮select检测的时候, 内核还会标记这个文件描述符缓冲区有数据 -> 再读一次// 	循环会一直持续, 直到缓冲区数据被读完为止int len = read(i, buf, sizeof(buf));if(len == 0){printf("客户端关闭了连接...\n");// 将检测的文件描述符从读集合中删除FD_CLR(i, &rdset);close(i);}else if(len > 0){// 收到了数据// 发送数据write(i, buf, strlen(buf)+1);printf("读取了数据回传给客户端\n");}else{// 异常perror("read");}}}}return 0;
} 
select_client.cpp
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <arpa/inet.h>int main()
{// 1. 创建用于通信的套接字int fd = socket(AF_INET, SOCK_STREAM, 0);if(fd == -1){perror("socket");exit(0);}// 2. 连接服务器struct sockaddr_in addr;addr.sin_family = AF_INET;     // ipv4addr.sin_port = htons(9999);   // 服务器监听的端口, 字节序应该是网络字节序inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr.s_addr);int ret = connect(fd, (struct sockaddr*)&addr, sizeof(addr));if(ret == -1){perror("connect");exit(0);}// 通信while(1){// 读数据char recvBuf[1024];// 写数据// sprintf(recvBuf, "data: %d\n", i++);fgets(recvBuf, sizeof(recvBuf), stdin);write(fd, recvBuf, strlen(recvBuf)+1);// 如果客户端没有发送数据, 默认阻塞read(fd, recvBuf, sizeof(recvBuf));printf("recv buf: %s\n", recvBuf);sleep(1);}// 释放资源close(fd); return 0;
} 
客户端不需要使用IO多路转接进行处理,因为客户端和服务器的对应关系是 1:N,也就是说客户端是比较专一的,只能和一个连接成功的服务器通信。
虽然使用select这种IO多路转接技术可以降低系统开销,提高程序效率,但是它也有局限性:
- 待检测集合(第2、3、4个参数)需要频繁的在用户区和内核区之间进行数据的拷贝,效率低
 - 内核对于select传递进来的待检测集合的检测方式是线性的 
- 如果集合内待检测的文件描述符很多,检测效率会比较低
 - 如果集合内待检测的文件描述符相对较少,检测效率会比较高
 
 - 使用select能够检测的最大文件描述符个数有上限,默认是1024,这是在内核中被写死了的。
 
自己的一点理解:
select里面的timeout参数一定要设置为结构体,要么就设为NULL,NULL就是默认阻塞
调试可以发现服务端怎么工作的,select循环,只有当有空闲文件描述符的时候,分逻辑,到底是连接还是通信的。如果是通信的,会记录是否读完了数据,如果读完了,内核会将文件描述符重新标记为0,除非重新写入数据。
但是上面的客户端代码有个问题,就是服务端多次写入的时候,客户端只能收到前面10个字节的数据,但是如果循环调用read,客户端read会阻塞,因为这里服务端是不会主动发送数据的,只是把客户端传过来的数据重新传回去。想一下怎么解决这个问题?
相关文章:
IO多路转接之select
IO多路转接之select 1. IO多路转接(复用)2. select2.1 函数原型2.2 细节描述 3. 并发处理3.1 处理流程3.2 通信代码 原文链接 1. IO多路转接(复用) IO多路转接也称为IO多路复用,它是一种网络通信的手段(机…...
linux如何删除大文件的第一行(sed)
可以用sed命令实现: 删除文档的第一行 1. sed -i 1d <file>删除文档的最后一行 1. sed -i $d <file>在文档指定行中增加一行 # 示例如下: echo "1"; echo "2"; echo "4"; echo "5"; # 想要在echo…...
Navicat 技术指引 | 适用于 GaussDB 分布式的备份/还原功能
Navicat Premium(16.3.3 Windows 版或以上)正式支持 GaussDB 分布式数据库。GaussDB 分布式模式更适合对系统可用性和数据处理能力要求较高的场景。Navicat 工具不仅提供可视化数据查看和编辑功能,还提供强大的高阶功能(如模型、结…...
【广州华锐互动VRAR】VR戒毒科普宣传系统有效提高戒毒成功率
随着科技的不断发展,虚拟现实(VR)技术已经逐渐渗透到各个领域,为人们的生活带来了前所未有的便利。在教育科普领域,VR技术的应用也日益广泛,本文将详细介绍广州华锐互动开发的VR戒毒科普宣传系统࿰…...
守护安全,六氟化硫气体泄漏报警装置校准服务
在电力工业中,六氟化硫(SF6)气体是一种重要的介质,它用作封闭式中、高压开关的灭弧和绝缘气体。六氟化硫气体的卓越性能实现了装置经济化、低维护化的操作。与普通装置相比,可以节省最多90%的空间。 六氟化…...
概率测度理论方法(第 2 部分)
一、说明 欢迎回到这个三部曲的第二部分!在第一部分中,我们为测度论概率奠定了基础。我们探索了测量和可测量空间的概念,并使用这些概念定义了概率空间。在本文中,我们使用测度论来理解随机变量。 作为一个小回顾,在第…...
实战:Docker Compose 下 Nginx、Java、Mysql 和 Redis 服务协同部署(包含解决浏览器访问Linux部署服务器本地资源问题)
1. 背景 在该实战中,我们将探讨如何使用Docker Compose协同部署Nginx、Java、Mysql和Redis服务,实现一个视频上传与展示的应用。具体需求如下: Java应用负责上传视频和图片资源到Nginx目录下,作为资源服务器。Nginx服务作为静态…...
Docker 设置国内镜像源
Docker 设置国内镜像源 您可以通过修改daemon配置文件/etc/docker/daemon.json来使用加速器 具体配置如下: {"registry-mirrors" : ["https://registry.docker-cn.com","http://hub-mirror.c.163.com","https://docker.mirro…...
通信协议 远程调用RPC
1.通讯协议 所有的HDFS通讯协议都是建立在TCP/IP协议之上。 客户端通过一个可配置的TCP端口连接到Namenode,通过ClientProtocol协议与Namenode交 互。而Datanode使用DatanodeProtocol协议与Namenode交互。 一个远程过程调用(RPC)模型被抽象出来封装ClientProtoc…...
决策树 算法原理
决策树 算法原理 决策树的原理 决策树: 从训练数据中学习得出一个树状结构的模型 决策树属于判别模型 决策树是一种树状结构,通过做出一系列决策 (选择) 来对数据进行划分,这类似于针对一系列问题进行选择。 决策树的决策过程就是从根节点开始&#…...
Git全局设置命令---设置提交人邮箱
介绍 使用git命令设置提交人邮箱。 命令 git config --global user.email "xxxxxxxx.com"...
3DCAT+上汽奥迪:打造新零售汽车配置器实时云渲染解决方案
在 5G、云计算等技术飞速发展的加持下,云渲染技术迎来了突飞猛进的发展。在这样的背景下,3DCAT应运而生,成为了业内知名的实时云渲染服务商之一。 交互式3D实时云看车作为云渲染技术的一种使用场景,也逐步成为一种新的看车方式&a…...
物联网+AI智慧工地云平台源码(SaaS模式)
智慧工地云平台充分运用数字化技术,聚焦施工现场岗位一线,依托物联网、互联网、AI等技术,围绕施工现场管理的人、机、料、法、环五大维度,以及施工过程管理的进度、质量、安全三大体系为基础应用,实现全面高效的工程管…...
python打开相机,用鼠标左键框选矩形区域,支持一次框选多个矩形区域,通过鼠标右标清除上一次画的矩形。
方案一 import cv2# Global variables rectangles [] current_rectangle [] drawing False# Mouse callback function def mouse_callback(event, x, y, flags, param):global rectangles, current_rectangle, drawingif event cv2.EVENT_LBUTTONDOWN:drawing Truecurren…...
卷积之后通道数为什么变了
通道数增多与卷积之后得到的图像特征数量有关 卷积层的作用本来就是把输入中的特征分离出来变成新的 feature map,每一个输出通道就是一个卷积操作提取出来的一种特征。在此过程中ReLU激活起到过滤的作用,把负相关的特征点去掉,把正相关的留…...
ubuntu18.04安装opencv-4.5.5+opencv_contrib-4.5.5
一、安装opencv依赖 sudo apt-get install build-essential sudo apt-get install cmake git libgtk2.0-dev pkg-config libavcodec-dev libavformat-dev libswscale-dev sudo apt-get install python-dev python-numpy libtbb2 libtbb-dev libjpeg-dev libpng-dev libtiff-d…...
实验3.5 路由器的单臂路由配置
实验3.5 路由器的单臂路由配置 一、任务描述二、任务分析三、具体要求四、实验拓扑五、任务实施1.SWA的基本配置2.RA的基本配置3.在RA上查看接口状态 六、任务验收七、任务小结 一、任务描述 某公司对部门划分了需VLAN之后,发现两个部门之间无法通信,但…...
nodejs微信小程序+python+PHP的基于大数据的家电销售分析系统设计与实现-计算机毕业设计推荐django
目 录 摘 要 I ABSTRACT II 目 录 II 第1章 绪论 1 1.1背景及意义 1 1.2 国内外研究概况 1 1.3 研究的内容 1 第2章 相关技术 3 2.1 nodejs简介 4 2.2 express框架介绍 6 2.4 MySQL数据库 4 第3章 系统分析 5 3.1 需求分析 5 3.2 系统可行性分析 5 3.2.1技术可行性:…...
Windows server flask
1、Windows server 通过python的flask执行命令 from flask import Flask, request, abort import subprocess from flask_basicauth import BasicAuth app Flask(__name__) # 获取url是进行账号密码认证,设置url的账号密码 app.config[BASIC_AUTH_USERNAME] 账号…...
maven工程的pom.xml文件中增加了依赖,但偶尔没有下载到本地仓库
maven工程pom.xml文件中的个别依赖没有下载到本地maven仓库。以前没有遇到这种情况,今天就遇到了这个问题,把解决过程记录下来。 我在eclipse中编辑maven工程的pom.xml文件,增加对mybatis的依赖,但保存文件后,依赖的j…...
K8S认证|CKS题库+答案| 11. AppArmor
目录 11. AppArmor 免费获取并激活 CKA_v1.31_模拟系统 题目 开始操作: 1)、切换集群 2)、切换节点 3)、切换到 apparmor 的目录 4)、执行 apparmor 策略模块 5)、修改 pod 文件 6)、…...
FFmpeg 低延迟同屏方案
引言 在实时互动需求激增的当下,无论是在线教育中的师生同屏演示、远程办公的屏幕共享协作,还是游戏直播的画面实时传输,低延迟同屏已成为保障用户体验的核心指标。FFmpeg 作为一款功能强大的多媒体框架,凭借其灵活的编解码、数据…...
8k长序列建模,蛋白质语言模型Prot42仅利用目标蛋白序列即可生成高亲和力结合剂
蛋白质结合剂(如抗体、抑制肽)在疾病诊断、成像分析及靶向药物递送等关键场景中发挥着不可替代的作用。传统上,高特异性蛋白质结合剂的开发高度依赖噬菌体展示、定向进化等实验技术,但这类方法普遍面临资源消耗巨大、研发周期冗长…...
C# SqlSugar:依赖注入与仓储模式实践
C# SqlSugar:依赖注入与仓储模式实践 在 C# 的应用开发中,数据库操作是必不可少的环节。为了让数据访问层更加简洁、高效且易于维护,许多开发者会选择成熟的 ORM(对象关系映射)框架,SqlSugar 就是其中备受…...
【数据分析】R版IntelliGenes用于生物标志物发现的可解释机器学习
禁止商业或二改转载,仅供自学使用,侵权必究,如需截取部分内容请后台联系作者! 文章目录 介绍流程步骤1. 输入数据2. 特征选择3. 模型训练4. I-Genes 评分计算5. 输出结果 IntelliGenesR 安装包1. 特征选择2. 模型训练和评估3. I-Genes 评分计…...
[免费]微信小程序问卷调查系统(SpringBoot后端+Vue管理端)【论文+源码+SQL脚本】
大家好,我是java1234_小锋老师,看到一个不错的微信小程序问卷调查系统(SpringBoot后端Vue管理端)【论文源码SQL脚本】,分享下哈。 项目视频演示 【免费】微信小程序问卷调查系统(SpringBoot后端Vue管理端) Java毕业设计_哔哩哔哩_bilibili 项…...
GO协程(Goroutine)问题总结
在使用Go语言来编写代码时,遇到的一些问题总结一下 [参考文档]:https://www.topgoer.com/%E5%B9%B6%E5%8F%91%E7%BC%96%E7%A8%8B/goroutine.html 1. main()函数默认的Goroutine 场景再现: 今天在看到这个教程的时候,在自己的电…...
day36-多路IO复用
一、基本概念 (服务器多客户端模型) 定义:单线程或单进程同时监测若干个文件描述符是否可以执行IO操作的能力 作用:应用程序通常需要处理来自多条事件流中的事件,比如我现在用的电脑,需要同时处理键盘鼠标…...
windows系统MySQL安装文档
概览:本文讨论了MySQL的安装、使用过程中涉及的解压、配置、初始化、注册服务、启动、修改密码、登录、退出以及卸载等相关内容,为学习者提供全面的操作指导。关键要点包括: 解压 :下载完成后解压压缩包,得到MySQL 8.…...
go 里面的指针
指针 在 Go 中,指针(pointer)是一个变量的内存地址,就像 C 语言那样: a : 10 p : &a // p 是一个指向 a 的指针 fmt.Println(*p) // 输出 10,通过指针解引用• &a 表示获取变量 a 的地址 p 表示…...
