IO多路複用——select

IO多路複用編程

     是同步IO的一種,用一個進程一次等待多個IO就緒事件的發生,加大機率,儘量高效的等。數組

     適用場景服務器

  (1)當客戶處理多個描述字時(通常是交互式輸入和網絡套接口),必須使用I/O複用。網絡

  (2)當一個客戶同時處理多個套接口時,而這種狀況是可能的,但不多出現。多線程

  (3)若是一個TCP服務器既要處理監聽套接口,又要處理已鏈接套接口,通常也要用到I/O複用。併發

  (4)若是一個服務器即要處理TCP,又要處理UDP,通常要使用I/O複用。socket

  (5)若是一個服務器要處理多個服務或多個協議,通常要使用I/O複用。ide

  與多進程和多線程技術相比,I/O多路複用技術的最大優點是系統開銷小,系統沒必要建立進程/線程,也沒必要維護這些進程/線程,從而大大減少了系統的開銷。函數


select函數測試

該函數准許進程指示內核等待多個事件中的任何一個發送,並只在有一個或多個事件發生或經歷一段指定的時間後才喚醒。函數原型以下:

#include <sys/.h>
<sys/>
 ( maxfdp1,fd_set *readset,fd_set *writeset,fd_set *exceptset,返回值:就緒描述符的數目,超時返回0,出錯返回-1

函數參數介紹以下:

(1)第一個參數maxfdp1指定待測試的描述字個數,它的值是待測試的最大描述字加1(所以把該參數命名爲maxfdp1),描述字0、一、2...maxfdp1-1均將被測試。由於文件描述符是從0開始的。

(2)中間的三個參數readset、writeset和exceptset指定咱們要讓內核測試讀、寫和異常條件的描述字。若是對某一個的條件不感興趣,就能夠把它設爲空指針。struct fd_set能夠理解爲一個集合,這個集合中存放的是文件描述符,可經過如下四個宏進行設置:

          void FD_ZERO(fd_set *fdset);           //清空集合

          void FD_SET(int fd, fd_set *fdset);   //將一個給定的文件描述符加入集合之中

          void FD_CLR(int fd, fd_set *fdset);   //將一個給定的文件描述符從集合中刪除

          int FD_ISSET(int fd, fd_set *fdset);   // 檢查集合中指定的文件描述符是否能夠讀寫 

fd_set結構體是文件描述符集,該結構體其實是一個整型數組,數組中的每一個元素的每一位標記一個文件描述符。fd_set能容納的文件描述符 數量由FD_SETSIZE指定,通常狀況下,FD_SETSIZE等  於1024,這就限制了select能同時處理的文件描述符的總量。

(3)timeout告知內核等待所指定描述字中的任何一個就緒可花多少時間。其timeval結構用於指定這段時間的秒數和微秒數。

         struct timeval{

                   long tv_sec;   //seconds

                   long tv_usec;  //microseconds

       };

這個參數有三種可能:

    1)永遠等待下去:僅在有一個描述字準備好I/O時才返回。爲此,把該參數設置爲空指針NULL。

    2)等待一段固定時間:在有一個描述字準備好I/O時返回,可是不超過由該參數所指向的timeval結構中指定的秒數和微秒數。

    3)根本不等待:檢查描述字後當即返回,這稱爲輪詢。爲此,該參數必須指向一個timeval結構,並且其中的定時器值必須爲0。

(4)返回值狀況: 
a)超時時間內,若是文件描述符就緒,select返回就緒的文件描述符總數(包括可讀、可寫和異常),若是沒有文件描述符就緒,select返回0; 
b)select調用失敗時,返回 -1並設置errno,若是收到信號,select返回 -1並設置errno爲EINTR。

(5)文件描述符的就緒條件: 
在網絡編程中, 
1)下列狀況下socket可讀: 
a) socket內核接收緩衝區的字節數大於或等於其低水位標記SO_RCVLOWAT; 
b) socket通訊的對方關閉鏈接,此時該socket可讀,可是一旦讀該socket,會當即返回0(能夠用這個方法判斷client端是否斷開鏈接); 
c) 監聽socket上有新的鏈接請求; 
d) socket上有未處理的錯誤。 
2)下列狀況下socket可寫: 
a) socket內核發送緩衝區的可用字節數大於或等於其低水位標記SO_SNDLOWAT; 
b) socket的讀端關閉,此時該socket可寫,一旦對該socket進行操做,該進程會收到SIGPIPE信號; 
c) socket使用connect鏈接成功以後; 
d) socket上有未處理的錯誤。


selelct原理圖

wKiom1ee582T0F4_AAGgKoA3mDk565.png

說明:

   一、select只負責等待IO,不負責對IO進行操做,由recv/send等函數進行

   二、select一共有兩次系統調用:1)select系統調用 2)recvfrom系統調用

select原理概述

調用select時,會發生如下事情:

  1. 從用戶空間拷貝fd_set到內核空間;

  2. 註冊回調函數__pollwait;

  3. 遍歷全部fd,對所有指定設備作一次poll(這裏的poll是一個文件操做,它有兩個參數,一個是文件fd自己,一個是當設備還沒有就緒時調用的回調函數__pollwait,這個函數把設備本身特有的等待隊列傳給內核,讓內核把當前的進程掛載到其中);

  4. 當設備就緒時,設備就會喚醒在本身特有等待隊列中的【全部】節點,因而當前進程就獲取到了完成的信號。poll文件操做返回的是一組標準的掩碼,其中的各個位指示當前的不一樣的就緒狀態(全0爲沒有任何事件觸發),根據mask可對fd_set賦值;

  5. 若是全部設備返回的掩碼都沒有顯示任何的事件觸發,就去掉回調函數的函數指針,進入有限時的睡眠狀態,再恢復和不斷作poll,再做有限時的睡眠,直到其中一個設備有事件觸發爲止。

  6. 只要有事件觸發,系統調用返回,將fd_set從內核空間拷貝到用戶空間,回到用戶態,用戶就能夠對相關的fd做進一步的讀或者寫操做了。

select優勢

  select模型是Windows sockets中最多見的IO模型。它利用select函數實現IO 管理。經過對select函數的調用,應用程序能夠判斷套接字是否存在數據、可否向該套接字寫入據。

    如:在調用recv函數以前,先調用select函數,若是系統沒有可讀數據那麼select函數就會阻塞在這裏。當系統存在可讀或可寫數據時,select函數返回,就能夠調用recv函數接   收數據了。

   能夠看出使用select模型,須要兩次調用函數。第一次調用select函數第二次socket API。使用該模式的好處是:能夠等待多個套接字。

select缺點

  1. 最大併發數限制:使用32個整數的32位,即32*32=1024來標識fd;

  2. 效率低:每次都會線性掃描整個fd_set,集合越大速度越慢;

  3. 內核/用戶空間內存拷貝問題。



select實現TCP服務器wKiom1ee54uSuX-fAAKAHYfIoGU521.png

代碼:

#include <stdio.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <sys/select.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
int fds[64];
const fds_nums=sizeof(fds)/sizeof(fds[0]);
static int startup(const char *ip,int port)
{
  int sock=socket(AF_INET,SOCK_STREAM,0);
  if(sock<0)
  {
    perror("socket");
    exit(2);
  }
  int opt = 1;
  setsockopt(sock, SOL_SOCKET, SO_REUSEADDR, &opt, sizeof(opt));
  struct sockaddr_in local;
  local.sin_family=AF_INET;
  local.sin_port=htons(port);
  local.sin_addr.s_addr=inet_addr(ip);
  if(bind(sock,(struct sockaddr *)&local,sizeof(local))<0)
  {
    perror("bind");
    exit(3);
  }
  if(listen(sock,5)<0)
  {
    perror("listen");
    exit(4);
  }
  return sock;
}
static void usage(const char *proc)
{
  printf("%s [ip] [port]\n",proc);
}
int main(int argc,char *argv[])
{
  if(argc!=3)
  {
    usage(argv[0]);
    exit(1);
  }
  int listen_sock=startup(argv[1],atoi(argv[2]));
  fd_set rset;
  int i=0;
  FD_ZERO(&rset);
  FD_SET(listen_sock,&rset);
  //initial fds
  for(;i<fds_nums;i++)
  {
      fds[i]= -1;
  }
  fds[0]=listen_sock;
  int done=0;
  while(!done)
  {
    //reset current rset
    int max_fd= -1;
    for(i=0;i<fds_nums;i++)
    {
      if(fds[i]>0)
      {
        FD_SET(fds[i],&rset);
        max_fd=max_fd<fds[i]?fds[i]:max_fd;
      }
    }
    //struct timeval _ti={5,0};
    switch(select(max_fd+1,&rset,NULL,NULL,NULL))
    {
      case 0:
        printf("time out...\n");
        break;
      case -1:
        perror("select");
        break;
      default:
          for(i=0;i<fds_nums;i++)
          {
            //listen_fd
            if(i==0&&FD_ISSET(listen_sock,&rset))
            {
                //printf("there\n");
                struct sockaddr_in peer;
                socklen_t len=sizeof(peer);
                int newfd=accept(listen_sock,(struct sockaddr *)&peer,&len);
                if(newfd>0)
                {
                  printf("get a new client$ socket->%s:%d\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port));
                }
                int j=0;
                for(j;j<fds_nums;j++)
                {
                  if(fds[j]== -1)
                  {
                    fds[j]=newfd;
                    break;
                  }
                }
                //mfull of queue
                if(j==fds_nums)
                {
                  close(newfd);
                }
            }  
            else//normal accept_fd
            {
             // printf("there\n");
              if(FD_ISSET(fds[i],&rset))
              {
               
                char buf[1024];
                memset(buf,'\0',sizeof(buf));
                ssize_t _s=read(fds[i],buf,sizeof(buf)-1);
                if(_s>0)
                {
                  buf[_s-1]='\0';
                  printf("client$ %s\n",buf);
                }
                else if(_s==0)
                {
                  printf("%d is read done..\n",fds[i]);
                  close(fds[i]);
                  fds[i]= -1;
                }
                else{
                  perror("read");
                }
                
              }
            }
          }
        break;
    }
  }
  return 0;
}

結果:wKiom1ee62mBjD5GAAEn4YXFzms772.png