1. 程式人生 > >I/O複用之select、poll、epoll函式

I/O複用之select、poll、epoll函式

為了提高程式處理效率和機制,經常需要一個程式可以達到監聽甚至處理多個檔案描述符的效能,為了帶到這種機制我們需要借用I/O複用來實現。I/O複用雖然可以同時處理多個檔案,但是它本身是阻塞的。就是當檔案有多個就緒的時候程式檢測到了才會繼續往下執行,而且在執行的時候如果沒有家外界措施他就會按照就緒的順序執行,如果要實現並行的處理,可以通過程序或者執行緒來實現,在Linux下面如果要實現I/O複用,主要靠select、poll、epoll三個函式來實現,在這裡我們業主要針對這三個函式進行描述。這三個系統呼叫較為複雜,概念性較強需要大家仔細閱讀他們的說明,下面我們來看看那這三個函式的具體實現:

1、select系統呼叫:

這個函式的作用是在一段時間內,監聽使用者就緒的檔案描述符上的可讀、可寫、異常這些事件。函式定義如下:

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

接下來我們先從前到後的對select函式的引數進行講解,首先nfds是整形型別的檔案描述符,就是客戶需要被堅挺的檔案描述符,可以使檔案描述符集合,當時通常在使用的時候我們要給檔案描述符的總數nfds加1,因為在這裡檔案描述符集合的下表是從0開始的;其次是readfds、writefds、excesfds三個引數分別是表示可讀、可寫、異常事件,他們三個是fd_set結構體指標型別。fd_set結構體定義如下:

#include<typesizes.h>
#define __FD_SETSIZE 1024
 #include<sys/select.h>
 #define FD_SETSIZE __FD_SETSIZE
 typedef long int __fd_mask;
 #undef __NFDBITS
 #define __NFDBITS
 typedef struct
 {
     #ifdef __USE_XOPEN
        __fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
    #define __FD_BITS(set) ((set)->fds_bits)
#else __fd_mask __fds_bits[__FD_SETSIZE / __NFDBITS]; #define __FD_BITS(set) ((set)->__fds_bits) #endif }fd_set;

fd_set是一個僅包含一個整形陣列的結構體,每個元素的每一位(bit)就可以標記一個檔案描述符。fd_set能容納的檔案描述符數量由FD_SETSIZE指定,有限制。
fd_set結構體定義的陣列中可以由四個函式來控制,通過這四個函式可以實現對結構體陣列的置位、清位、將所有位置零、測試某一位是否被設定:

#include<sys/select.h>
FD_ZERO(fd_set *fdset);//清除fdset的所有位
FD_SET(int fd, fd_set *fdset);//設定fdset的位fd
FD_CLR(int fd, fd_set* fdset);//清除fdset的位fd
int FD_ISSET(int fd, fd_set* fdset);//檢測fdset的位fd是否被設定

最後一個引數timeout是用來設定select函式的超時時間,就是設定在檔案描述符沒有準備好等待的時間,因為timeout是結構體變數,它的成員有秒和微秒,所以設定的時間較為精確,設定的時間如果時間到了檔案描述符還沒有事件就緒,它就會不再等待直接返回,如果設定為0那麼將不會阻塞且會立即返回,設定為NULL或者-1將會一直阻塞,知道檔案描述符時間就緒才會返回,這也是我們經常用到的地方。下面我們來看看這個檔案時間結構體的定義:

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

select成功呼叫後會返回就緒的檔案描述符的個數,沒有準備好的檔案描述符如果在設定的時間或者立即返回的情況下會返回0,呼叫失敗就會返回-1並設定errno的值,如果在select函式阻塞的期間接收到訊號,也會立即返回-1設定errno的值為EINTR。
下面我來看看程式碼的實現:

#include<iostream>                                                                                                                      
#include<sys/types.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#include<stdio.h>
#include<errno.h>
#include<unistd.h>
#include<stdlib.h>
#include<fcntl.h>
#include<sys/socket.h>
#include<string.h>
using namespace std;

#define MAX_CLIENT_SIZE 5
#define MAX_BUFFER_SIZE 1024

int main(int argc, char* argv[])
 {
     if(argc <= 2)
     {
         cout<<"usage: %s ip_address port_number"<<basename( argv[0] )<<endl;
         return 1;
     }
     char* ip = argv[1];
     int port = atoi( argv[2] ); 
     int sockser = socket( AF_INET, SOCK_STREAM, 0);
     assert( sockser >= 0);

     struct sockaddr_in address,client_addr;
     address.sin_family = AF_INET;
     address.sin_port = htons(port);
     inet_pton( AF_INET, ip, &address.sin_addr);
     //address.sin_addr.s_addr = inet_addr(ip ); 
     int ret = 0;
     fd_set readfds;
     int conn_num = 0;
     int max_sock = sockser;
    int conn_clientfd[MAX_CLIENT_SIZE] = {0};
     cout<<"max_sock = "<<max_sock<<endl;

     ret = bind(sockser, (struct sockaddr*)&address, sizeof(address));
     assert( ret != -1);

     ret = listen(sockser, 5);
     assert( ret != -1);
     char buf[MAX_BUFFER_SIZE];
     socklen_t client_addrlength = sizeof( client_addr );
     while(1)
     {
             FD_ZERO(&readfds);
             FD_SET(sockser, &readfds);
             for(int i=0; i<conn_num; ++i )//輪詢檢視事件就緒檔案描述符
             {
                 if( conn_clientfd[i]!= 0)
                     FD_SET(conn_clientfd[i], &readfds);
            }
             ret = select( max_sock+2, &readfds, NULL, NULL, NULL);
             assert( ret > 0 );
            if( conn_num < MAX_CLIENT_SIZE )
            {
                  conn_clientfd[conn_num++] = conn;
                  if(sockser > max_sock)
                      max_sock = sockser;
            }
            else
            {
                  char *str = "server over load";
                  cout<<str<<endl;
                  send(conn , str, strlen(str)+1, 0);
            }
               continue;
         }
         for(int i=0; i< conn_num; ++i)
         {
                if( FD_ISSET(conn_clientfd[i], &readfds))
                {
                    ret = recv(conn_clientfd[i], buf, MAX_BUFFER_SIZE, 0);
                    printf("from %d client maseg,maseg size is  %d Byte", i, ret);
                     send(conn_clientfd[i], buf, strlen(buf)+1, 0);
                }
                else
                 conn_num --;

             }
         //continue;

        }
         close(sockser);
         return 0;
     }                                                                                                                                       

2、poll系統呼叫:

poll函式也是系統呼叫和select類似,需要輪詢一定數量的檔案描述符,以測試其中是否有事件就緒,函式定義如下:

#include<poll.h>
int poll(struct pollfd* fds, nfds_t nfds, int timeout);

1)fds 是struct pollfd型別的陣列,它可以檢測設定的檔案描述符上發生的可讀、可寫、異常事件等。pollfd結構體的定義如下:

struct pollfd
{
    int fd;//檔案描述符
    short events;//註冊的事件
    short revents;//檔案描述符實際發生的事件,由核心填充
};

poll和select函式一樣,如果有事件發生,需要輪詢產看revents來檢視具體是哪個描述符就緒,呼叫成功返回檔案符事件就緒的總個數,如果呼叫失敗就返回-1,並設定error。poll支援的事件有:
這裡寫圖片描述
這裡寫圖片描述
上圖這些事件都是events成員可以檢測的事件,同時events可以告訴poll函式檢測的是fd上面的那些事件,它是一些列時間的按位或,事件的發生會有核心修改成員revents,已通知程式fd上發生了那些事。
其中POLLRDNORM、POLLRDBAND、POLLWRNORM、POLLWRBAND由XOPEN規範定義。但他們實際上是將POLLIN和POLLOUT事件分的更精細,以區別對普通資料和有線資料。但Linux並不完全支援通它們。
通常我們寫程式是通過recv呼叫返回值區分socket上接受的是有效資料還是對方關閉連線的請求,並做出處理,但是現在Linux核心2.6.17開始,GNU為poll系統呼叫增加了一個專門檢測對方關閉連線的請求好出發的事件,就是POLLRDHUP事件。
2)nfds是nfds_t定義的引數,它的功能主要是指定被監聽書劍幾個fd的大小,nfds_t的定義如下:
typedef unsigned long int nfds_t;
3)timeout 是指定函式poll的超時值,單位是毫秒。如果timeout設定-1,poll永遠等待、阻塞,知道事件發生;設定為0就會立即返回。下面我們看看具體poll函式在程式中的使用,程式碼實現:

#include<iostream>
#include<unistd.h>
#include<string.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdio.h>
#include<stdlib.h>
#include<errno.h>
#include<assert.h>
#include<poll.h>
using namespace std;
#define MAX_CLIENT_SIZE 5
int main()
{
     int sockser = socket(AF_INET, SOCK_STREAM, 0);
     assert(sockser >= 0);     
     struct sockaddr_in address,client_addr;
     address.sin_family = AF_INET;
     address.sin_port = htons( 9090 );
     inet_pton( AF_INET, "127.0.0.1", &address.sin_addr );

     int ret = 0;
     ret = bind( sockser, (struct sockaddr*)&address, sizeof(address));
      assert(ret != -1);
     ret = listen( sockser, 5);
     assert( ret != -1); 
      struct pollfd client_fds[MAX_CLIENT_SIZE+1];
      for(int i=1; i<MAX_CLIENT_SIZE; ++i)                                                                                                                                   
      {
          client_fds[i].fd = -1;
          client_fds[i].events = 0;
      }
      nfds_t nfds = 0;
      client_fds[0].fd = sockser;
      client_fds[0].events = POLLIN;
      socklen_t client_addrlength = sizeof(client_addr);
      char buf[256];
      while(1)
      {
          ret = poll(client_fds, nfds+1, -1);
          if(ret < 0)
          {
              cout<<"poll fail"<<endl;
              return 1;
          }
          if(client_fds[0].revents & POLLIN)
          {
              int sockconn = accept( client_fds[0].fd, (struct sockaddr*)&client_addr, &client_addrlength);
              if(sockconn < 0)
              {
                  cout<<"accept fail"<<endl;
                  continue;
              }
              cout<<"a nwe client come"<<endl;
              for(int i=1; i< MAX_CLIENT_SIZE+1; ++i)
              {
                  if(i > MAX_CLIENT_SIZE)
                  {
                      char *str = "server overload";
                      cout<<str<<endl;
                      send(sockconn, str, strlen(str)+1, 0);
                      return 1;
                  }
                  else  if(client_fds[i].fd == -1)
                  {
                      client_fds[i].fd = sockconn;
                      client_fds[i].events = POLLIN;
                      nfds ++;
                      break;
                  }
              }
              continue;
          }                                                                                                                                                                  
          for(int i=1; i<MAX_CLIENT_SIZE; ++i)
          {
              if(client_fds[i].revents & POLLIN)
              {
                 ret =  recv(client_fds[i].fd, buf, 256, 0);
                  cout<<"from "<<i<<"client maseg,the maseg size is"<<ret<<"Bite:>"<<buf<<endl;
                  send(client_fds[i].fd, buf, strlen(buf), 0);
              }
          }

      }
      close(sockser);
      return 0;
 }                                                                                                                                                                           



3、epoll系統呼叫:

它是Linux特有的I/O服用函式。它在實現和使用上與select、poll有很大差異。第一epoll使用一組函式來完成任務,而不是單個函式。其次,epoll把使用者關心的檔案描述符上的事件放在核心裡的一個事件表裡,不想兩個系統呼叫每次呼叫都需要重新傳入檔案描述符或事件集。最後epoll系統呼叫有一個藉口函式,就是用來監測事件表中的檔案描述符集中檔案描述符是否有事件發生,返回值和前兩個系統呼叫的意義一樣。下面我們來看看這三個函式的原型和使用方法。
1)epoll需要一個額外的檔案描述符來標示這個事件表,事件表的建立:

#include<sys/epoll.h>
int epoll_create(int size);

引數size只是告訴核心這個事件表需要多大的空間而已,由該函式返回的檔案描述符是epoll系統呼叫的第一個引數。
2)我們來看看epoll核心事件表的操作函式,epoll_ctl:

#include<sys/epoll.h>
int epol_ctl(int epollfd, int op, int fd, struct epoll_event *event);

這個函式的作用是對使用者關心的檔案描述符和設定好該檔案描述符的被監測事件一併新增進事件表中,為下面epoll系統呼叫的介面epoll_wait函式的檢測做鋪墊,這裡的第一個引數epollfd就是在程式前面設定的事件表描述符;引數op是制定對事件表的操作型別,操作型別有三種:
EPOLL_CAT_ADD,往事件表中註冊fd上的事件。
EPOLL_CTL_MOD,修改fd上註冊事件。
EPOLL——CTL_DEL,刪除fd檔案描述符的註冊事件。
fd引數就是要注測進事件表中的檔案描述符。events是引數為了指定檔案描述符fd上的事件設定的,它是一個結構體epoll_event型別的,該型別有兩個成員,我們來詳細看一下epoll_event結構體:

struct epoll_event
{
    __unit32_t events;//
    epoll_data_t data;//
};

events成員是epoll事件型別,epoll支援的事件和poll系統呼叫大部分相同,只是在poll對應事件前面加’E’,epoll比poll多了兩個額外的事件型別:EPOLLET和EPOLLONESHOT,對於epoll的高校運作非常關鍵;data成員適用於存放使用者資料的,它是一個聯合體類行的變數,我們也一樣來看看epoll_data_t這個型別的原型:

typedef union epoll_data
{
    void *ptr;
    int fd;
    uint32_T u32;
    uint64_t u64;
}epoll_data_t;

在這四個成員中用的最多的是fd,它是指定事件所有目標的檔案描述符。ptr成員成員可用來指定與fd相關的使用者資料。由於是聯合體,我們不能同時使用其ptr成員和fd成員,因此,如果將檔案描述符和使用者資料關聯起來,以實現快速的資料訪問,只能使用其他手段,比如放棄使用epoll_data_t的fd成員,而在ptr指定的使用者資料中包含fd。
—-epoll_ctl這個函式呼叫成功返回0,失敗則返回-1,並設定errno。
3)epoll_wait函式:
它是epoll系統呼叫最後一步,也是比較關鍵的一步,這裡開始對事件表中檔案描述符集設定的事件進行檢測工作,其原型如下:

#include<sys/epoll.h>
int epoll_wait(int epollfd, struct epoll_event* events, int maxevents, int timeout);

第一個引數就是我們申請的時間表空間識別符號,第二個是我們申請用來存放事件發生檔案描述符的陣列,這也就是epoll系統呼叫和select、poll不同的地方,它是把檢測到事件發生的文間描述符單獨的放在這個陣列中,不用像前兩個系統呼叫那樣,雖然能檢測到檔案描述符集中事件發生,但是沒法確定是那個或者那幾個檔案描述符事件發生了,需要輪詢檢視,epoll系統呼叫只需對這裡的events集合裡左右成員操作即可。下面我們來看看具體程式碼實現:

 #include<iostream>
 #include<stdio.h>
 #include<unistd.h>
 #include<stdlib.h>
 #include<string.h>
 #include<sys/socket.h>
 #include<arpa/inet.h>
 #include<netinet/in.h>
 #include<assert.h>
 #include<sys/epoll.h>
 using namespace std;                                                                                                                                                                             
 #define MAX_BUFFER_SIZE 256
 #define MAX_EVENT_SIZE 1024

 void add_fd(int epollfd, int fd, int stat)
 {
     struct epoll_event ev;
     ev.events = stat;//設定該檔案描述符的被監測事件
     ev.data.fd = fd;
     epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &ev);
 }
 int main()
 {
     int sockser = socket(AF_INET, SOCK_STREAM, 0);
     struct sockaddr_in address, clientaddr;
     address.sin_family = AF_INET;
     address.sin_port = htons(9090);
     inet_pton( PF_INET, "127.0.0.1", &address.sin_addr);
     int ret = 0;
     ret = bind(sockser, (struct sockaddr*)&address, sizeof(address));
     assert(ret != -1);
     ret = listen(sockser, 5);
     assert( ret !=  -1 );
     int epoll_fd = epoll_create(5);
     add_fd(epoll_fd, sockser, EPOLLIN);//將伺服器放在事件表中,並設定關注事件
     struct epoll_event events[MAX_EVENT_SIZE];//單獨存放發生時間的集合
     char buf[MAX_BUFFER_SIZE];
     socklen_t client_addrlength = sizeof( clientaddr );
     while(1)
     {
         int ret = epoll_wait(epoll_fd, events, MAX_EVENT_SIZE, -1);
         if(ret < 0)
         {
             perror("epoll:> ");
             close( sockser );
             return 1;
         }
         else
         {
             for(int i=0; i< ret; ++i)
             {
                 int fd = events[i].data.fd;
                 if( (sockser == fd)&& (events[i].events & EPOLLIN))
                 {
                     int sockconn = accept(sockser, (struct sockaddr*)&clientaddr, &client_addrlength);
                     assert(sockconn != -1);
                     add_fd(epoll_fd, sockconn, EPOLLIN);

                 }
                 else if(events[i].events & EPOLLIN)
                 {
                     int res = recv(events[i].data.fd, buf, MAX_BUFFER_SIZE, 0);
                     printf("from %d number client masege,the size of maseg is %d byte %s:>",i,res,buf);
                     send(events[i].data.fd, buf, strlen(buf)+1, 0);
                 }
             }
         }

     }
     printf("sever over!\n");
     close(sockser);
     return 0;
 }                                                                                                                                                                                                                                                                                            

上面所有說明是本人再學習I/O複用時候對三個系統呼叫的理解,歡迎大家留言討論select、poll、epoll三個函式的各種屬性。