I/O多路複用之select&poll

系統提供select函數用來實現I/O多路複用輸入/輸出模型。select系統調用是用來讓我們的程序監視多個文件描述狀態變化的。程序會停在select這裏等待,直到被監視的文件描述符有一個或多個發生狀態變化。通常I/O操作有兩個步驟,一個是等,另一個是數據搬遷。select主要是在等的這個狀態阻塞着直到事件發生。
頭文件:

#include<sys/select.h>
#include<sys/types.h>
#include<unistd.h>
#include<sys/time.h>

函數原型
int select(int nfds,fd_set *reads,fd_set *writefds,fd_set *exceptfds,struct timeval *timeout);

參數:
nfds:是需要監視的最大的文件描述符的值+1。
fd_set:
fd_set底層是用位圖實現的,每一個位都代表一個文件描述符。readfds,writefds,exceptfds分別對應於需要檢測的可讀文件描述符結合,可寫描述符集合,異常文件描述符集合,他們都是輸入輸出型參數。
當作爲輸入參數時:只要文件描述符集合中對應的位上爲1,就表示select需要監視這個描述符的狀態。比如readfds裏面的文件描述符就代表他們需要等待讀事件,writefds裏面的文件描述符就代表他們需要等待寫事件。
當作爲輸出參數時,只要文件描述符集合中對應的位上爲1,就代表他們等待的事件已經就緒,這是由內核設定的。

timeout:設置超時時間。
timeout裏面的成員設定爲特定的時間值:
如果在這段時間裏面沒有事件發生,select將超時返回。struct timeval結構用於描述一段時間長度,如果在這個時間內,需要監聽的描述符沒有事件發生則函數返回,返回值爲0。
struct timeval
{
long tv_sec; //秒
long tv_usec; //微秒
}
timeout裏面的成員等於0:表示非阻塞輪詢方式,不斷的去檢測描述符集合的狀態,然後立即返回。
timeout爲NULL:表示以阻塞的方式等待事件發生。

返回值:
成功的話,返回文件描述符狀態已改變的個數。如果返回0代表在描述符狀態改變之前已經超過timeout時間。如果有錯誤發生的話,則返回-1。

下面的宏用來處理描述符集合:
void FD_CLR(int fd,fd_set* set); 用來清除描述符詞組set中相關fd的位。
int FD_ISSET(int fd,fd_set* set); 用來測試set中相關fd的位是否爲真。
void FD_SET(int fd,fd_set* set); 用來設置描述詞組set中相關fd的位。
void FD_ZERO(fd_set *set);用來清除描述符詞組set的全部位。

select模型:
select可監控的描述符取決於sizeof(fd_set)的值,因爲文件描述符是用位圖表示的,所以能監控的描述符的最大數量是sizeof(fd_set)*8,fd_set的大小可以調整。
將fd加入select監控集的同時,還要使用一個額外的數組保存select監控集中的fd。一方面是用於在select返回後,array作爲源數據和fd_set進行FD_ISSET判斷事件是否就緒。另一方面是select返回之後會把以前加入的但並無事件發生fd清空,這是由內核清空的,所以每次開始select前都要重新從array中取得fd加入到fd_set中。
還有就是因爲select第一個參數是當前要監測的文件描述符的最大值加1,可以在掃描array的同時取得fd的最大值maxfd,用於select的第一個參數。
所以select的缺點就是,每次selcet之前都要遍歷數組加入fd,select返回後還要遍歷數組進行判斷哪些事件已經就緒(FD_ISSET判斷是否有事件發生)。

select的缺點:
1、每次調用select,都需要把fd集合從用戶態拷貝到內核態。這個開銷在fd很多的時候會很大。
2、select在返回之後,需要我們遍歷數組去查找事件就緒的描述符。這個過程的時間複雜度是O(N)。而epoll它查找就緒事件的時候是O(1)。
3、select支持的文件描述符的數量太小了,默認是1024。

總結:針對select的缺點來看,即時fd_set可以改動,也不建議將它改的很大,因爲一但支持的文件描述多了,效率自然也就降低了。

例:實現一個服務器,使用select讓服務器可以同時接受多個客戶的鏈接,並將客戶發送的數據打印出來。

#include<string.h>
#include<stdio.h>
#include<stdlib.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<sys/select.h>
#include<unistd.h>
#include<sys/time.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<assert.h>
#define SIZE 128

int startup(char *ip,int port)
{
    assert(ip);
    int sock=socket(AF_INET,SOCK_STREAM,0);
    if(sock<0)
    {
        perror("socket");
        exit(0);
    }
    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(1);
    }
    if(listen(sock,5)<0)
    {
        perror("listen");
        exit(2);
    }
    return sock;
}

int main(int argc,char* argv[])
{
    if(argc!=3)
    {
        printf("usae: %s [IP] [PORT]\n",argv[0]);
        return 0;
    }
    int lis_sock=startup(argv[1],atoi(argv[2]));
    int gfds[SIZE];
    memset(gfds,-1,SIZE*4);
    fd_set rfds;
    FD_ZERO(&rfds);
    while(1)
    {
        struct timeval timeout={5,0};
        gfds[0]=lis_sock;
        int max_fd=-1;
        int i=0;
        for(;i<SIZE;i++)
        {
            if(max_fd<gfds[i])
            {
                max_fd=gfds[i];
            }

            if(gfds[i]>=0)
            {
                FD_SET(gfds[i],&rfds);
            }

        }
        int ret=select(max_fd+1,&rfds,NULL,NULL,NULL);

        switch(ret)
        {
        case 0:
            printf("timeout...\n");
            break;
        case -1:
            printf("error");
            break;
        default:
            if(FD_ISSET(gfds[0],&rfds))
            {
                struct sockaddr_in peer;
                socklen_t len=sizeof(peer);
                int connfd=accept(lis_sock,(struct sockaddr*)&peer,&len);
                if(connfd<0)
                {
                    perror("accept");
                }
                else
                {
                    printf("client: %s:%d fd(%d)\n",inet_ntoa(peer.sin_addr),\
                            ntohs(peer.sin_port),connfd);                    
                    int k=0;
                    for(;k<SIZE;k++)
                    {
                        if(gfds[k]==-1)
                        {
                            gfds[k]=connfd;
                             break;
                         }
                    }
                    if(k>=SIZE)
                    {
                        close(connfd);
                        gfds[k]=-1;
                     }
                 }
            }

            int j=1;
            for(;j<SIZE;j++)
            {
                if(FD_ISSET(gfds[j],&rfds))
                {
                    char buf[SIZE];
                    ssize_t s=read(gfds[j],buf,sizeof(buf));
                    if(s<0)
                    {
                        perror("read");
                         continue;
                    }
                    else if(s==0)
                    {
                         printf("client is quit!\n");
                        close(gfds[j]);
                        gfds[j]=-1;
                    }
                    else
                    {
                         buf[s]=0;
                        printf("client# %s\n",buf);
                     }
                 }
            }
            break;
        }
    }
    return 0;
}

poll也是一種I/O多路轉接的方式,select將三種事件進行了區分,並且用三個位圖來表示不同的監測事件。而poll統一用一種結構來管理要監測的事件。

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

參數:
fds:
它是一個結構體數組,其中元素的類型如下:
struct pollfd{
int fd; //保存要監測的文件描述符,由用戶自己設定
int events; //保存要監測的事件,比如讀事件或寫事件,由用戶自己設定
short revents; //保存就緒事件,等到事件就緒,由內核設定
}
pollfd結構裏面包含了要監視的event和已經發生的event。同時pollfd並沒有數量的限制,但是因爲select和poll在返回之後,都需要輪詢來獲取就緒的描述符,所以當監視的文件描述符很多的時候,poll的性能也會下降。fds裏面的每一個元素都代表一個要監測的事件。

nfds:表示數組的長度,也就是要監測事件的個數。
timeout:設置超時時間。
返回值:成功的話返回就緒事件的個數。如果超時的話返回0,失敗的話返回-1。

poll支持的常見事件類型:
POLLIN:數據可讀
POLLOUT:數據可寫
POLLERR:錯誤
POLLRDHUP:TCP連接被對方關閉,或者對方關閉了寫操作,他由GNU引入。在使用的時候要加上#define _GNU_SOURCE。

``
**poll與select的比較:
1、select將讀寫異常的事件分開進行監測,poll將所有的事件類型都統一用一種結構體表示。
2、select的3個fd_set參數都是輸入輸出型參數,當輸入的時候表示要監測的文件描述符,當輸出的時候表示就緒的文件描述符,所以每一次select之前都要重新將要監測的文件描述符設置進fd_set中。而poll用events表示要監測的文件描述符,revents表示就緒的文件描述符,所以poll只需要設置一次就可以了。
3、當事件就緒的時候,select和poll都要遍歷整個所有監測的文件描述符來查找就緒的事件。**

發佈了123 篇原創文章 · 獲贊 15 · 訪問量 3萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章