I/O多路轉接之select

系統提供select函數來實現多路複用輸入/輸出模型。select系統調用時用來讓我們程序監視多個文件句柄的狀態變化的。程序會停在select這裏等待,直到被監視的文件句柄有一個或多個發生了狀態改變。關於文件句柄,其實就是一個整數,我們最熟悉的文件句柄是0,1,2三個,0是標準輸入,1是標準輸出,2是標準錯誤輸出。


函數原型:

wKiom1eq91uBHwK8AABI0Zm_eOU219.png

參數:

nfds 輸入型參數   readfds,writefds,exceptfds,timeout即爲輸入型參數又爲輸出型參數

nfds:最大文件描述符+1;

readfds:讀文件符集。

         調用時:當文件描述符添加到readfds,只關心讀。

         返回:關心的很多讀事件,哪個讀事件已經就緒。

writefds:寫文件符集

         調用時:當文件描述符添加到writefds,只關心寫

         返回:關心的很多寫事件,哪個寫事件已經就緒。

exceptfds:異常文件符集。

timeout:所等待的時間。

若timeout爲NULL時,阻塞,不進行任何返回直到有事件發生。

若timeout爲0時,非阻塞,立即返回。

若timeout>0時,在指定的timeout時間內,沒有事件發生,則超時返回。


返回值:

若爲0表示超時

若爲-1,出錯

若>0,表示就緒事件的個數


FD_CLR(int fd,fd_set* set);刪除文件描述符集set中的fd
FD_ISSET(int fd,fd_set *set);判斷fd是否在set中
FD_SET(int fd,fd_set*set);將fd加到set中
FD_ZERO(fd_set *set);將set置爲0


理解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前循環array(加fd,取maxfd),select返回後循環array(FD_ISSET判斷是否有事件發生)。

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <string.h>
#include <sys/socket.h>
#include <sys/types.h>
#include <netinet/in.h>
#include <arpa/inet.h>
int fds[64];
const fds_num = 64;
void usage(const char* _proc)
{
    printf("Usage:%s [ip] [port]\n",_proc);
}
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)); //設置在TIME_WAIT時,端口號還可以用
    struct sockaddr_in local;  //設置本地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) //監聽套接字   5表示的爲連接隊列的長度  表示最多同時不能超過5個連接
    {
        perror("listen");
        exit(4);
    }
    return sock;
}
int main(int argc,char* argv[])
{
    if(argc != 3)
    {
        usage(argv[0]);
        exit(1);
    }
    int i = 0;
    for(;i<64;++i)
    {
        fds[i] = -1;
    }
    int listen_sock = startup(argv[1],atoi(argv[2]));  //監聽套接字
    fd_set rset;  //文件符集
    FD_ZERO(&rset); //清空文件符集
    FD_SET(listen_sock,&rset); //將套接字加入文件符集
    fds[0] = listen_sock;
    int done = 0;
    while(!done)
    {
        int max_fd = -1;
        for(i=0;i<fds_num;++i)
        {
            if(fds[i] > 0 )  //將讀事件加到文件符集
            {
                FD_SET(fds[i],&rset);
                max_fd = max_fd<fds[i]?fds[i]:max_fd; //記錄最大的文件符
            }
        }
    struct timeval timeout = {0,0};
    switch(select(max_fd+1,&rset,NULL,NULL,NULL))    //select返回就緒讀事件
    {
        case 0:    //返回值爲0,超時
            printf("timeout\n");
            break;
        case -1: //錯誤
            printf("select\n");
            break;
        default: //返回的爲就緒讀事件的個數
            {
                for(i=0;i<fds_num;++i)
                {
                    if(i==0 && FD_ISSET(listen_sock,&rset)) //如果就緒讀事件爲監聽套接字,且在文件符集中
                    {
                        struct sockaddr_in peer;
                        socklen_t len = sizeof(peer);
                        int new_fd = accept(listen_sock, (struct sockaddr*)&peer,&len); 
                        //指定服務器連接客戶端,返回客戶端的標識符(新的文件描述符)
                        if(new_fd > 0)
                        {
                            printf("get a new client:socket->%s:%d\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port));
                            int j = 0;
                            for(j=0;j<fds_num;j++)
                            {
                                if(fds[j] == -1)
                                {
                                    fds[j] = new_fd;
                                    break;
                                }
                            }
                            if(j == fds_num)
                            {
                                close(new_fd);
                            }
                        }
                    }
                    else
                    {
                        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)
                            {
                                printf("client# %s\n",buf);
                            }else if(_s == 0)   //讀到文件末尾
                            {
                                printf("client close..\n");
                                close(fds[i]);
                                fds[i] = -1;
                            }else
                            {
                                perror("read");
                            }
                        }
                    }
                }
                break;
            }
    }
    }
    return 0;
}

測試結果:

wKiom1eq_ozDWj7-AADEDnQVi0k580.png


select模型的優點:


優點一:在調用select模型時,若沒有事件發生,則會阻塞直到有事件就緒。

優點二:一次可以等待多個套接字。


select模型的缺點:


select在執行之前必須先循環添加要監聽的文件描述符到fds集合中,所以

缺點一:每次調用select都需要把fds集合從用戶態拷貝到內核態,這個開銷在fds很多時開銷很大。

缺點二:每次調用select都需要在內核遍歷所有傳遞進來的fds,判斷是不是我關心的事件,這個開銷在fd很多時,開銷也很大。

缺點三:這個由系統內核決定了,支持的文件描述符的默認值只有1024,想想應用到稍微大一點的服務器就不夠用了。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章