select poll epoll使用示例

select,poll,epoll都是IO多路複用的機制。I/O多路複用就通過一種機制,可以監視多個描述符,一旦某個描述符就緒(一般是讀就緒或者寫就緒),能夠通知程序進行相應的讀寫操作。
這三個方法都要調用驅動中的struct file_operations中的.poll方法。poll是一個函數指針,申明如下
unsigned int (*poll) (struct file *, struct poll_table_struct *);
在驅動中poll的實現方法如下:
.....
static unsigned int Test_poll(struct file *filp, poll_table *wait)
{
        unsigned int mask = 0;
        ......... 
        poll_wait(filp, &(int_queue), wait);
        if(read ready)
        { 
                mask |=POLLIN | POLLRDNORM;
        }
        if(write ready)
        { 
                mask |=return POLLOUT | POLLWRNORM;
        }
        ............
        return  mask ;
}
static const struct file_operations Test_fops = {
        .owner = THIS_MODULE,
        .......
        .poll = Test_poll ,
        .......
};
注意:.poll本身是不會阻塞的,不論讀寫條件是否滿足,都會立即返回。阻塞是在調用 .poll的時候實現的,poll返回0的時候,將阻塞,等待int_queue。
int_queue是一個 類型爲wait_queue_head_t結構的等待隊列。
在驅動初始化的時候調用init_waitqueue_head(&(int_queue));初始化等待隊列
當讀寫條件滿足的時候,調用wake_up_interruptible(&( int_queue));喚醒等待隊列


poll方法返回的數據如下:
常量             說明
POLLIN           普通或優先級數據可讀
POLLRDNORM       普通數據可讀
POLLRDBAND       優先級數據可讀
POLLPRI          高優先級數據可讀
POLLOUT          普通數據可寫
POLLWRNORM       普通數據可寫
POLLWRBAND       優先級數據可寫
POLLERR          發生錯誤
POLLHUP          發生掛起
POLLNVAL         描述字不是一個打開的文件




1 select的實現示例
  select相關函數原型如下:
  
  int select(int maxfd,fd_set *rdset,fd_set *wrset,fd_set *exset,struct timeval *timeout);
  maxfd是需要監視的最大的文件描述符值+1,例如如果我們分別要監視3個文件描述符fd1 fd2 fd3 ,數值分別爲100,500,1000,那麼maxfd就必須寫1001。
  rdset,wrset,exset分別對應於需要檢測的可讀文件描述符的集合,可寫文件描述符的集 合及異常文件描述符的集合。
  timeout 爲設置的超時時間,如果爲NULL,則一直等待,直到監視的資源可用。
  函數返回值:小於0 函數執行出錯 ,超時將返回0 。大於0 表示有資源可用的數目,例如我們分別監視了fd1 fd2 fd3 那麼如果fd1 發的同時可以寫了則返回2。


  對fd_set的操作可以通過下面的宏來操作:
  FD_ZERO(fd_set*); 用來清空fd_set集合,即讓fd_set集合不再包含任何文件句柄。
  FD_SET(int ,fd_set *); 用來將一個給定的文件描述符加入fd_set集合之中
  FD_CLR(int ,fd_set*); 用來將一個給定的文件描述符從集合中刪除
  FD_ISSET(int ,fd_set*);檢測fd在fdset集合中是否存在,存在返回真,否則,返回假(也可以認爲集合中指定的文件描述符是否可以讀寫)。
  這裏需要注意的是select執行的時候,rdset、wrset、exset、timeout、會被改變,所以每次執行select前一定要重新設置下。例如rdset分別監視fd1 fd2 fd3,當fd3可以讀都,select返回,那麼這個時候rdset就只設置了fd3,如果我們沒有重新設置,那fd1 fd2就監視不到了。同樣timeout也是一樣,當超時返回後,timeout變爲0,如果這個時候沒有重新設置,那麼select函數就會立即返回了,無論是否資源可用。


  下面是示例代碼:
void * Test_select( void*  arg )
{
        fd_set fdsi;
        fd_set fdso;
        int maxfdp ;
        int ret;
        struct timeval timeout;
        int fd ,fd2;
        fd = open(DEV_FILENAME, O_RDWR  );
        if(fd<0) 
        {
                printf("open device error\r\n");
                return NULL ;
        }
        fd2 = open(DEV_FILENAME2, O_RDWR  );
        if(fd<0) 
        {
                printf("open device error\r\n");
        
                return NULL ;
        }
        maxfdp = fd>fd2?fd+1:fd2+1 ;
        while(exitFlag==0)
        {
                timeout.tv_sec=1;//1秒超時
                timeout.tv_usec = 0;
                FD_ZERO(&fdsi); 
                FD_SET(fd,&fdsi);
                FD_SET(fd2,&fdsi);
                
                FD_ZERO(&fdso); 
                FD_SET(fd,&fdso);
                FD_SET(fd2,&fdso); 
                
                ret = select(maxfdp,&fdsi,&fdso,NULL,&timeout);
                  //如果不需要超時可以改爲  ret = select(maxfdp,&fdsi,&fdso,NULL,NULL);
                if(ret==0)
                {
                        printf("select timeout\r\n");
                        continue;
                }
                else if(ret<0)
                {
                        printf("select error\r\n");
                        exitFlag= 1;
                }
                else
                {
             
                        if(FD_ISSET(fd,&fdsi)) 
                        {
                           //read data;
                        }
                        else if(FD_ISSET(fd,&fdso)) 
                        {
                           //write data;
                        }
                        
                        if(FD_ISSET(fd2,&fdsi)) 
                        {
                           //read data;
                        }
                        else if(FD_ISSET(fd2,&fdso)) 
                        {
                           //write data;
                        }
                }
        }
        close(fd );
        close(fd2 );
        return NULL;
}
說明:在系統內核中select是採用輪詢來處理的,如果需要監視的文件描述符越多,就需要消耗更多的資源。所以如果要監視很多文件描述符,最好使用epoll(epoll在後面會介紹)。
select監視的最大數量有限制,通常是1024。


2 poll的實現示例
  poll實現比select簡單,只有一個函數就可以了,函數原型如下
  int poll (struct pollfd *fds, nfds_t nfds, int timeout);
  參數fds :pollfd 結構體的指針,這個結構描述了需要監視的文件描述符和我們關心的事件。這個函數也可以監視多個文件描述符。
            struct pollfd結構描述如下
  struct pollfd
  {
                int fd;                     /* poll 的文件描述符.  */
                short int events;           /* fd 上感興趣的事件(需要等待的事件).  */
                short int revents;          /* fd 上實際發生的事件. 也就是poll返回時 的狀態*/
  }; 
  參數 nfds:fds的個數,也就是要監視的文件描述符的個數。
  參數 timeout : 超時的毫秒數,如果爲-1則表示永遠等待。
  函數返回值 返回0表示超時 返回值小於0函數出錯,大於0 實際可用的資源數


  下面是示例代碼:  
void * Test_poll( void*  arg )
{
        struct pollfd Events[2] ;
        int ret;
        int fd ,fd2;
        fd = open(DEV_FILENAME, O_RDWR  );
        if(fd<0) 
        {
                printf("open device error\r\n");
                return NULL ;
        }
        fd2 = open(DEV_FILENAME2, O_RDWR  );
        if(fd<0) 
        {
                printf("open device error\r\n");
                return NULL ;
        }
        Events[0].fd = fd;
        Events[0].events = POLLIN | POLLERR;     /*關心讀取和出錯事件*/
        
        Events[1].fd = fd2;
        Events[1].events = POLLIN | POLLERR;     /*關心讀取和出錯事件*/
        while(exitFlag==0)
        {
                ret = poll ((struct pollfd *)&Events[0], 2, -1);//如果要設置超時可以把-1改爲要設置的超時的毫秒數
                if(ret==0)
                {
                        printf("poll timeout\r\n");
                        continue;
                }
                else if(ret<0)
                {
                        printf("poll error\r\n");
                        exitFlag= 1;
                }
                else
                {
                        if (Events[0].revents & POLLERR) 
                        {
                                printf ("device error!\n");              
                        }
                        if (Events[1].revents & POLLERR) 
                        {
                                printf ("device error!\n");              
                        }
                                 
                        if (Events[0].revents & POLLIN) {
                                //read data;              
                        } 
                    
                        if (Events[1].revents & POLLIN) {
                                //read data;              
                        } 
                }
        }
        close(fd );
        close(fd2 );
        return NULL;
}
說明:poll函數的實現方法和select類似,所以等待的文件描述符越多,越消耗資源。


3 epoll實現說明
  epoll實現很簡單,就三個函數就解決了,下面是函數申明:
  int epoll_create(int size);
  這個函數的功能創建一個epoll的句柄,當創建好epoll句柄後,它就是會佔用一個fd值所以使用完epoll後一定要close掉,避免佔用資源。
  參數size:用來告訴內核這個監聽的數目一共有多少。 自從Linux 2.6.8開始,size參數被忽略,但是依然要大於0。
  返回值:成功返回epoll的句柄,失敗返回-1。


  int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
  參數 epfd: epoll的句柄,是epoll_create()的返回值
  參數 op:操作碼。用三個宏來表示的
           EPOLL_CTL_ADD:註冊新的fd到epfd中;
           EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;
           EPOLL_CTL_DEL:從epfd中刪除一個fd;
  參數 fd:要監視的文件描述符。
  參數event :要監視的事件的描述。
  struct epoll_event的結構說明如下:
  struct epoll_event {
          __uint32_t events; /* 需要監視的事件,如EPOLLIN、EPOLLOUT等等 */
          epoll_data_t data; /* 用戶數據*/
  }; 
  typedef union epoll_data
  {
          void        *ptr;
          int          fd;
          __uint32_t   u32;
          __uint64_t   u64;
  } epoll_data_t;
  返回值:0成功,小於0 失敗


  int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
  該函數等待事件產生。
  參數epfd: epoll的句柄
  參數events:從內核得到的事件集合
  參數maxevents:events的大小。events的大小取決於可能同時產生的事件的數量。例如我們雖然同時監視了fd1 fd2 fd3 三個文件描述符,但是這3個之間是不可能同時可讀或者可寫的,那麼將maxevents設置爲1也沒有問題,但是如果三個事件可能會同時產生,那麼最好就設置爲3。如果設置爲1,3個事件同時產生,epoll_wait第一次返回的時候就會只返回第一事件了。(不過在此調用這個函數的時候,會馬上返回。例如當fd1 fd2 fd3同時可讀,但是maxevents爲1時,那麼第一次調用epoll_wait返回值爲1,報告fd1的時間,第二次調用,馬上返回1,調用fd2的事件,第三次調用馬上返回1,報告fd3的事件.而如果maxevents爲3的話,當fd1 fd2 fd3同時可讀,epoll_wait返回值爲3,同時報告fd1 fd2 fd3的事件)。
  參數timeout:超時的毫秒數,如果爲-1表示一直等待直到事件產生。
  返回值:超時返回0 ,返回小於0 函數執行失敗,大於0,返回值爲實際的事件數目。


下面是示例代碼:
static int epoll_register( int  epoll_fd, int  fd )
{
        struct epoll_event  ev;
        int                 ret, flags;
        /*****************************************/ 
        flags = fcntl(fd, F_GETFL);
        fcntl(fd, F_SETFL, flags | O_NONBLOCK);
        //資料上說需要設置爲非阻塞的方式,我試了下,似乎不設置爲非阻塞的方式也不會出錯
        /**********************************************/
        ev.events  = EPOLLIN;
        ev.data.fd = fd; 
        ret = epoll_ctl( epoll_fd, EPOLL_CTL_ADD, fd, &ev );  
        return ret;
}
void * Test_epoll( void*  arg )
{
        int   epoll_fd  ,ret ;
        int count =0;
        struct epoll_event   events[2];
        int                  ne, nevents; 
        int fd ,fd2;
        fd = open(DEV_FILENAME, O_RDWR  );
        if(fd<0) 
        {
                printf("open device error\r\n");           
                return NULL ;
        }
        fd2 = open(DEV_FILENAME2, O_RDWR  );
        if(fd<0) 
        {
                printf("open device error\r\n");              
                return NULL ;
        } 
        epoll_fd = epoll_create(1);
        epoll_register( epoll_fd, fd );
        epoll_register( epoll_fd, fd2 );
        while(exitFlag==0)
        {
                nevents = epoll_wait( epoll_fd, events, 2, -1 );
                if (nevents < 0) 
                {
                        exitFlag =1;
                        printf("epoll_wait() unexpected error: %s", strerror(errno));
                }
                for (ne = 0; ne < nevents; ne++) 
                {
                        if ((events[ne].events & (EPOLLERR|EPOLLHUP)) != 0) 
                        {
                             
                        }
                        if ((events[ne].events & EPOLLIN) != 0) 
                        {
                               if( fd == events[ne].data.fd)
                               {
                                       //read data;
                               }
                               else if( fd2 == events[ne].data.fd)
                               {
                                       //read data;
                               }
                        }
                }
        }
        epoll_ctl( epoll_fd, EPOLL_CTL_DEL, fd, NULL ); 
        epoll_ctl( epoll_fd, EPOLL_CTL_DEL, fd2, NULL ); 
        ret=close(epoll_fd);
         
        return NULL;
}


最後總結:
(1)select,poll實現需要自己不斷輪詢所有fd集合,直到設備就緒,期間可能要睡眠和喚醒多次交替。而epoll其實也需要調用epoll_wait不斷輪詢就緒鏈表,期間也可能多次睡眠和喚醒交替,但是它是設備就緒時,調用回調函數,把就緒fd放入就緒鏈表中,並喚醒在epoll_wait中進入睡眠的進程。雖然都要睡眠和交替,但是select和poll在“醒着”的時候要遍歷整個fd集合,而epoll在“醒着”的時候只要判斷一下就緒鏈表是否爲空就行了,這節省了大量的CPU時間。這就是回調機制帶來的性能提升。
(2)select,poll每次調用都要把fd集合從用戶態往內核態拷貝一次,並且要把current往設備等待隊列中掛一次,而epoll只要一次拷貝,而且把current往等待隊列上掛也只掛一次(在epoll_wait的開始,注意這裏的等待隊列並不是設備等待隊列,只是一個epoll內部定義的等待隊列)。這也能節省不少的開銷。
所以最好儘量使用epoll。他的速度更快,系統開銷更小。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章