高效IO——IO多路转接select

编程入门 行业动态 更新时间:2024-10-15 04:22:06

<a href=https://www.elefans.com/category/jswz/34/1769624.html style=高效IO——IO多路转接select"/>

高效IO——IO多路转接select

目录

一.概念

二.select函数

        2.1 函数原型

        2.2参数详细介绍

                2.2.1 nfd

                2.2.2readfds,writefds,errorfds

                2.2.3 timeout

        2.3. 网络中读/写/异常的就绪条件

        2.4 select特点

        2.5 select缺点

三.select的使用


一.概念

        IO主要有两个动作,等待条件就绪和进行数据拷贝。高效IO就是将等待时间比重减小。

        IO多路转接是高效IO的一种。通过调用select,poll,epoll在同一时刻等待多个文件描述符。当至少一个文件描述符准备就绪,再来进行IO操作时,就不需要等待了。

        这样一次性等待多个文件描述符,条件就绪的概率增加了,等待的时间也会减少。

        下文主要介绍select。

二.select函数

        2.1 函数原型

#include <sys/select.h>int select(int nfds, fd_set *restrict readfds,fd_set *restrict writefds,fd_set *restrict errorfds,struct timeval *restrict timeout);

作用:select可以监视多个文件描述符的状态,程序会停在select这里等待,直到一个或者多个文件描述符状态发生变化。

参数:

参数作用
nfd需要监视最大文件描述符值加1
readfds类型为fd_set,可读文件文件描述符的集合,输入输出型参数
writefds类型为fd_set,可写文件文件描述符的集合,输入输出型参数
errorfds类型为fd_set,异常文件文件描述符的集合,输入输出型参数
timeout结构为timeval,用来设置select等待时间,输入输出型参数

返回值:

  • 执行成功返回文件描述符状态改变个数
  • 返回0,说明等待时间超过timeout。
  • 当有错误发生时返回-1,错误原因存于errno,此时参数readfds,writefds,errorfds和timeout的值变得不可预测。
    • 错误码可能是:
      • EBADF:文件描述词无效或者该文件已经关闭
      • EINTR:此调用被信号中断
      • EINVAL:参数n为负值
      • ENOMEM:核心内存不足

        2.2参数详细介绍

                2.2.1 nfd

        nfd:需要监视最大文件描述符值加1。

如果需要监视的文件描述符为1,2,3,4,nfd等于5。如果逍遥监视的文件描述符为1,5,nfd等于6。

                2.2.2readfds,writefds,errorfds

        readfds,writefds,errorfds是主要和类型fd_set有关。并且它们是类似的。

关于fd_set结构:

typedef struct
{
/*XPG4.2requiresthismembername.Otherwiseavoidthename
fromtheglobalnamespace.*/
#ifdef__USE_XOPEN
__fd_maskfds_bits[__FD_SETSIZE/__NFDBITS];
#define__FDS_BITS(set)((set)->fds_bits)
#else
__fd_mask__fds_bits[__FD_SETSIZE/__NFDBITS];
#define__FDS_BITS(set)((set)->__fds_bits)
#endif
}fd_set;

        fd_set是文件描述符集,结构实际是一个位图。

        readfds,writefds,errorfds是输入输出参数,输入时是用户想告诉内核需要监视哪些文件描述符,当作为输出时,是内核想告诉用户,那些文件描述符已经就绪。

        位图的对应位代表着要监视的文件描述符,比特位的内容,作为输入时,内容代表需要监视的文件,作为输出时,内容代表那些文件条件已经就绪。

        比如:readfds:拿8位举例,作为输入时,当输入1001 0101时,是用户想告诉内核,需要监视文件描述符等于0,2,4,7的文件的读事件的状态。作为输出时,输出为1000 0001时,是内核想告诉用户,文件描述符为0,7的文件读事件一ing就绪,可以进行读操作。

        内核监视文件的个数是确定的,说明内核监视文件的个数是有限的。内核监视多个文件描述符,采用监视的方法是轮询监视。

由于不同的系统fd_set实现方式可能不同,可能是数组,可能是结构体,所以提供了一组操作fd_set的接口,来对位图进行设置。

void FD_CLR(int fd, fd_set *fdset);  //用来清除fd_set中相关fd的位
int FD_ISSET(int fd, fd_set *fdset); //用来测试fd_set中相关fd的位是否为真
void FD_SET(int fd, fd_set *fdset);  //用来设置fd_set中相关fd的位
void FD_ZERO(fd_set *fdset);         //用来清除fd_set中的全部位,相当于初始化

        注意:用户输入了监视那些为你文件描述符,内核输出条件就绪的文件,一定只会是这些文件描述符里的子集。 

                2.2.3 timeout

        timeout,结构是timeval。用来设置select等待时间。

关于timeval结构:

struct timeval
{time_t      tv_sec;     /* seconds 秒*/suseconds_t tv_usec;    /* microseconds 微秒*/
};

参数timeout的取值:

  1. NULL:表示select在没有文件条件就绪时,会阻塞等待。
  2. 0:非阻塞等待,不管条件就没就绪都会返回,用于检测监视的文件的状态。
  3. 特定的时间值:等待一段时间,在时间范围内有文件条件就绪,返回,超过时间select返回0。

timeout也是一个输入输出参数。当输入时,用户告诉内核等待时间timeout,当输出时,内核等待完毕,等待时间timeout就为0了。

        注意:在编码时,由于readfds,writefds,errorfds和timeout都是输入输出型参数,当select一次后,重新select时,由于输出时已经改变参数的值,所以需要重新设定readfds,writefds,errorfds和timeout的值。

        2.3. 网络中读/写/异常的就绪条件

读就绪

  • 在socket内核中,接收缓存区的字节数,大于等于低水位标记SO_RCVLOWAT,此时可以无阻塞的读取该文件。
  • 监听的socket上有了新的连接请求,socket连接请求也是以读的方式获取的。
  • socketTCP通信,对端关闭连接,此时对该socket读返回0.
  • socket上有未处理的错误。

写就绪

  • socket内核中,发送缓冲区中的空闲位置大小,大于或者等于低水位标记。
  • socket使用非阻塞connect连接成功或失败之后。
  • socket的写操作被关闭,对于写操作被关闭的socket进行写操作,会触发SIGPIPE信号。
  • socket上有为读取的错误。

异常就绪:

  • socket收到带外数据。

        2.4 select特点

  • 内核可监控的文件是有限的,取决于fd_set的位数,即能监控的文件数为sizeof(fd_set)*8。
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>int main(){printf("%lu\n",sizeof(fd_set)*8); return 0;
}

不同系统值不同。

  • 将fd加入select监控集中,还需要一个数组array来保存select监控的文件描述符。
    • select返回后,需要对数组中保存每一个文件描述符进行判断,是否就绪。FD_ISSET。
    • select返回后会改变fd_set文件描述符集,每次重新开始select时,需要重新设定数组中保存的文件描述符到fd_set中。FD_SET。并且需要保存文件描述符的最大值,用于select参数。

这个在下面编码是可以明显观察到。

        2.5 select缺点

  • 每次调用select,都需要程序员重新设定fd_set集合。
  • 每次调用select,都需要把fd集合从用户态拷贝到内核,每次调用select都需要将要监视的文件事件,重新从用户拷贝到内核,这个开销在fd很多会很大。
  • 内核在监视所有文件时,都需要不断遍历传进来的所有文件,轮询检测哪个文件事件就绪,这个开销在fd很多时会很大。
  • select支持监视的文件数量是有限的。

三.select的使用

用select编写一个单进程echo服务器。

注意点:

  1. 需要用数组保存所有要监视的文件描述符。该文件描述符包括进行读的套接字文件描述符和连接套接字。
  2. 当没有连接时,accept会阻塞等待,所以连接也需要select等待。
  3. 连接是socket返回的套接字做的,有新连接来时,也是读就绪。
  4. IO读写是accept返回套接字做的,接收缓冲区的字节数大于低水位标记,读就绪。发送缓冲区剩余空间大小,大于低水位标记,写就绪。
  5. 当获取到连接,不是直接进行读写,而是将accept返回值放到数组中,如果直接读写,如果客户端没有发数据,又会阻塞。
  6. 注意fd_set位图需要用FD_ZERO初始化

        再程序中有一个BUG,一次性读取并不一定将整个request全部读上来了,可能只读了一部分。我需要将整个数据读上来,再返回response给客户端。而我们定义的收集数据的缓冲区是一个局部变量,每次从调用函数都会被重新建立,之前数据不能保存了。        

        如果定义成全局,其它文件的数据也保存在缓冲区中,就乱了,所以要每一个文件描述符需要一个缓冲区。

        我们可以定义一个map,键值key为文件描述符,value为缓冲区,每次将数据保存到对应文件描述符的缓冲区中。当整个数据时,再返回response。

        但是在epoll中很好的解决了这个问题。

套接字设定: 

#pragma once #include <iostream>
#include <string>
#include <stdlib.h>
#include <unistd.h>#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <fcntl.h>#define BLACKLOG 5
using namespace std;class Sock{public:static int Socket(){int sock = 0;sock = socket(AF_INET, SOCK_STREAM, 0);if(sock < 0){cerr << "socket error"<<endl;exit(1);}return sock;}static void Bind(int sock, int port){struct sockaddr_in local;local.sin_family = AF_INET;local.sin_port = htons(port);local.sin_addr.s_addr = htonl(INADDR_ANY);if(bind(sock, (struct sockaddr *)&local, sizeof(local)) < 0){cerr << "bind error" <<endl;exit(3);}}static void Listen(int sock){if(listen(sock, BLACKLOG) < 0){cerr << "listen error"<<endl;exit(4);}}static int Accept(int lsock){struct sockaddr_in peer;socklen_t len = sizeof(peer);return  accept(lsock, (struct sockaddr *)&peer, &len);}};

select服务器主体:

#pragma once #include "Sock.hpp"#define NUM sizeof(fd_set)*8//数组大小=最多能监视文件个数
#define DET_FD -1//数组默认文件描述符 class SelectServer{private:int _lsock;//套接字int _port;//端口号int array[NUM];//保存要监视的文件描述符public:SelectServer(int lsock = -1, int port = 8080):_lsock(lsock),_port(port){}void InitServer(){for(size_t i =  0; i < NUM; i++){array[i] = DET_FD;}_lsock = Sock::Socket();//端口复用                                                                                                                                         int opt = 1;                                                    setsockopt(_lsock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt)); Sock::Bind(_lsock, _port);Sock::Listen(_lsock);array[0] = _lsock;}void AddtoArray(int index){//找到没有数组没有占用的位置size_t i = 0;for(; i < NUM; i++){if(array[i] == DET_FD){break;}}//满了if(i >= NUM){cout<<"select is full, close fd"<<endl;close(index);}else{array[i] = index;}}void Delete(size_t index){if(index >= 0 && index < NUM){array[index] = DET_FD;}}void Handle(int i){//IO条件就绪char buf[10240];ssize_t n = recv(array[i], buf, sizeof(buf), 0);if(n > 0){buf[n] = 0;cout<<buf<<endl;}else if(n == 0){//对端关闭cout<<"client close..."<<endl;close(array[i]);//文件已经关闭,还需要将数组文件描述符删除Delete(i);}else{cerr << "read error"<<endl;close(array[i]);Delete(i);}}void Start(){while(1){int maxfd = DET_FD;//重新设定,需要等待的文件fd_set readfds;//初始化fd_setFD_ZERO(&readfds);//找文件描述符,将要监视的fd_set对应位设置为1for(size_t i =0; i < NUM; i++){if(array[i] == DET_FD){continue;}cout <<array[i];FD_SET(array[i], &readfds);//找文件描述符最大值if(maxfd < array[i]){maxfd = array[i];}}cout<<endl;//struct timeval timeout = {5, 0};//struct timeval timeout = {0, 0};非阻塞等待//调用 select 等待多个文件//阻塞等待int fdn = select(maxfd+1, &readfds, nullptr, nullptr, nullptr);if(fdn > 0){//有文件就绪//找哪个文件就绪for(size_t i =0; i < NUM; i++){if(array[i] != DET_FD && FD_ISSET(array[i] , &readfds)){if(array[i] == _lsock){//有新连接int sock = Sock::Accept(array[i]);if(sock >= 0){cout << "get a link...."<<endl;//加入到数组中AddtoArray(sock);}}else{//进行IO操作Handle(i);}}}}//超时else if(fdn == 0){cerr << "select timeout..."<<endl;}//异常else{cerr <<"fdn:"<<fdn<< "select error"<<endl;}}}~SelectServer(){for(size_t i = 0; i < NUM; i++){if(array[i] != DET_FD){close(array[i]);}}}};
#include"selectServer.hpp"void Notice(string str){cout<<"Notice\n\t"<<"please enter port"<<endl;
}int main(int argc, char *argv[]){if(argc != 2){Notice(argv[0]);exit(1);}SelectServer *sser = new SelectServer(atoi(argv[1]));sser->InitServer();sser->Start();delete sser;return 0;
}

更多推荐

高效IO——IO多路转接select

本文发布于:2024-03-23 22:44:28,感谢您对本站的认可!
本文链接:https://www.elefans.com/category/jswz/34/1743715.html
版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系,我们将在24小时内删除。
本文标签:高效   多路   IO   select

发布评论

评论列表 (有 0 条评论)
草根站长

>www.elefans.com

编程频道|电子爱好者 - 技术资讯及电子产品介绍!