select 機制的優勢介紹

select系統調用的的用途是:在一段指定的時間內,監聽用戶感興趣的文件描述符上可讀、可寫和異常等事件。

select 機制的優勢

為什么會出現select模型?

先看一下下面的這句代碼:

int iResult = recv(s, buffer,1024);

這是用來接收數據的,在默認的阻塞模式下的套接字里,recv會阻塞在那里,直到套接字連接上有數據可讀,把數據讀到buffer里后recv函數才會返回,不然就會一直阻塞在那里。在單線程的程序里出現這種情況會導致主線程(單線程程序里只有一個默認的主線程)被阻塞,這樣整個程序被鎖死在這里,如果永 遠沒數據發送過來,那么程序就會被永遠鎖死。這個問題可以用多線程解決,但是在有多個套接字連接的情況下,這不是一個好的選擇,擴展性很差。

再看代碼:

int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul);  iResult = recv(s, buffer,1024);

這一次recv的調用不管套接字連接上有沒有數據可以接收都會馬上返回。原因就在于我們用ioctlsocket把套接字設置為非阻塞模式了。不過你跟蹤一下就會發現,在沒有數據的情況下,recv確實是馬上返回了,但是也返回了一個錯誤:WSAEWOULDBLOCK,意思就是請求的操作沒有成功完成。

看到這里很多人可能會說,那么就重復調用recv并檢查返回值,直到成功為止,但是這樣做效率很成問題,開銷太大。

select模型的出現就是為了解決上述問題。
select模型的關鍵是使用一種有序的方式,對多個套接字進行統一管理與調度 。

select 機制的優勢介紹

如上所示,用戶首先將需要進行IO操作的socket添加到select中,然后阻塞等待select系統調用返回。當數據到達時,socket被激活,select函數返回。用戶線程正式發起read請求,讀取數據并繼續執行。

從流程上來看,使用select函數進行IO請求和同步阻塞模型沒有太大的區別,甚至還多了添加監視socket,以及調用select函數的額外操作,效率更差。但是,使用select以后最大的優勢是用戶可以在一個線程內同時處理多個socket的IO請求。用戶可以注冊多個socket,然后不斷地調用select讀取被激活的socket,即可達到在同一個線程內同時處理多個IO請求的目的。而在同步阻塞模型中,必須通過多線程的方式才能達到這個目的。

select流程偽代碼如下:

{      select(socket);      while(1)       {          sockets = select();          for(socket in sockets)           {              if(can_read(socket))               {                  read(socket, buffer);                  process(buffer);              }          }      }  }

select相關API介紹與使用

#include <sys/select.h>  #include <sys/time.h>  #include <sys/types.h>  #include <unistd.h>  int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);

參數說明:

maxfdp:被監聽的文件描述符的總數,它比所有文件描述符集合中的文件描述符的最大值大1,因為文件描述符是從0開始計數的;

readfds、writefds、exceptset:分別指向可讀、可寫和異常等事件對應的描述符集合。

timeout:用于設置select函數的超時時間,即告訴內核select等待多長時間之后就放棄等待。timeout == NULL 表示等待無限長的時間

timeval結構體定義如下:

struct timeval  {            long tv_sec;   /*秒 */      long tv_usec;  /*微秒 */     };

返回值:超時返回0;失敗返回-1;成功返回大于0的整數,這個整數表示就緒描述符的數目。

以下介紹與select函數相關的常見的幾個宏:

#include <sys/select.h>     int FD_ZERO(int fd, fd_set *fdset);   //一個 fd_set類型變量的所有位都設為 0  int FD_CLR(int fd, fd_set *fdset);  //清除某個位時可以使用  int FD_SET(int fd, fd_set *fd_set);   //設置變量的某個位置位  int FD_ISSET(int fd, fd_set *fdset); //測試某個位是否被置位

select使用范例:
當聲明了一個文件描述符集后,必須用FD_ZERO將所有位置零。之后將我們所感興趣的描述符所對應的位置位,操作如下:

fd_set rset;     int fd;     FD_ZERO(&rset);     FD_SET(fd, &rset);     FD_SET(stdin, &rset);

然后調用select函數,擁塞等待文件描述符事件的到來;如果超過設定的時間,則不再等待,繼續往下執行。

select(fd+1, &rset, NULL, NULL,NULL);

select返回后,用FD_ISSET測試給定位是否置位:

if(FD_ISSET(fd, &rset)     {       ...       //do something    }

下面是一個最簡單的select的使用例子:

#include <sys/select.h>  #include <sys/time.h>  #include <sys/types.h>  #include <unistd.h>  #include <stdio.h>    int main()  {      fd_set rd;      struct timeval tv;      int err;              FD_ZERO(&rd);      FD_SET(0,&rd);            tv.tv_sec = 5;      tv.tv_usec = 0;      err = select(1,&rd,NULL,NULL,&tv);            if(err == 0) //超時      {          printf("select time out!n");      }      else if(err == -1)  //失敗      {          printf("fail to select!n");      }      else  //成功      {          printf("data is available!n");      }              return 0;  }

我們運行該程序并且隨便輸入一些數據,程序就提示收到數據了。
select 機制的優勢介紹

深入理解select模型:

理解select模型的關鍵在于理解fd_set,為說明方便,取fd_set長度為1字節,fd_set中的每一bit可以對應一個文件描述符fd。則1字節長的fd_set最大可以對應8個fd。

(1)執行fd_set set; FD_ZERO(&set); 則set用位表示是0000,0000。

(2)若fd=5,執行FD_SET(fd,&set);后set變為0001,0000(第5位置為1)

(3)若再加入fd=2,fd=1,則set變為0001,0011

(4)執行select(6,&set,0,0,0)阻塞等待

(5)若fd=1,fd=2上都發生可讀事件,則select返回,此時set變為0000,0011。注意:沒有事件發生的fd=5被清空。

基于上面的討論,可以輕松得出select模型的特點:

(1)可監控的文件描述符個數取決與sizeof(fd_set)的值。我這邊服務器上sizeof(fd_set)=512,每bit表示一個文件描述符,則我服務器上支持的最大文件描述符是512*8=4096。據說可調,另有說雖然可調,但調整上限受于編譯內核時的變量值。

(2)將fd加入select監控集的同時,還要再使用一個數據結構array保存放到select監控集中的fd,一是用于再select返回后,array作為源數據和fd_set進行FD_ISSET判斷。二是select返回后會把以前加入的但并無事件發生的fd清空,則每次開始select前都要重新從array取得fd逐一加入(FD_ZERO最先),掃描array的同時取得fd最大值maxfd,用于select的第一個參數。

(3)可見select模型必須在select前循環加fd,取maxfd,select返回后利用FD_ISSET判斷是否有事件發生。

用select處理帶外數據

網絡程序中,select能處理的異常情況只有一種:socket上接收到帶外數據。

什么是帶外數據?

帶外數據(out—of—band data),有時也稱為加速數據(expedited data),
是指連接雙方中的一方發生重要事情,想要迅速地通知對方。
這種通知在已經排隊等待發送的任何“普通”(有時稱為“帶內”)數據之前發送。
帶外數據設計為比普通數據有更高的優先級。
帶外數據是映射到現有的連接中的,而不是在客戶機和服務器間再用一個連接。

我們寫的select程序經常都是用于接收普通數據的,當我們的服務器需要同時接收普通數據和帶外數據,我們如何使用select進行處理二者呢?

下面給出一個小demo:

#include <stdio.h>  #include <sys/time.h>  #include <sys/types.h>  #include <unistd.h>  #include <sys/socket.h>  #include <netinet/in.h>  #include <arpa/inet.h>  #include <string.h>  #include <fcntl.h>  #include <stdlib.h>      int main(int argc, char* argv[])  {      if(argc <= 2)      {          printf("usage: ip address + port numbersn");          return -1;      }            const char* ip = argv[1];      int port = atoi(argv[2]);            printf("ip: %sn",ip);          printf("port: %dn",port);            int ret = 0;      struct sockaddr_in address;      bzero(&address,sizeof(address));      address.sin_family = AF_INET;      inet_pton(AF_INET,ip,&address.sin_addr);      address.sin_port = htons(port);            int listenfd = socket(PF_INET,SOCK_STREAM,0);      if(listenfd < 0)      {          printf("Fail to create listen socket!n");          return -1;      }            ret = bind(listenfd,(struct sockaddr*)&address,sizeof(address));      if(ret == -1)      {          printf("Fail to bind socket!n");          return -1;      }            ret = listen(listenfd,5); //監聽隊列最大排隊數設置為5      if(ret == -1)      {          printf("Fail to listen socket!n");          return -1;      }            struct sockaddr_in client_address;  //記錄進行連接的客戶端的地址      socklen_t client_addrlength = sizeof(client_address);      int connfd = accept(listenfd,(struct sockaddr*)&client_address,&client_addrlength);      if(connfd < 0)      {          printf("Fail to accept!n");          close(listenfd);      }            char buff[1024]; //數據接收緩沖區      fd_set read_fds;  //讀文件操作符      fd_set exception_fds; //異常文件操作符      FD_ZERO(&read_fds);      FD_ZERO(&exception_fds);            while(1)      {          memset(buff,0,sizeof(buff));          /*每次調用select之前都要重新在read_fds和exception_fds中設置文件描述符connfd,因為事件發生以后,文件描述符集合將被內核修改*/          FD_SET(connfd,&read_fds);          FD_SET(connfd,&exception_fds);                    ret = select(connfd+1,&read_fds,NULL,&exception_fds,NULL);          if(ret < 0)          {              printf("Fail to select!n");              return -1;          }                              if(FD_ISSET(connfd, &read_fds))          {              ret = recv(connfd,buff,sizeof(buff)-1,0);              if(ret <= 0)              {                  break;              }                            printf("get %d bytes of normal data: %s n",ret,buff);                        }          else if(FD_ISSET(connfd,&exception_fds)) //異常事件          {              ret = recv(connfd,buff,sizeof(buff)-1,MSG_OOB);              if(ret <= 0)              {                  break;              }                            printf("get %d bytes of exception data: %s n",ret,buff);          }                }            close(connfd);      close(listenfd);                  return 0;  }

用select來解決socket中的多客戶問題

上面提到過,,使用select以后最大的優勢是用戶可以在一個線程內同時處理多個socket的IO請求。在網絡編程中,當涉及到多客戶訪問服務器的情況,我們首先想到的辦法就是fork出多個進程來處理每個客戶連接。現在,我們同樣可以使用select來處理多客戶問題,而不用fork。

服務器端

#include <sys/types.h>   #include <sys/socket.h>   #include <stdio.h>   #include <netinet/in.h>   #include <sys/time.h>   #include <sys/ioctl.h>   #include <unistd.h>   #include <stdlib.h>    int main()   {       int server_sockfd, client_sockfd;       int server_len, client_len;       struct sockaddr_in server_address;       struct sockaddr_in client_address;       int result;       fd_set readfds, testfds;       server_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立服務器端socket       server_address.sin_family = AF_INET;       server_address.sin_addr.s_addr = htonl(INADDR_ANY);       server_address.sin_port = htons(8888);       server_len = sizeof(server_address);       bind(server_sockfd, (struct sockaddr *)&server_address, server_len);       listen(server_sockfd, 5); //監聽隊列最多容納5個       FD_ZERO(&readfds);       FD_SET(server_sockfd, &readfds);//將服務器端socket加入到集合中      while(1)       {          char ch;           int fd;           int nread;           testfds = readfds;//將需要監視的描述符集copy到select查詢隊列中,select會對其修改,所以一定要分開使用變量           printf("server waitingn");             /*無限期阻塞,并測試文件描述符變動 */          result = select(FD_SETSIZE, &testfds, (fd_set *)0,(fd_set *)0, (struct timeval *) 0); //FD_SETSIZE:系統默認的最大文件描述符          if(result < 1)           {               perror("server5");               exit(1);           }             /*掃描所有的文件描述符*/          for(fd = 0; fd < FD_SETSIZE; fd++)           {              /*找到相關文件描述符*/              if(FD_ISSET(fd,&testfds))               {                 /*判斷是否為服務器套接字,是則表示為客戶請求連接。*/                  if(fd == server_sockfd)                   {                       client_len = sizeof(client_address);                       client_sockfd = accept(server_sockfd,                       (struct sockaddr *)&client_address, &client_len);                       FD_SET(client_sockfd, &readfds);//將客戶端socket加入到集合中                      printf("adding client on fd %dn", client_sockfd);                   }                   /*客戶端socket中有數據請求時*/                  else                   {                       ioctl(fd, FIONREAD, &nread);//取得數據量交給nread                                            /*客戶數據請求完畢,關閉套接字,從集合中清除相應描述符 */                      if(nread == 0)                       {                           close(fd);                           FD_CLR(fd, &readfds); //去掉關閉的fd                          printf("removing client on fd %dn", fd);                       }                       /*處理客戶數據請求*/                      else                       {                           read(fd, &ch, 1);                           sleep(5);                           printf("serving client on fd %dn", fd);                           ch++;                           write(fd, &ch, 1);                       }                   }               }           }       }         return 0;  }

客戶端

//客戶端  #include <sys/types.h>   #include <sys/socket.h>   #include <stdio.h>   #include <netinet/in.h>   #include <arpa/inet.h>   #include <unistd.h>   #include <stdlib.h>  #include <sys/time.h>    int main()   {       int client_sockfd;       int len;       struct sockaddr_in address;//服務器端網絡地址結構體        int result;       char ch = 'A';       client_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立客戶端socket       address.sin_family = AF_INET;       address.sin_addr.s_addr = inet_addr("127.0.0.1");      address.sin_port = htons(8888);       len = sizeof(address);       result = connect(client_sockfd, (struct sockaddr *)&address, len);       if(result == -1)       {            perror("oops: client2");            exit(1);       }       //第一次讀寫      write(client_sockfd, &ch, 1);       read(client_sockfd, &ch, 1);       printf("the first time: char from server = %cn", ch);       sleep(5);            //第二次讀寫      write(client_sockfd, &ch, 1);       read(client_sockfd, &ch, 1);       printf("the second time: char from server = %cn", ch);            close(client_sockfd);            return 0;   }

運行流程:

客戶端:啟動->連接服務器->發送A->等待服務器回復->收到B->再發B給服務器->收到C->結束

服務器:啟動->select->收到A->發A+1回去->收到B->發B+1過去

測試:我們先運行服務器,再運行客戶端
select 機制的優勢介紹

select總結:

select本質上是通過設置或者檢查存放fd標志位的數據結構來進行下一步處理。這樣所帶來的缺點是:

1、單個進程可監視的fd數量被限制,即能監聽端口的大小有限。一般來說這個數目和系統內存關系很大,具體數目可以cat/proc/sys/fs/file-max察看。32位機默認是1024個。64位機默認是2048.

2、 對socket進行掃描時是線性掃描,即采用輪詢的方法,效率較低:當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。如果能給套接字注冊某個回調函數,當他們活躍時,自動完成相關操作,那就避免了輪詢,這正是epoll與kqueue做的。

3、需要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時復制開銷大。

? 版權聲明
THE END
喜歡就支持一下吧
點贊13 分享