【Linux网络编程】高效I/O--select/poll服务器

devtools/2025/2/27 22:40:51/

目录

多路转接之select

select服务器实现

获取连接

handlerEvent

select服务器代码链接 

select的优缺点 

多路转接之poll 

poll服务器实现(select服务器改写) 

poll的优缺点


多路转接之select

select的作用

I/O的本质 = 等 + 拷贝

多路转接就是通过同时等待多个文件描述符的方式实现效率的提升,这些在五种I/O模型中都详细说过!

select是实现多路复用I/O的一种方式

一个多路复用I/O可以分为两步:

  • 同时等待多个文件描述符
  • I/O条件就绪,直接进行拷贝

select完成的工作就是I/O等待这一步,即对多个文件描述符进行等待,若有一个或多个文件描述符读写条件就绪,select就会返回!

select返回后,即进行多路复用的第二步拷贝。拷贝是由read/write...这些拷贝函数实现的!


select调用

int select(int nfds, fd_set *readfds,fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
  • nfds:表示你需要监管的所有的文件描述符中,最大的文件描述符+1
  • readfds:指向fd_set的指针,包含需要监视的可读文件描述符
  • writefds:指向fd_set的指针,包含需要监视的可写文件描述符(可选)。
  • exceptfds:指向fd_set的指针,包含需要监视的异常文件描述符(可选)。
  • timeout:指定select进行等待时的等待规则
  • 返回值:若大于0,则表示实际I/O条件就绪的文件描述符的个数。若等于0,则表示超时。若小于0,则表示select发生错误,错误码被设置
  • 头文件:sys/select.h

以下是对select参数的详细介绍 


timeout参数 

 struct timeval类型的结构:

struct timeval {  long tv_sec;   // 秒数  long tv_usec;  // 微秒数  
};
  • 若tv_sec设置为0,且tv_usec设置为0,则表示的是select进行非阻塞等待。即便没有任何一个文件描述符就绪,select依旧会返回0
  • 若timeout参数设置为NULL,则表示select进行阻塞等待,在没有任何外部干扰(如信号中断)的情况下,若没有任何一个文件描述符就绪,则select永不返回,直到有一个或多个文件描述符就绪为止
  • 若timeout中的tv_sec或tv_usec设置为>0的数,此时的timeout是一个输入和输出型参数。例如timeout设置为5秒0微秒之后传入select,过了两秒以后有一个文件描述符就绪了,那么select返回,timeout被修改为3秒0微秒

fd_set类型 

读写条件就绪:

  • 若一个文件描述符可以进行读了,即该文件的内核接收缓冲区有数据了,我们称该文件描述符的读条件就绪
  • 若一个文件描述符可以进行写了,即该文件的内核发送缓冲区未满,我们称该文件描述符的写条件就绪

fd_set内部是一个对应文件描述符的位图结构

select参数中的fd_set类型参数含义:

  • readfds:该文件描述符集中比特位的值为1的文件描述符,select只关心该文件描述符的读条件是否就绪
  • writefds:该文件描述符集中比特位的值为1的文件描述符,select只关心该文件描述符的写条件是否就绪
  • exceptfds:该文件描述符集中比特位的值为1的文件描述符,select只关心该文件描述符的异常条件是否就绪
  • 若需要select同时关心一个文件描述符的读写条件,即把readfds和writefds对应位置的值同时设置为1
  • 若无需select关心异常时间,我们把exceptfds设置为空即可!

select参数中的三个文件描述符集都是即是输入型参数,又是输出型参数

  • 以readfds为例
  • 当readfds作为输入型参数的时候,表示的含义是:用户告诉内核需要关心readfds中所有的fd的读事件
  • 当readfds作为输出型参数的时候,表示的含义是:内核告诉用户需要关心的文件描述符中,有哪些已经就绪了

fd_set输入输出时位图的含义:

  • 当作为输入型参数的时候,比特位的位置表示的是第几号fd,比特位的值表示是否需要内核关心的文件描述符的xx事件。若比特位的值为0表示无需内核关心该文件描述符的xx事件,若比特位的值为1表示需要内核关心该文件描述符的xx事件
  • 当作为输出型参数的时候,比特位的位置表示的是第几号fd,比特位的值表示需要内核关心的文件描述符中,哪些已经就绪了。若比特位的值为0表示未关心或未就绪,若比特位的值为1表示已就绪

文件描述符集的操作

对fd_set的操作只允许使用系统调用,不允许自己去改,哪怕你已经知道了它是个位图

文件描述符集的头文件是:sys/select.h

清空(所有比特位清零)文件描述符集:FD_ZERO

void FD_ZERO(fd_set *set);
  • 功能:把set文件描述符集中的所有比特位清零 

清零文件描述符集中指定fd的值:FD_CLR

void FD_CLR(int fd, fd_set *set);
  • 功能:把set集中fd对应的比特位清零 

判断文件描述符集中指定fd的值:FD_ISSET

int  FD_ISSET(int fd, fd_set *set);
  • 功能:判断set集中fd对应的比特位是否为1
  • 返回值:就是set集中fd对应比特位的值。(若在即为1,若不在即为0)

新增(置为1)文件描述符集中指定fd:FD_SET

void FD_SET(int fd, fd_set *set);
  • 功能:把set集中fd对应的比特位置为1

select服务器实现

select服务器和一个简单的TCP服务器的区别主要体现在I/O上,所以我们基于我们之前写过的TCP服务器进行修改即可!TCP服务器

select服务器 vs 之前的TCP服务器

  • 相同点:都是要创建套接字并基于套接字进行通信,甚至构建套接字的过程都一摸一样
  • 不同点:select的I/O方案采用的是多路转接,而之前的TCP服务器是采用阻塞I/O的方式

基于上述原因,所以对于select服务器来说,只需要在TCP服务器的基础上,修改Loop函数即可


获取连接

accept的本质其实也是I/O,I/O = 等 + 拷贝,accept也是等+拷贝。

所以新连接到来时就等价于有了读事件。

读事件可以由select进行监管,即可以把listen套接字添加到readfds中

把listen套接字交由select进行监管要完成的3个步骤:

  • 定义fd_set
  • 填充fd_set
  • 系统调用select 

 select获取到哪些fd条件就绪,进行返回时,我们要根据select的返回值来分情况处理

  • 若select的返回值大于0,表示已经有多少个文件描述符的条件就绪。上层已经可以根据readfds进行处理了
  • 若select的返回值等于0,则表示超时。若select是阻塞监管,那么select不会返回0。
  • 若select的返回值小于0,则表示select发生错误。
    void Loop() // 修改I/O为select{while(true)//需要不断获取连接,打上死循环即可{//定义fd_setfd_set rfds;//填充fd_setFD_ZERO(&rfds);FD_SET(_listensock,&rfds);//调用selectint n = select(_listensock + 1 , &rfds,nullptr,nullptr,nullptr);//select只关心读事件,并且阻塞等待if(n > 0){//表示已经有文件描述符条件就绪了,此时rfds中比特位为1的位置就是已经就绪的文件描述符handlerEvent(rfds);}else if(n == 0){//阻塞等待不会走到这std::cout << "timeout..." << std::endl;}else {std::cerr << "Select Error!" << std::endl;exit(2);}}

handlerEvent

上述,我们已经将listen套接字交给select进行监管了。那么若listen套接字读条件就绪,即有客户端向发送连接时,如何处理呢?

这就由handlerEvent进行处理

由于select监管的文件描述符可能是listen套接字,也可能是普通套接字,所以我们需要对这两种套接字进行分类讨论

对于listen套接字来说:

  • listen套接字的读事件就绪,也就意味着有新的客户端连接到来,我们首先要accept新连接
  • accept过后会获取一个新的普通套接字,我们需要把普通套接字交给select进行监管

对于普通套接字来说:

  • 普通套接字的读事件就绪,也就意味着客户端发来了一条新的I/O数据
  • 此时服务器提供服务即可! 

细节问题:辅助数组

当用户调用select传入rfds时,此时的rfds是输入型参数

当select返回时,此时的rfds是输出型参数

rfds作为输入型参数和输出型参数时,含义是完全不同的!

举个例子:若我们调用select时设置了1号3号5号文件描述符的读事件。假设select在监管时只有3号文件描述符就绪了,那么select会直接修改rfds,此时没有就绪的文件描述符就被修改了

 

  • rfds被select修改了,但我们之后还需要关心1号和5号文件描述符的读事件! 

为了解决这个问题,所以调用select时一般会搭配上一个辅助数据结构来实现!(以数组为例)

辅助数组中一般会存储调用select之前的rfds,方便内核修改了rfds后,用户进行重新设置

服务器构造的时候,我们可以直接构造好辅助数组,此外,辅助数组我们可以直接设置为成员变量

class TCPServer
{
public:const static int N = sizeof(fd_set) * 8; //辅助数组的大小const static int defaultfd = -1; //辅助数组元素的初始值TCPServer(uint16_t port): _port(port), _isrunning(false){//初始化辅助数组for(int i = 0 ; i < N ; ++i){fd_array[i] = defaultfd;}//listen套接字是必须要的,直接添加进辅助数组}
protected:uint16_t _port;int _listensock;bool _isrunning;int fd_array[N]; //辅助数组,元素的含义是需要内核关心的套接字编号
};

注意:该服务器的listensock是在初始化套接字时创建的,在socket创建listensock的后面再把listen套接字传入到辅助数组中  


handlerEvent的实现 

通过之前介绍select调用,我们会发现若我们不通知内核需要关心某个fd,那么select返回时这个fd一定不会在rfds中。

  • 若我们告诉内核需要关心第n号文件描述符,那么select返回时该文件描述符不一定会就绪
  • 若我们不告诉内核需要关心第n号文件描述符,那么select返回时该文件描述符一定不会就绪

除此之外,select返回时,可能不止一个文件描述符就绪了

所以第一步我们需要遍历整个辅助数组

  • 若遍历过程中,fd_array[i]的值是默认值,说明该位置还没被使用,直接跳过即可
  • 若遍历过程中,fd_array[i]的值不是默认值,意味着该位置存储的一定是需要内核关心的文件描述符的编号,此时需要看这个文件描述符是否在输出型参数rfds中,若在,则该文件描述符就绪。否则,该文件描述符没就绪
  • 判断一个文件描述符是否在fd_set中,我们使用的调用是FD_SET 

第二步就是分类讨论,区分该套接字是listen套接字还是普通套接字

    void handlerEvent(fd_set &rfds){for(int i = 0 ; i < N ; ++i){//若该fd不在辅助数组中,说明该文件描述符一定不会就绪if(fd_array[i] == defaultfd) continue;//走到这,说明该文件描述符被关心,经过rfds判断是否就绪if(!FD_ISSET(fd_array[i],&rfds)) continue; //区分处理if(fd_array[i] == _listensock){//该套接字是listen套接字,完成的工作是处理连接AcceptClient();}else {//该套接字是普通套接字,完成的工作是提供I/O服务Service(fd_array[i]);}}}

处理连接:AcceptClient 

 到这一步,说明listen套接字的读事件已经就绪

需要进行两步操作:

  • 获取新连接,并获取新的套接字
  • 把套接字交给select进行监管

如何交给select进行监管?把这个新的套接字设置进辅助数组即可

    void AcceptClient(){//1.获取新连接,获取新的套接字int sockfd;Accept(sockfd);//2.把新连接设置进辅助数组int pos = 1;for(; pos < N ; ++pos){//2.1选择一个没被用过的位置 if(fd_array[pos] == defaultfd) break;}if(pos < N){//2.2找到了一个没有被用的空位置fd_array[pos] = sockfd;}else{//pos == N//2.3说明fd_array已经被使用满了close(sockfd);std::cerr << "Server is full!" << std::endl;}}

 提供服务:Service

Service注意不要死循环即可! 

    void Service(int sockfd){//获取客户端的输入char buffer[1024];int n = recv(sockfd, buffer, sizeof(buffer), MSG_WAITALL);if (n > 0){// 接收成功std::cout << "Client say# " << buffer << std::endl;// 服务器把buffer中的数据重新发送给客户端,完成服务//发送客户端的输入send(sockfd, buffer, sizeof(buffer), 0);}else if (n == 0){// 客户端套接字关闭std::cout << "client socket close!" << std::endl;close(sockfd);}else{// recv errorstd::cerr << "recv error!" << std::endl;close(sockfd);exit(5);}}

修改Loop函数

Loop函数的修改主要有几点 

  • 先把listen套接字放入辅助数组
  • 每一次循环都得遍历一遍辅助数组,构建fd_set输入型参数,用于传递select参数
  • 遍历过程中找出最大的文件描述符,用于传递select参数
    void Loop() // 修改I/O为select{fd_array[0] = _listensock;while(true)//需要不断获取连接,打上死循环即可{//定义fd_setfd_set rfds;//填充fd_setFD_ZERO(&rfds);//max_fd主要用于select参数填写int max_fd = defaultfd;for(size_t i = 0 ; i < N ; ++i){if(fd_array[i] == defaultfd) continue;FD_SET(fd_array[i],&rfds);if(fd_array[i] > max_fd) max_fd = fd_array[i]; }//调用selectint n = select(max_fd + 1 , &rfds,nullptr,nullptr,nullptr);//select只关心读事件,并且阻塞等待if(n > 0){//表示已经有文件描述符条件就绪了,此时rfds中比特位为1的位置就是已经就绪的文件描述符handlerEvent(rfds);}else if(n == 0){//阻塞等待不会走到这std::cout << "timeout..." << std::endl;continue;}else {std::cerr << "Select Error!" << std::endl;continue;}}}

select服务器代码链接 

select服务器实现


select的优缺点 

优点:

  • 与其他I/O模型相比,select可以同时等待多个fd。对I/O性能有着显著提升
  • select是最早出现的多路转接的方案,在很多较旧的服务器或系统上,select仍然会被使用 

缺点:

  • 同时等待的fd的个数,有上限。这一缺点是由于fd_set类型被设置为固定大小的位图。通常为1024个比特位
  • select输入输出参数混合。每次都要进行重新设定。通过服务器的编写我们会发现,使用select往往会造成大范围的遍历,而遍历对性能的损耗其实也是蛮大的
  • 内核和用户之间的数据拷贝,这个缺点所有的多路转接方案都会存在
  • select底层监视多个fd的时候,OS会在内部进行遍历检测所有的fd,至少有一个就绪就返回。如果没有,就按照策略!

由于select的缺点比较多,所有后来提出的多路转接方案都是基于这些缺点进行的改良。

其中,我们接下来介绍的poll就对select的某些缺点进行了改良 


多路转接之poll 

poll对select进行一定程度的改良

  • poll解决了select同时等待fd有上限的问题
  • poll解决了select输入输出混合,导致代码中出现多次遍历的问题

poll接口 

       #include <poll.h>int poll(struct pollfd *fds, nfds_t nfds, int timeout);
  • timeout:poll的timeout是输入型参数,poll等待时按照timeout的策略进行等待,可以类比select的timeout参数,poll的timeout的单位是ms(毫秒),若poll在等待了timeout毫秒后还没有任何fd就绪,那么poll会超时返回。特殊的:若timeout被设置为0则表示非阻塞等待。若timeout被设置为-1则表示阻塞等待
  • fds和nfds:这两个合起来我们可以看成是一个数组,其中nfds表示的是数组的元素个数,而fds表示数组的首元素地址。数组的元素类型是struct pollfd。

poll如何解决select输入输出参数混合的问题的? 

 poll的fds实际上也是一个输入且输出型参数!

struct pollfd结构:

           struct pollfd {int   fd;         /* file descriptor */short events;     /* requested events */short revents;    /* returned events */};
  • fd:表示需要关心的文件描述符编号
  • events:请求事件,这是用于用户告诉内核需要关心fd的哪些事件,若这个参数用户设置为POLLIN,则表示需要关心fd的读事件。若这个参数用户设置为POLLOUT,则表示需要关心fd的写事件。若用户既想关心读,又想关心写,则 POLLOUT | POLLIN即可,也就是说参数可以进行组合
  • revents:返回事件,这是内核告诉用户,指定fd的哪些事件好了!若用户想看看该fd的读事件是否就绪,则revents & POLLIN即可!

poll的事件除了读写以外还有哪些:(man手册有详细介绍) 

我们可以看出,poll的fds参数虽然还是输入输出型的参数,但用户通知内核和内核通知用户所用的变量不再是同一个,这就是poll解决输入输出混合问题的方案 


poll如何解决select同时等待的fd有上限的问题? 

从参数上,我们已经可以猜到它的解决方案了

poll的fds参数和nfds参数是配合使用的,fds相当于数据首元素地址,nfds是由用户确定的,换句话来说,只要用户想且内存允许,你的fd想要多少就有多少!不会受到poll接口的影响


poll服务器实现(select服务器改写) 

经过select服务器的铺垫,poll服务器改写起来是相对容易的:poll服务器的改写代码


poll的优缺点

优点:

  • poll在select的基础上,弥补了select的一些缺点。并且poll也是多路转接方案的一种,与其他四种I/O模型相比,poll的优点是它可以同时接收多个文件描述符
  • poll在select的基础上,弥补了select同时等待的fd有上限的问题,poll则是完全由用户自主决定fd的大小。
  • poll在select的基础上,弥补了select输入输出参数混合的问题,每次调用poll时,都不再需要重新遍历设定参数!

缺点:

  • poll与我们后续所学习的epoll相比,它的缺点是底层poll在检查fd是否就绪时,仍然采用的是遍历整个fd数组的方式,效率较低
  • poll虽然比起select来说较优,但由于其出现的时间点和技术发展的趋势,大多数旧的设备/服务器当中,都已经采用了select。而对于新的设备/服务器来说,poll被epoll所替代。所以poll所处的地位较为尴尬
  • 内核和用户之间的数据拷贝,这个缺点所有的多路转接方案都会存在


http://www.ppmy.cn/devtools/163187.html

相关文章

内容中台的企业CMS架构是什么?

企业CMS模块化架构 现代企业内容管理系统的核心在于模块化架构设计&#xff0c;通过解耦内容生产、存储、发布等环节构建灵活的技术栈。动态/静态发布引擎整合技术使系统既能处理实时更新的产品文档&#xff0c;也能生成高并发的营销落地页&#xff0c;配合版本控制机制确保内…

HDFS联邦机制与HA

1.Federation背景介绍 从上图中,我们可以很明显地看出现有的HDFS数据管理&#xff0c;数据存储2层分层的结构。也就是说,所有关于存储数据的信息和管理是放在NameNode这边,而真实数据的存储则是在各个DataNode下。而这些隶属于同一个NameNode&#xff0c;所管理的数据都是在同…

ElasticSearch12-8.x安装

零、文章目录 ElasticSearch12-8.x安装 Windows版本安装文件百度网盘地址&#xff1a;https://pan.baidu.com/s/1AEBK1VFnVhogBbs-MSwWJw?pwd8888Linux版本安装文件百度网盘地址&#xff1a; https://pan.baidu.com/s/1qlzBaim5f1qPFOy7qHnaWw?pwd8888Linux相关知识参考&am…

鸿蒙5.0实战案例:基于AVCodecKit的音视频解码及二次处理播放

往期推文全新看点&#xff08;文中附带全新鸿蒙5.0全栈学习笔录&#xff09; ✏️ 鸿蒙&#xff08;HarmonyOS&#xff09;北向开发知识点记录~ ✏️ 鸿蒙&#xff08;OpenHarmony&#xff09;南向开发保姆级知识点汇总~ ✏️ 鸿蒙应用开发与鸿蒙系统开发哪个更有前景&#…

CF 90A.Cableway(Java实现)

题目分析 输入三个数字分别代表r&#xff0c;g&#xff0c;b三组学生数量&#xff0c;缆车按照r-g-b-r的方式循环&#xff0c;每一分钟来一辆车&#xff0c;且初始0时r车准备出发。全程上山时间为30分钟。 思路分析 每走一辆车则到达山顶的时间就多一分钟&#xff0c;那只需要…

LLM2CLIP论文学习笔记:强大的语言模型解锁更丰富的视觉表征

1. 写在前面 今天分享的一篇论文《LLM2CLIP: P OWERFUL L ANGUAGE M ODEL U NLOCKS R ICHER V ISUAL R EPRESENTATION》&#xff0c; 2024年9月微软和同济大学的一篇paper&#xff0c; 是多模态领域的一篇工作&#xff0c;主要探索了如何将大模型融合到Clip模型里面来进一步提…

DeepSeek开源周高能开场:新一代高效推理引擎FlashMLA正式发布

全球AI社区沸腾&#xff01;DeepSeek开源周高能开场&#xff1a;新一代高效推理引擎FlashMLA正式发布 北京时间今晨&#xff0c;国内领先的人工智能研究机构深度求索&#xff08;DeepSeek&#xff09;在GitHub平台重磅推出全新开源项目FlashMLA&#xff0c;以破竹之势在开源界…

【多线程-第三天-NSOperation的练习-tableView异步下载网络图片-下载操作缓存池 Objective-C语言】

一、下载操作缓存池 1.下面我们来看操作缓存池,我们先演示一下问题,看看为什么要加这么一个操作缓存池,什么是操作缓存池,不用管呢,我们先来看啊,首先有什么问题, 看这个问题之前,我这儿写一个touch,点击屏幕的时候调用, 额,不能点击屏幕啊,因为现在屏幕点不着,我…