高級IO—select

高級IO—select


文章目錄

  • 高級IO—select
      • IO的概念
    • 五種IO模型
      • 阻塞IO
      • 非阻塞IO
      • 信號驅動IO
      • IO多路轉接
      • 異步IO
    • I/O多路轉接之select

IO的概念

通常指數據在內部存儲器和外部存儲器或其他周邊設備之間的輸入和輸出。輸入是系統接收的信號或數據,輸出則是從其發送的信號或數據。也可把輸入輸出認為是信息處理系統(例如計算器)與外部世界(人類或另一信息處理系統)之間的通信。

IO分為IO設備和IO接口

  • IO設備

IO設備是硬件中由人使用并與計算機進行通信的設備。例如鍵盤或鼠標是計算機的輸入設備,監視器和打印機是輸出設備。計算機之間的通信設備進行的通常是運行輸入輸出操作。

  • IO接口

I/O接口的功能是負責實現CPU通過系統總線把I/O電路和外圍設備聯系在一起。IO函數的底層是系統提供的系統調用,供用戶通過調用來實現從用戶態到內核態或內核態到用戶態的數據拷貝。

image-20231120103359858

實際上在網絡通信中,調用write并不是直接將數據寫到網絡中,而是將數據從應用層拷貝到傳輸層的發送緩沖區當中,然后由OS自主決定什么時候將數據向下交付,發送到網絡中。同理調用read并不是直接從網絡中讀取數據,而是將傳輸層的接收緩沖區的數據讀到應用層中。這意味調用read的時候,傳輸層的接收緩沖區并沒有數據,那么read函數就會阻塞住,直到緩沖區有數據,才能將數據讀到應用層。

因此IO本質不僅僅只有讀取/寫入,還有等待資源就緒的過程,即等+拷貝

提高IO的效率本質是每次IO中減少等待的時間,讓IO過程盡可能都是拷貝。因此為了提高IO的效率,衍生出多種IO模型。

五種IO模型

阻塞IO

在內核將數據準備好之前,系統調用會一直等待,所有的套接字默認的是阻塞方式。

常見的阻塞IO模型

用戶調用recvfrom函數,嘗試讀取數據,即調用系統調用,由用戶態切換到內核態,由于數據沒有準備好導致阻塞等待,數據準備好了立刻拷貝數據報并返回用戶態。

image-20231120105340836

代碼以使用read為例,讀取文件描述符為0即stdin的數據,默認以阻塞式方式讀取

#include"until.hpp"
#include<iostream>
#include<unistd.h>
using namespace std;int main()
{char buffer[1024];while(true){printf(">>>>");fflush(stdout);ssize_t i=read(0,buffer,sizeof(buffer)-1);if(i>0){buffer[i-1]=0;cout<<"echo# "<<buffer<<endl;}else if(i==0){cout<<"read end"<<endl;break;}else{//...}}return 0;
}

image-20231121171724379

非阻塞IO

如果內核還未將數據準備好, 系統調用不會阻塞等待,會直接返回, 并且返回EWOULDBLOCK錯誤碼。

非阻塞IO往往需要程序員以循環的方式反復嘗試讀寫文件描述符, 這個過程稱為輪詢。這意味著輪詢的過程需要一直占用CPU資源,對CPU來說是較大的浪費,一 般只有特定場景下才使用。

常見的非阻塞IO模型

用戶調用recvfrom函數,這次該函數是以非阻塞的方式進行調用,嘗試讀取數據,由用戶態切換到內核態,由于數據沒有準備好,直接返回EWOULDBLOCK。因此程序員需要以輪詢的方式調用recvfrom函數,數據準備好了立刻拷貝數據報并返回用戶態。輪詢的過程中一是需要占用CPU的資源,二是需要多次進行用戶態與內核態之間的轉換,資源浪費較為嚴重,該方式一般在特定場景才使用。

image-20231120105902797

需要將文件描述符設置為非阻塞狀態,那么讀取該文件描述符就以非阻塞方式讀取。

fcntl

用于控制文件描述符屬性的系統調用,它可以用于執行各種操作,包括設置文件狀態標志、獲取文件狀態標志、鎖定文件等。

函數原型

#include <fcntl.h>int fcntl(int fd, int cmd, ... /* struct flock *flockptr */);
  • fd:表示要操作的文件描述符。
  • cmd:表示操作類型,可以是以下值之一:F_GETFL:獲取文件狀態標志,F_SETFL:設置文件狀態標志,F_GETLK:獲取文件鎖定信息,F_SETLK:設置文件鎖定等。
  • 使用不同的cmd,會有不同的返回值。使用F_GETFL時,返回值是文件狀態標志flag。可以通過文件狀態標志將文件設置為非阻塞狀態。

until.hpp

#include<unistd.h>
#include<fcntl.h>
#include<stdio.h>
void Setnonblock(int sock)
{int flag=fcntl(sock,F_GETFL,0);if(flag<0){perror("fcntl");return;}fcntl(sock,F_SETFL,flag|O_NONBLOCK);//把文件描述符狀態設置為非阻塞O_NONBLOCK
}
#include"until.hpp"
#include<iostream>
#include<unistd.h>
using namespace std;int main()
{char buffer[1024];Setnonblock(0);while(true){printf(">>>>");fflush(stdout);ssize_t i=read(0,buffer,sizeof(buffer)-1);if(i>0){buffer[i-1]=0;cout<<"echo# "<<buffer<<endl;}else if(i==0){cout<<"read end"<<endl;break;}else{//...}sleep(1);}return 0;
}

image-20231121173823759

  • 非阻塞的返回值

對于非阻塞來說,底層沒有數據直接返回,返回值為-1,但這并不是發生錯誤,原因由錯誤碼來標記。錯誤碼為EAGAINEWOULDBLOCK表示沒有讀取到數據。相同的還有EINTER表示因為信號中斷導致返回,需要繼續讀取。

#include"until.hpp"
using namespace std;fd_set readset;
int main()
{setNonBlock(0);//將輸入緩沖區的IO行為設置為非阻塞char buffer[1024];//設置緩沖區while(true){ssize_t i= read(0,buffer,sizeof(buffer)-1);//從文件描述符為0(鍵盤)開始讀,讀到buffer緩沖區中if(i>0){buffer[i-1]=0;cout<<"echo# "<<buffer<<endl;}else if(i==0){cout<<"read end"<<endl;break;}else{cout<<"i: "<<i<<endl;cout<<"EAGAIN: "<<EAGAIN<<endl;cout<<"EWOULDBLOCK: "<<EWOULDBLOCK<<endl;}sleep(1);}return 0;
}

image-20231121205815467

非阻塞沒有讀取到數據直接返回的錯誤碼是11,EAGAINEWOULDBLOCK的錯誤碼也是11。

信號驅動IO

內核將數據準備好的時候, 使用SIGIO信號通知應用程序進行IO操作。

常見的信號驅動IO模型

先前建立好SIGIO信號處理程序,進程將等待資源就緒的過程托管給sigaction函數,讓該函數去等待數據,數據準備好后,以信號通知的方式返回,通知進程,此時進程直接調用recvfrom函數,拷貝數據報并返回。

image-20231120110521585

IO多路轉接

IO多路轉接能夠同時等待多個文件描述符的就緒狀態。

常見的IO多路轉接模型

進程將等待資源的過程托管給select函數,讓select去等待數據,資源準備好后,select函數返回可讀條件,通知進程,此時進程直接調用recvfrom函數,拷貝數據報并返回。這意味著可以讓多個進程將等待資源的過程托管給同一個select函數,哪個資源就緒,select函數就通知相應的程序進行讀取。

image-20231120114836319

異步IO

由內核在數據拷貝完成時, 通知應用程序(而信號驅動是告訴應用程序何時可以開始拷貝數據)。

常見異步IO模型

進程需要讀取某種資源時,調用aio_read函數(系統調用),將IO(等+拷貝)的過程托管給OS,讓OS負責等,數據準備好后,OS自動將數據拷貝到用戶層的緩沖區,然后返回指定信號,通知進程來處理數據。進程并不參與IO的過程,只負責處理數據。

image-20231120120525495

總結一下:

  1. 阻塞、非阻塞、信號驅動在IO的效率上并無差別,差別在于等待資源的過程。阻塞式在等的過程中不能做別的事,而非阻塞和信號驅動在等的過程中可以做其他事情。
  2. 阻塞、非阻塞、信號驅動、多路轉接實際上都參與了IO的過程,即IO的等待過程和拷貝過程,參與了其中一個過程都算作是同步IO。
  3. 異步IO是將IO過程托管給OS,并沒有參與IO的過程。
  4. 多路轉接的高效在于可以同時等待多個文件描述符,即等待多個資源就緒,并行等待資源,減少了等待資源的過程。

I/O多路轉接之select

select系統調用是用來讓我們的程序監視多個文件描述符的狀態變化的。可以將多個文件描述符托管給select去等待,存在文件描述符就緒,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:指向struct timeval結構的指針,用于設置超時時間。如果為NULLselect函數將一直阻塞,直到有文件描述符就緒。
  • select函數的返回值是就緒文件描述符的數量,如果返回值為-1,則表示出現錯誤。在這種情況下,可以使用perror函數來輸出錯誤信息。

說明一下:

  1. 由于文件描述符是OS中的文件描述符表的下標,該表是從小到大依次使用,因此所被占用的文件描述符是連續的,即nfds能涵蓋所使用的文件描述符范圍。如nfds=5,表示在0~4號文件描述符中查詢。

  2. readfdswritefdsexceptfdstimeout都是指針,即都是輸入輸出型參數。timeout所指向的結構是能夠表示秒、微妙。

     struct timeval {long    tv_sec;         /* seconds */long    tv_usec;        /* microseconds */};
    
    struct timeval timeout ={0,0};//表示非阻塞。
    struct timeval timeout =nullptr;//表示阻塞。
    struct timeval timeout ={5,0};//表示5秒內是阻塞式,超過5秒,非阻塞返回一次。
    

    需要注意的是,timeout是輸入輸出型參數,例如timeout ={5,0},若在第3秒結束時sock就緒,此時timeout的返回值為等待的剩余時間,即返回值為2秒即timeout ={2,0}。若在5秒期間sock都沒有就緒,那么返回值為0秒即timeout ={0,0},此時再次將該timeout參數傳入就表示非阻塞等待。因此timeout參數在傳入時需要重置。

  3. fd_set實際上是一個位圖結構。以readfd為例,用戶想要OS關心4,5號文件描述符的讀時間,那么輸入的位圖結構是0011 0000

image-20231122154737459

當關心時間內5號文件描述符就緒了,OS會對輸入的位圖進行改動,輸出表示哪些文件描述符已經就緒。輸出的位圖結構是0010 0000

image-20231122154905077

readfdwritefdexceptfd的結構都是位圖,且是分別不同的位圖,因此用戶可以傳入一個或多個位圖讓OS關心位圖指定的文件描述符上的讀事件,寫事件,異常事件,OS通過該位圖輸出哪些事件已經就緒。

我們并不需要直接傳入自己設置的位圖結構,而是通過OS提供的接口對位圖進行修改。 可以使用以下宏來操作fd_set

FD_ZERO(fd_set*set);將set中的所有位清零
FD_SET(int fd, fd_set *set);將set中的指定文件描述符位設置為1。傳入fd,用位圖來標定傳入的fd是否需要關心。
FD_CLR(int fd, fd_set *set);將set中的指定文件描述符位清零。
FD_ISSET(int fd, fd_set *set);檢查set中的指定文件描述符位是否被設置為1

通過一段server代碼來應用select函數

select.hpp

#include<unistd.h>
#include"Sock.hpp"
using namespace std;
static const int defaultport=8081;
class SelectServer
{
public:SelectServer(uint16_t port=defaultport):_port(port)
{}void initserver()
{
_listensock=Sock::Socket();//創建套接字
Sock::Bind(_listensock,_port);//bind信息
Sock::Listen(_listensock);//把sock設置為監聽狀態}void start()
{for(;;){fd_set rfd;FD_ZERO(&rfd);//清空位圖FD_SET(_listensock,&rfd);//把listensock設置進位圖,企圖讓OS對該sock關心struct timeval timeout={1,0};int n=select(_listensock+1,&rfd,nullptr,nullptr,&timeout);switch (n){case 0:cout<<"timeout......"<<endl;break;case -1:cout<<"select err"<<endl;default:cout<<"get new link..."<<endl;break;}sleep(1);}
}private:
uint16_t _port;
int _listensock;
};

main.cc

#include<iostream>
#include<unistd.h>
#include<memory>
#include"select.hpp"
using namespace std;static void Usage(string proc)
{cerr<<"Usage:\n\t"<<proc<<" port "<<"\n\n";
}string resp(const string& s)
{return s;
}int main(int argc,char* argv[])
{if(argc!=2){Usage(argv[0]);exit(USAGE_ERR);}unique_ptr<SelectServer> selsv(new SelectServer(atoi(argv[1])));selsv->initserver();selsv->start();return 0;
}

Sock.hpp

#pragma once#include<iostream>
#include<string>
#include<cstring>
#include<sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include "log.hpp"enum
{USAGE_ERR = 1,SOCKET_ERR,BIND_ERR,LISTEN_ERR
};class Sock
{const static int backlog=32;public:static int Socket(){int sock=socket(AF_INET,SOCK_STREAM,0);//創建套接字if(sock<0)//創建失敗{logMessage(FATAL,"create sock error");exit(SOCKET_ERR);}//創建成功logMessage(NORMAL,"create sock success");int opt=1;setsockopt(sock,SOL_SOCKET,SO_REUSEADDR|SO_REUSEPORT,&opt,sizeof(opt));//允許套接字關閉后立刻重啟return sock;} static void Bind(int sock,int port){//綁定自己的網絡信息struct sockaddr_in local;memset(&local,0,sizeof(local));//將結構體清空local.sin_family=AF_INET;//添加協議local.sin_port=htons(port);//添加端口號local.sin_addr.s_addr=htons(INADDR_ANY);//不綁定指定IP,可以接收任意IP主機發送來的數據//將本地設置的信息綁定到網絡協議棧if (bind(sock,(struct sockaddr*)&local,sizeof(local))<0){logMessage(FATAL,"bind socket error");exit(BIND_ERR);}logMessage(NORMAL,"bind socket success");}static void Listen(int sock)//將套接字設置為監聽{if(listen(sock,0)<0){logMessage(FATAL,"listen socket error");exit(LISTEN_ERR);}logMessage(NORMAL,"listen socket success");}static int Accpet(int listensock,string * clientip,uint16_t* clientport){struct sockaddr_in cli;socklen_t len= sizeof(cli);int sock=accept(listensock,(struct sockaddr*)&cli,&len);if(sock<0){logMessage(FATAL,"accept error");//這里accept失敗為什么不退出}else{logMessage(NORMAL,"accept a new link,get new sock : %d",sock);*clientip=inet_ntoa(cli.sin_addr);*clientport=ntohs(cli.sin_port);}return sock;}
};

log.hpp

#pragma once#include <iostream>
#include <string>
#include<ctime>
#include <sys/types.h>#include <unistd.h>#include <stdio.h>
#include <stdarg.h>
using namespace std;
#define DEBUG   0
#define NORMAL  1
#define WARNING 2
#define ERROR   3
#define FATAL   4#define NUM 1024
#define LOG_STR "./logstr.txt"
#define LOG_ERR "./log.err"
const char* to_str(int level)
{switch(level){case DEBUG: return "DEBUG";case NORMAL: return "NORMAL";case WARNING: return "WARNING";case ERROR: return "ERROR";case FATAL: return "FATAL";default: return nullptr;}
}void logMessage(int level, const char* format,...)
{char logprestr[NUM];
snprintf(logprestr,sizeof(logprestr),"[%s][%ld][%d]",to_str(level),(long int)time(nullptr),getpid());char logeldstr[NUM];
va_list arg;
va_start(arg,format); 
vsnprintf(logeldstr,sizeof(logeldstr),format,arg);//arg是logmessage函數列表中的...cout<<logprestr<<logeldstr<<endl;}

說明一下:

  1. 通過select函數對_listensock進行讀事件的關心,當連接到來時,select返回就緒的時間數。表示連接到來屬于讀事件

  2. 連接沒到來時select返回值為0,表示0個事件就緒。連接到來后返回值為1,表示已經有一個事件就緒。多次打印get new link...是因為底層的連接到來,上層并沒有把連接取走,因此底層的就緒事件一直存在。

由于服務器在最初時只使用listensock拿取底層的連接,而后續需要等待多個文件描述符時,可以通過數組來管理fd_set位圖的大小為128字節,那么該位圖可以同時關心128*8—1024個sock的就緒事件,因此管理sock的數組大小也應該是1024。

select.hpp

#include <unistd.h>
#include "Sock.hpp"
using namespace std;
static const int defaultport = 8081;
static const int fdnum = sizeof(fd_set) * 8;
static const int defaultfd = -1;
class SelectServer
{
public:SelectServer(uint16_t port = defaultport) : _port(port), _listensock(-1), _fdarry(nullptr){}void initserver(){_listensock = Sock::Socket();   // 創建套接字Sock::Bind(_listensock, _port); // bind信息Sock::Listen(_listensock);      // 把sock設置為監聽狀態// cout<<"fd_set size: "<<sizeof(fd_set)<<endl;_fdarry = new int[fdnum]; // 保存fd的數組for (int i = 0; i < fdnum; i++){_fdarry[i] = defaultfd;}_fdarry[0] = _listensock;}void Print()
{cout<<"fd list: ";for(int i=0;i<fdnum;i++){if(_fdarry[i]!=defaultfd)cout<<_fdarry[i]<<" ";}cout<<endl;
}void handleract(fd_set&rfd){if(FD_ISSET(_listensock,&rfd)){char buffer[1024];uint16_t clientport;string clientip;int sock = Sock::Accpet(_listensock, &clientip, &clientport); // 獲取sockif (sock < 0){cout << "Sock::Accept err " << endl;return;}cout << "get a new sock: " << sock << endl;int i=0;for(;i<fdnum;i++){if(_fdarry[i]!=defaultfd)continue;else break;}if(i==fdnum){cout<<"server is full,please wait"<<endl;close(sock);}_fdarry[i]=sock;FD_SET(_fdarry[i],&rfd);Print();}}void start(){for (;;){fd_set rfd;FD_ZERO(&rfd); // 清空位圖int maxfd = _fdarry[0];int i = 0;for (; i < fdnum; i++){if(_fdarry[i]==defaultfd)continue;FD_SET(_fdarry[i],&rfd);maxfd=maxfd>_fdarry[i]?maxfd:_fdarry[i];//更新最大fd數}// struct timeval timeout={1,0};// int n=select(_listensock+1,&rfd,nullptr,nullptr,&timeout);int n = select(maxfd + 1, &rfd, nullptr, nullptr, nullptr); // 阻塞式switch (n){case 0:cout << "timeout......" << endl;break;case -1:cout << "select err" << endl;default:cout << "get new link..." << endl;handleract(rfd);break;}sleep(1);}}private:uint16_t _port;int _listensock;int *_fdarry;
};

適用數組管理的原因在于:

  1. select的readfdwritefdexceptfd參數是輸入輸出型參數,函數返回時會改變這三個位圖,此時就需要通過數組去重置初始化這三個位圖。
  2. 通過位圖可以方便很方便的知道最大文件描述符數,前提是設置數組的默認sock。
  3. 根據數組內的默認sock和已經保存的sock,很方便的賦值給fd_set位圖參數。

現結合管理數組和select函數寫一個能夠接收client端發送來的信息,并且能夠返回的服務器

main.cc

#include<iostream>
#include<functional>
#include<vector>
#include<memory>
#include"err.hpp"
#include"selectserver.hpp"
using namespace std;
using namespace Select_sv;static void Usage(string proc)
{cerr<<"Usage:\n\t"<<proc<<" port "<<"\n\n";
}string resp(const string& s)
{return s;
}int main(int argc,char* argv[])
{unique_ptr<SelectServer> selsv(new SelectServer(resp));selsv->initServer();selsv->Start();return 0;
}

selectserver.hpp

#pragma once#include <iostream>
#include <sys/select.h>
#include <string>
#include <functional>
#include "Sock.hpp"using namespace std;namespace Select_sv
{static const int defaultport = 8080;         // 默認端口號static const int fdnum = sizeof(fd_set) * 8; // 可使用的套接字數量static const int defaultfd = -1;             // 默認套接字標志using func_t = function<string(const string &)>;class SelectServer{public:SelectServer(func_t f, int port = defaultport) : _func(f), _port(port), _listensock(-1), _fdarray(nullptr){}void initServer(){// 獲取套接字_listensock = Sock::Socket();cout << "Sock success" << endl;// 綁定網絡信息Sock::Bind(_listensock, _port);cout << "Bind success" << endl;// 把套接字設置為監聽狀態Sock::Listen(_listensock);cout << "Listen success" << endl;// 給每一個套接字都設置一個數組,保存套接字的設置情況cout << "fd_set size: " << sizeof(fd_set) << endl;_fdarray = new int[fdnum];for (int i = 0; i < fdnum; i++)_fdarray[i] = defaultfd; // 將每個套接字狀態都設置為默認(未使用狀態)_fdarray[0] = _listensock; // 第一個設置的套接字是通信套接字,供accept函數使用-建立連接//    cout << "initServer" << endl;}void Print(){cout << "now using socket: ";for (int i = 0; i < fdnum; i++){if (_fdarray[i] != defaultfd)cout << _fdarray[i] << " "; // 將設置進數組內的套接字進行打印}cout << endl;}void Accpter(int lsock){//     logMessage(DEBUG, "Accepter begin");string clientip;uint16_t clientport = 0;int sock = Sock::Accpet(lsock, &clientip, &clientport); // 若成功返回,返回一個用于通信的套接字if (sock < 0)return;logMessage(NORMAL, "accept success [%s:%d]", clientip.c_str(), clientport);int i = 0;for (; i < fdnum; i++){if (_fdarray[i] != defaultfd)continue;elsebreak;}if (i == fdnum) // 遍歷完全部socket發現沒用可使用的套接字{logMessage(WARNING, "server is full,please wait");close(sock); // 關閉用于通信的套接字,重新建立連接// _fdarray[i] = defaultfd;不需要去除,規定數組的0號下標對應的位置是專門用來拿連接的}else{_fdarray[i] = sock; // 把用于通信的套接字給select監管,讓它等待}Print();//     logMessage(DEBUG, "Accepter end");}void Recver(int sock, int pos){//    logMessage(DEBUG, "Recver begin");char buffer[1024];ssize_t s = recv(sock, buffer, sizeof(buffer) - 1, 0);if (s > 0){buffer[s] = 0;cout << "client# " << buffer << endl;}else if (s == 0){close(sock);               // 關閉該套接字,關閉通信通道_fdarray[pos] = defaultfd; // 將數組中的該套接字清除logMessage(NORMAL, "client quit");return;}else{close(sock);_fdarray[pos] = defaultfd; // 將數組中的該套接字清除logMessage(ERROR, "recv error");return;}// 將客戶端發來的數據原樣寫回去string resp = _func(buffer);write(sock, resp.c_str(), resp.size()); // 寫回去//     logMessage(DEBUG, "Recever end");}void Handlerop(fd_set &rfds){for (int i = 0; i < fdnum; i++){if (_fdarray[i] == defaultfd)continue;if (FD_ISSET(_fdarray[i], &rfds) && (_fdarray[i] == _listensock))// 此時i對應的數組位置是拿到連接的文件描述符,意味著在底層連接已經拿到,等待上層提取{Accpter(_listensock);}else if (FD_ISSET(_fdarray[i], &rfds)) // 此時存在數組內的對應套接字都是底層讀資源就緒{Recver(_fdarray[i], i);}else{}}}void Start(){// 將數組管理的套接字設置進fd_set類型的結構內for (;;){fd_set rfds;    // 當前程序只關心讀事件FD_ZERO(&rfds); // 對該結構(位圖)清空int maxfd = _fdarray[0];for (int i = 0; i < fdnum; i++){if (_fdarray[i] == defaultfd)continue;FD_SET(_fdarray[i], &rfds); // 將需要使用的套接字設置進讀事件結構中//若此時已經將連接拿到上層,因此select管理連接對應的sock就不會就緒,而可以只管理通信資源是否就緒if (maxfd < _fdarray[i])maxfd = _fdarray[i]; // 更新最大文件描述符//        cout << "listensock set to _fdarray success" << endl;}// 把讀事件交給select監管cout << "will select " << endl;int n = select(maxfd + 1, &rfds, nullptr, nullptr, nullptr); // 阻塞式監管cout << "select end" << endl;switch (n){case 0:logMessage(NORMAL, "timeout..."); // 監管時間內沒用套接字就緒,即超時返回break;case -1:logMessage(WARNING, "select error,error:%d, error string: ", errno, strerror(errno));break;default:logMessage(NORMAL, "get a new link..."); // 拿到新連接,即拿到通信的連接,客戶端主動斷開連接后,為何后續循環select都是拿到連接?Handlerop(rfds);break;}}}~SelectServer(){if (_listensock < 0) // 為什么是小于0?close(_listensock);if (_fdarray)delete[] _fdarray;}private:int _port;int _listensock;int *_fdarray; // 記錄需要交給select管理的套接字,每個套接字交給select管理的方式是傳遞整數給位圖,因此該數組的類型也是整數intfunc_t _func;};
}

image-20231124155440498

說明一下:

  1. initServer函數里,完成創建套接字,bind信息,將套接字設置為監聽狀態,并且初始化管理數組_fdarry,并將監聽套接字設置優先設置進數組的0號下標處,這不再改變。
  2. Start函數里,首先該函數是需要保證服務器的正常運行,因此是調用鏈是存在于死循環中。將管理數組內的sock設置進rfds位圖中,即告訴內核需要關心這些sock。接著調用select函數進行等待就緒事件。等待到就緒事件后調用Handlerop函數,對就緒事件進行處理。
  3. 由于該服務器目前只處理獲取連接,接收客戶端發送過來的數據并返回這兩個業務。因此在Handlerop函數中,通過管理數組對已經返回的位圖進行對比,判斷出是listensock就緒還是通信的數據到來。若是獲取到連接,則調用Accepter函數將底層的連接提取到應用層。若是數據到來,則調用Recver函數讀取底層的數據,并進行處理。
  4. Accpter函數中,不僅將連接獲取上來,還需要將獲取到的連接添加到管理數組中,以便于在下次循環中告訴OS關心該新連接。
  5. Recver函數中,調用recv函數進行讀取,通過仿函數對數據進行處理并寫回到sock中。

梳理調用鏈

image-20231123211007403

image-20231123211700037

image-20231123212652194

image-20231123212844031

總結一下:

select可以同時等待多個文件描述符,提高了IO的效率。但是也存在以下缺陷:

  1. select能夠等待的文件描述符是有上限的,在我這臺云服務器中能夠使用的fd一共有10002個(通過ulimit -a查詢

image-20231124153505075

而select使用的位圖結構fd_set所能管理的sock數為1024,這表明了select能夠同時等待的文件描述符是具有上限的。除非更改內核的參數,否則不能解決。

  1. 由于fd_set位圖是輸入輸出型參數,那么在傳入傳出時必然發送改變,因此我們需要通過第三方數組去管理合法的文件描述符。
  2. select函數的大部分參數都是輸入輸出型的,調用函數時,通過輸入參數用戶告訴內核信息,函數返回,通過輸出參數內核告訴用戶信息,即采用位圖結構傳遞參數時,需要不斷的進行用戶到內核,內核到用戶的狀態切換,并且還進行了數據拷貝,造成了不少成本。
  3. 由于使用的是位圖結構傳遞參數,并且位圖結構在輸入輸出時發生改變,導致我們需要遍歷所有的文件文件描述符,這帶來了一定的遍歷成本。而select的第一個參數是最大fd+1,是用來確定遍歷的范圍。

基于以上select函數的劣勢,前人總結衍生出了更好的方案,如pollepoll等等。

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/166997.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/166997.shtml
英文地址,請注明出處:http://en.pswp.cn/news/166997.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

Jmeter接口測試——使用教程(下)

前言 上一篇我給大家講了jmeter的基本介紹跟參數化和jmeter腳本及jmeter斷言&#xff0c;今天讓我們繼續往下看&#xff0c;學習一下jmeter新的知識點。 一、Jmeter關聯 我們知道斷言是從返回結果中檢查有沒有預期的值&#xff0c;現在有一個問題&#xff0c;有一個購買商品…

【學習筆記】GameFramework的非官方實例TowerDefense-GameFramework-Demo的流程

一、從游戲開始到打開一個Menu GameStart.unity GameEntry.Builtin.cs ProcedureComponent.cs GameStart.unity->GameFramework->Builtin->Procedure ProcedureLaunch.cs ProcedureSplash.cs ProcedurePreload.cs ProcedureLoadingScene.cs DataTables/Scene.txt Pro…

transformers中的data_collator

前言 使用huggingface的Dataset加載數據集&#xff0c;然后使用過tokenizer對文本數據進行編碼&#xff0c;但是此時的特征數據還不是tensor&#xff0c;需要轉換為深度學習框架所需的tensor類型。data_collator的作用就是將features特征數據轉換為tensor類型的dataset。 本文…

小學語文老師重點工作

小學語文老師是學生在語言學習過程中的關鍵引導者&#xff0c;他們的主要職責是幫助學生建立正確的語言基礎&#xff0c;培養良好的閱讀習慣&#xff0c;并提高學生的語文素養。以下是小學語文老師的一些重點工作。 一、教授語言知識 小學語文老師首要的任務是教授學生語言知識…

《DApp開發:開啟全新數字時代篇章》

隨著區塊鏈技術的日益成熟&#xff0c;去中心化應用&#xff08;DApp&#xff09;逐漸成為數字世界的新焦點。在這個充滿無限可能的全新領域&#xff0c;DApp開發為創新者們提供了開啟數字時代新篇章的鑰匙。 一、DApp&#xff1a;區塊鏈創新成果 DApp是建立在區塊鏈技術基礎之…

C/C++ 開發SCM服務管理組件

SCM&#xff08;Service Control Manager&#xff09;服務管理器是 Windows 操作系統中的一個關鍵組件&#xff0c;負責管理系統服務的啟動、停止和配置。服務是一種在后臺運行的應用程序&#xff0c;可以在系統啟動時自動啟動&#xff0c;也可以由用戶或其他應用程序手動啟動。…

CMakeLists.txt:打印find_package變量;判斷庫文件路徑設定是否正確;install文件設置

CMake打印find_package變量&#xff1b;install文件設置 打印find_package找到的各種變量判斷庫文件是否被找到install文件設置install詳細說明 打印find_package找到的各種變量 目的&#xff1a;find_package后&#xff0c;想使用找到的include/lib文件夾。 find_package(Yo…

chromium通信系統-mojo系統(一)-ipcz系統基本概念

ipcz 是chromium的跨進程通信系統。z可能是代表zero&#xff0c;表示0拷貝通信。 chromium的文檔是非常豐富的&#xff0c;關于ipcz最重要的一篇官方文檔是IPCZ。 關于ipcz本篇文章主要的目的是通過源代碼去分析它的實現。再進入分析前我們先對官方文檔做一個總結&#xff0c;…

axios封裝和請求跨域和.gitignore文件

axios封裝 首先這部分網上找找應該一大堆&#xff0c;其中本人喜歡同.env文件一同配合使用&#xff1b; let base_url process.env.PROJECT_NAME if (process.env.NODE_ENV production){base_url process.env.PROJECT_BASEURL process.env.PROJECT_NAME// base_url http:…

Java計算兩個時間的相差年,日,小時,分,秒

主函數 public static int dateDiff(char flag, Calendar calSrc, Calendar calDes) {long millisDiff getMillis(calSrc) - getMillis(calDes);if (flag y) {return (calSrc.get(Calendar.YEAR) - calDes.get(Calendar.YEAR));}if (flag d) {return (int) (millisDiff / D…

Unity RenderFeature架構分析

自定義RenderFeature接口流程 URP內部ScriptableRenderPass分析 public、protected屬性 renderPassEvent &#xff1a;渲染事件發生的時刻colorAttachments &#xff1a;渲染的顏色紋理列表 m_ColorAttachmentscolorAttachment &#xff1a;m_ColorAttachments[0];depthAttac…

【網絡奇幻之旅】那年我與大數據的邂逅

&#x1f33a;個人主頁&#xff1a;Dawn黎明開始 &#x1f380;系列專欄&#xff1a;網絡奇幻之旅 ?每日一句&#xff1a;循夢而行&#xff0c;向陽而生 &#x1f4e2;歡迎大家&#xff1a;關注&#x1f50d;點贊&#x1f44d;評論&#x1f4dd;收藏?? 文章目錄 &#x1f4…

Windows 下安裝MySQL8.0 Zip

1、將下載的mysql 壓縮包解壓。 2、已管理員身份證 打開 cmd窗口&#xff0c;進入到解壓目錄的&#xff0c;本文以解壓到 D:\soft\mysql-8.0.29-winx64 為例來介紹。 3、在解壓目錄下 新建一個 my.ini 文件。 my.ini 文件內容如下&#xff1a; [mysqld] # 設置3306端口 por…

linux wget --no-check-certificate

如果您希望每次使用wget命令時都跳過SSL證書檢查&#xff0c;可以將–no-check-certificate參數添加到wget的默認配置文件中。 請按照以下步驟進行操作&#xff1a; vi ~/.wgetrc# 插入內容 check_certificate off保存并關閉文件。 現在&#xff0c;wget命令將在每次使用時自…

windows遠程linux或遠程虛擬機連接拒絕問題排查

當我們使用MobaXterm遠程連接時&#xff0c;報錯如下&#xff1a; 1.首先檢查該ubuntu防火墻是否關閉&#xff0c;先將防火墻關閉。 1.檢查防火墻狀態 sudo ufw status 2.開啟防火墻 sudo ufw enable 3.關閉防火墻 sudo ufw disable 2.關閉防火墻后&#xff0c;使用ping命令相…

【數據結構/C++】棧和隊列_順序棧

#include<iostream> using namespace std; #define MaxSize 10 // 1. 順序棧 typedef int ElemType; struct Stack {ElemType data[MaxSize];int top; } SqStack; // 初始化棧 void init(Stack &s) {// 初始化棧頂指針s.top -1; } // 入棧 bool push(Stack &s, …

什么是工業物聯網(IOT)?這樣的IOT平臺你需要嗎?——青創智通

物聯網(IOT)是指在互聯網上為傳輸和共享數據而嵌入傳感器和軟件的互聯設備的廣泛性網絡。這允許將從物理對象收集的信息(數據)存儲在專用服務器或云中。通過分析這些積累的信息&#xff0c;通過提供最優的設備控制和方法&#xff0c;可以實現一個更安全、更方便的社會。在智能家…

【完美解決】 Python pyecharts Map 地圖數據不顯示

目錄 項目場景問題描述原因分析解決方案完整代碼 項目場景 Python數據可視化&#xff0c;使用 Pyecharts.charts 模塊中的Map&#xff0c;并導入數據來構建全國疫情熱力地圖 B站 黑馬程序員 Python課程【P106 第一階段 - 第十一章 - 02全國疫情地圖構建】 問題描述 本人在學習…

vue+face-api.js實現前端人臉識別功能

近期做了一個前端vue實現人臉識別的功能&#xff0c;主要功能邏輯包含&#xff1a;人臉識別&#xff0c;人臉驗證&#xff0c;喚起攝像頭視頻流之后從三個事件&#xff08;用戶點頭、搖頭、眨眼睛&#xff09;中隨機選中兩個事件&#xff0c;待兩個事件通過判斷后人臉靜止不動3…

基于Java+Vue+uniapp微信小程序微信閱讀網站平臺設計和實現

博主介紹&#xff1a;?全網粉絲30W,csdn特邀作者、博客專家、CSDN新星計劃導師、Java領域優質創作者,博客之星、掘金/華為云/阿里云/InfoQ等平臺優質作者、專注于Java技術領域和畢業項目實戰? &#x1f345;文末獲取源碼聯系&#x1f345; &#x1f447;&#x1f3fb; 精彩專…