Linux編程之select

来源:http://www.cnblogs.com/skyfsm/archive/2017/06/26/7079458.html
-Advertisement-
Play Games

select系統調用的的用途是:在一段指定的時間內,監聽用戶感興趣的文件描述符上可讀、可寫和異常等事件。 select 機制的優勢 為什麼會出現select模型? 先看一下下麵的這句代碼: 這是用來接收數據的,在預設的阻塞模式下的套接字里,recv會阻塞在那裡,直到套接字連接上有數據可讀,把數據讀到 ...


select系統調用的的用途是:在一段指定的時間內,監聽用戶感興趣的文件描述符上可讀、可寫和異常等事件。

select 機制的優勢

為什麼會出現select模型?

先看一下下麵的這句代碼:

int iResult = recv(s, buffer,1024);

這是用來接收數據的,在預設的阻塞模式下的套接字里,recv會阻塞在那裡,直到套接字連接上有數據可讀,把數據讀到buffer里後recv函數才會返回,不然就會一直阻塞在那裡。在單線程的程式里出現這種情況會導致主線程(單線程程式里只有一個預設的主線程)被阻塞,這樣整個程式被鎖死在這裡,如果永 遠沒數據發送過來,那麼程式就會被永遠鎖死。這個問題可以用多線程解決,但是在有多個套接字連接的情況下,這不是一個好的選擇,擴展性很差。

再看代碼:

int iResult = ioctlsocket(s, FIOBIO, (unsigned long *)&ul);
iResult = recv(s, buffer,1024);

這一次recv的調用不管套接字連接上有沒有數據可以接收都會馬上返回。原因就在於我們用ioctlsocket把套接字設置為非阻塞模式了。不過你跟蹤一下就會發現,在沒有數據的情況下,recv確實是馬上返回了,但是也返回了一個錯誤:WSAEWOULDBLOCK,意思就是請求的操作沒有成功完成。

看到這裡很多人可能會說,那麼就重覆調用recv並檢查返回值,直到成功為止,但是這樣做效率很成問題,開銷太大。

select模型的出現就是為瞭解決上述問題。
select模型的關鍵是使用一種有序的方式,對多個套接字進行統一管理與調度 。

如上所示,用戶首先將需要進行IO操作的socket添加到select中,然後阻塞等待select系統調用返回。當數據到達時,socket被激活,select函數返回。用戶線程正式發起read請求,讀取數據並繼續執行。

從流程上來看,使用select函數進行IO請求和同步阻塞模型沒有太大的區別,甚至還多了添加監視socket,以及調用select函數的額外操作,效率更差。但是,使用select以後最大的優勢是用戶可以在一個線程內同時處理多個socket的IO請求。用戶可以註冊多個socket,然後不斷地調用select讀取被激活的socket,即可達到在同一個線程內同時處理多個IO請求的目的。而在同步阻塞模型中,必須通過多線程的方式才能達到這個目的。

select流程偽代碼如下:

{
    select(socket);
    while(1) 
    {
        sockets = select();
        for(socket in sockets) 
        {
            if(can_read(socket)) 
            {
                read(socket, buffer);
                process(buffer);
            }
        }
    }
}

select相關API介紹與使用

#include <sys/select.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
int select(int maxfdp, fd_set *readset, fd_set *writeset, fd_set *exceptset,struct timeval *timeout);

參數說明:

maxfdp:被監聽的文件描述符的總數,它比所有文件描述符集合中的文件描述符的最大值大1,因為文件描述符是從0開始計數的;

readfds、writefds、exceptset:分別指向可讀、可寫和異常等事件對應的描述符集合。

timeout:用於設置select函數的超時時間,即告訴內核select等待多長時間之後就放棄等待。timeout == NULL 表示等待無限長的時間

timeval結構體定義如下:

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

返回值:超時返回0;失敗返回-1;成功返回大於0的整數,這個整數表示就緒描述符的數目。

以下介紹與select函數相關的常見的幾個巨集:

#include <sys/select.h>   
int FD_ZERO(int fd, fd_set *fdset);   //一個 fd_set類型變數的所有位都設為 0
int FD_CLR(int fd, fd_set *fdset);  //清除某個位時可以使用
int FD_SET(int fd, fd_set *fd_set);   //設置變數的某個位置位
int FD_ISSET(int fd, fd_set *fdset); //測試某個位是否被置位

select使用範例:
當聲明瞭一個文件描述符集後,必須用FD_ZERO將所有位置零。之後將我們所感興趣的描述符所對應的位置位,操作如下:

fd_set rset;   
int fd;   
FD_ZERO(&rset);   
FD_SET(fd, &rset);   
FD_SET(stdin, &rset);

然後調用select函數,擁塞等待文件描述符事件的到來;如果超過設定的時間,則不再等待,繼續往下執行。

select(fd+1, &rset, NULL, NULL,NULL);

select返回後,用FD_ISSET測試給定位是否置位:

if(FD_ISSET(fd, &rset)   
{ 
    ... 
    //do something  
}

下麵是一個最簡單的select的使用例子:

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

int main()
{
    fd_set rd;
    struct timeval tv;
    int err;
    

    FD_ZERO(&rd);
    FD_SET(0,&rd);
    
    tv.tv_sec = 5;
    tv.tv_usec = 0;
    err = select(1,&rd,NULL,NULL,&tv);
    
    if(err == 0) //超時
    {
        printf("select time out!\n");
    }
    else if(err == -1)  //失敗
    {
        printf("fail to select!\n");
    }
    else  //成功
    {
        printf("data is available!\n");
    }

    
    return 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前迴圈加fd,取maxfd,select返回後利用FD_ISSET判斷是否有事件發生。

用select處理帶外數據

網路程式中,select能處理的異常情況只有一種:socket上接收到帶外數據。

什麼是帶外數據?

帶外數據(out—of—band data),有時也稱為加速數據(expedited data),
是指連接雙方中的一方發生重要事情,想要迅速地通知對方。
這種通知在已經排隊等待發送的任何“普通”(有時稱為“帶內”)數據之前發送。
帶外數據設計為比普通數據有更高的優先順序。
帶外數據是映射到現有的連接中的,而不是在客戶機和伺服器間再用一個連接。

我們寫的select程式經常都是用於接收普通數據的,當我們的伺服器需要同時接收普通數據和帶外數據,我們如何使用select進行處理二者呢?

下麵給出一個小demo:

#include <stdio.h>
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <string.h>
#include <fcntl.h>
#include <stdlib.h>


int main(int argc, char* argv[])
{
    if(argc <= 2)
    {
        printf("usage: ip address + port numbers\n");
        return -1;
    }
    
    const char* ip = argv[1];
    int port = atoi(argv[2]);

        printf("ip: %s\n",ip);
        printf("port: %d\n",port);
    
    int ret = 0;
    struct sockaddr_in address;
    bzero(&address,sizeof(address));
    address.sin_family = AF_INET;
    inet_pton(AF_INET,ip,&address.sin_addr);
    address.sin_port = htons(port);
    
    int listenfd = socket(PF_INET,SOCK_STREAM,0);
    if(listenfd < 0)
    {
        printf("Fail to create listen socket!\n");
        return -1;
    }
    
    ret = bind(listenfd,(struct sockaddr*)&address,sizeof(address));
    if(ret == -1)
    {
        printf("Fail to bind socket!\n");
        return -1;
    }
    
    ret = listen(listenfd,5); //監聽隊列最大排隊數設置為5
    if(ret == -1)
    {
        printf("Fail to listen socket!\n");
        return -1;
    }
    
    struct sockaddr_in client_address;  //記錄進行連接的客戶端的地址
    socklen_t client_addrlength = sizeof(client_address);
    int connfd = accept(listenfd,(struct sockaddr*)&client_address,&client_addrlength);
    if(connfd < 0)
    {
        printf("Fail to accept!\n");
        close(listenfd);
    }
    
    char buff[1024]; //數據接收緩衝區
    fd_set read_fds;  //讀文件操作符
    fd_set exception_fds; //異常文件操作符
    FD_ZERO(&read_fds);
    FD_ZERO(&exception_fds);
    
    while(1)
    {
        memset(buff,0,sizeof(buff));
        /*每次調用select之前都要重新在read_fds和exception_fds中設置文件描述符connfd,因為事件發生以後,文件描述符集合將被內核修改*/
        FD_SET(connfd,&read_fds);
        FD_SET(connfd,&exception_fds);
        
        ret = select(connfd+1,&read_fds,NULL,&exception_fds,NULL);
        if(ret < 0)
        {
            printf("Fail to select!\n");
            return -1;
        }
        
        
        if(FD_ISSET(connfd, &read_fds))
        {
            ret = recv(connfd,buff,sizeof(buff)-1,0);
            if(ret <= 0)
            {
                break;
            }
            
            printf("get %d bytes of normal data: %s \n",ret,buff);
            
        }
        else if(FD_ISSET(connfd,&exception_fds)) //異常事件
        {
            ret = recv(connfd,buff,sizeof(buff)-1,MSG_OOB);
            if(ret <= 0)
            {
                break;
            }
            
            printf("get %d bytes of exception data: %s \n",ret,buff);
        }
        
    }
    
    close(connfd);
    close(listenfd);
    
    
    return 0;
}

用select來解決socket中的多客戶問題

上面提到過,,使用select以後最大的優勢是用戶可以在一個線程內同時處理多個socket的IO請求。在網路編程中,當涉及到多客戶訪問伺服器的情況,我們首先想到的辦法就是fork出多個進程來處理每個客戶連接。現在,我們同樣可以使用select來處理多客戶問題,而不用fork。

伺服器端

#include <sys/types.h> 
#include <sys/socket.h> 
#include <stdio.h> 
#include <netinet/in.h> 
#include <sys/time.h> 
#include <sys/ioctl.h> 
#include <unistd.h> 
#include <stdlib.h>

int main() 
{ 
    int server_sockfd, client_sockfd; 
    int server_len, client_len; 
    struct sockaddr_in server_address; 
    struct sockaddr_in client_address; 
    int result; 
    fd_set readfds, testfds; 
    server_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立伺服器端socket 
    server_address.sin_family = AF_INET; 
    server_address.sin_addr.s_addr = htonl(INADDR_ANY); 
    server_address.sin_port = htons(8888); 
    server_len = sizeof(server_address); 
    bind(server_sockfd, (struct sockaddr *)&server_address, server_len); 
    listen(server_sockfd, 5); //監聽隊列最多容納5個 
    FD_ZERO(&readfds); 
    FD_SET(server_sockfd, &readfds);//將伺服器端socket加入到集合中
    while(1) 
    {
        char ch; 
        int fd; 
        int nread; 
        testfds = readfds;//將需要監視的描述符集copy到select查詢隊列中,select會對其修改,所以一定要分開使用變數 
        printf("server waiting\n"); 

        /*無限期阻塞,並測試文件描述符變動 */
        result = select(FD_SETSIZE, &testfds, (fd_set *)0,(fd_set *)0, (struct timeval *) 0); //FD_SETSIZE:系統預設的最大文件描述符
        if(result < 1) 
        { 
            perror("server5"); 
            exit(1); 
        } 

        /*掃描所有的文件描述符*/
        for(fd = 0; fd < FD_SETSIZE; fd++) 
        {
            /*找到相關文件描述符*/
            if(FD_ISSET(fd,&testfds)) 
            { 
              /*判斷是否為伺服器套接字,是則表示為客戶請求連接。*/
                if(fd == server_sockfd) 
                { 
                    client_len = sizeof(client_address); 
                    client_sockfd = accept(server_sockfd, 
                    (struct sockaddr *)&client_address, &client_len); 
                    FD_SET(client_sockfd, &readfds);//將客戶端socket加入到集合中
                    printf("adding client on fd %d\n", client_sockfd); 
                } 
                /*客戶端socket中有數據請求時*/
                else 
                { 
                    ioctl(fd, FIONREAD, &nread);//取得數據量交給nread
                    
                    /*客戶數據請求完畢,關閉套接字,從集合中清除相應描述符 */
                    if(nread == 0) 
                    { 
                        close(fd); 
                        FD_CLR(fd, &readfds); //去掉關閉的fd
                        printf("removing client on fd %d\n", fd); 
                    } 
                    /*處理客戶數據請求*/
                    else 
                    { 
                        read(fd, &ch, 1); 
                        sleep(5); 
                        printf("serving client on fd %d\n", fd); 
                        ch++; 
                        write(fd, &ch, 1); 
                    } 
                } 
            } 
        } 
    } 

    return 0;
}

客戶端

//客戶端
#include <sys/types.h> 
#include <sys/socket.h> 
#include <stdio.h> 
#include <netinet/in.h> 
#include <arpa/inet.h> 
#include <unistd.h> 
#include <stdlib.h>
#include <sys/time.h>

int main() 
{ 
    int client_sockfd; 
    int len; 
    struct sockaddr_in address;//伺服器端網路地址結構體 
     int result; 
    char ch = 'A'; 
    client_sockfd = socket(AF_INET, SOCK_STREAM, 0);//建立客戶端socket 
    address.sin_family = AF_INET; 
    address.sin_addr.s_addr = inet_addr("127.0.0.1");
    address.sin_port = htons(8888); 
    len = sizeof(address); 
    result = connect(client_sockfd, (struct sockaddr *)&address, len); 
    if(result == -1) 
    { 
         perror("oops: client2"); 
         exit(1); 
    } 
    //第一次讀寫
    write(client_sockfd, &ch, 1); 
    read(client_sockfd, &ch, 1); 
    printf("the first time: char from server = %c\n", ch); 
    sleep(5);
    
    //第二次讀寫
    write(client_sockfd, &ch, 1); 
    read(client_sockfd, &ch, 1); 
    printf("the second time: char from server = %c\n", ch);
    
    close(client_sockfd); 
   
    return 0; 
}

運行流程:

客戶端:啟動->連接伺服器->發送A->等待伺服器回覆->收到B->再發B給伺服器->收到C->結束

伺服器:啟動->select->收到A->發A+1回去->收到B->發B+1過去

測試:我們先運行伺服器,再運行客戶端

select總結:

select本質上是通過設置或者檢查存放fd標誌位的數據結構來進行下一步處理。這樣所帶來的缺點是:

1、單個進程可監視的fd數量被限制,即能監聽埠的大小有限。一般來說這個數目和系統記憶體關係很大,具體數目可以cat/proc/sys/fs/file-max察看。32位機預設是1024個。64位機預設是2048.

2、 對socket進行掃描時是線性掃描,即採用輪詢的方法,效率較低:當套接字比較多的時候,每次select()都要通過遍歷FD_SETSIZE個Socket來完成調度,不管哪個Socket是活躍的,都遍歷一遍。這會浪費很多CPU時間。如果能給套接字註冊某個回調函數,當他們活躍時,自動完成相關操作,那就避免了輪詢,這正是epoll與kqueue做的。

3、需要維護一個用來存放大量fd的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時複製開銷大。


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • " 探索Windows命令行系列(1):導航目錄 " " 探索Windows命令行系列(2):命令行工具入門 " " 探索Windows命令行系列(3):命令行腳本基礎 " " 探索Windows命令行系列(4):通過命令管理文件和文件夾 " " 探索Windows命令行系列(5):幾個實用的命令例 ...
  • Let's Encrypt免費又好用的證書,廢話不多說。 假設我的功能變數名稱為:163.org 1、克隆代碼 git clone https://github.com/letsencrypt/letsencrypt # 沒有git的先安裝git # yum install git # apt-get in ...
  • 1、回顧基礎命令 2、腳本 3、變數 4、別名 5、條件判斷 6、test判斷 一、回顧基礎命令 shutdown --關機/重啟 exit --退出當前shell rmdir --刪除空目錄 du --查看目錄占用的存儲空間 df --查看已 經掛載的文件系統的空間使用情況 ln --創建鏈接 c ...
  • 很多電腦愛好者對於Win10內置的PIN碼功能不太瞭解,很多朋友都還沒有使用。其實,創建PIN碼可以提到密碼使用,當你登錄到Windows和其它應用服務時,可以通過PIN碼替代輸入賬戶密碼,提升安全性。話不多說,以下是Win10開啟PIN碼設置使用教程,步驟如下。 一、從Win10左下角的開始菜單, ...
  • 對於游戲玩家來說,對顯卡的關註度要高於電腦其它硬體,一般來說,顯卡越好,游戲性能往往越強。不過要持續發揮顯卡的最佳游戲性能,經常更新顯卡驅動也是很有必要的。那麼筆記本顯卡驅動怎麼更新?下麵小編以自己的Win10筆記本為例,教大家如何升級筆記本顯卡驅動。 Win10筆記本顯卡驅動更新升級方法 升級筆記 ...
  • 1.下載最新的openssh包 http://www.openssh.com/portable.html#http 2.升級openssh之前要先打開伺服器telnet,通過telnet登錄伺服器,因為升級過程中會導致ssh暫時不能用 打開linux telnet服務: 查看telnet是否已經安裝 ...
  • 環境:筆記本 + 家用WIFI + 公司WIFI + VMware + CentOS6.8 + Xshell 問題描述:初學Linux時,用筆記本裝了虛擬機(單網卡),想實現linux在家和公司都能夠無線連網,但又不想上網地點變動之後每次手動輸入IP登錄Xshell。 解決思路:增加一塊網卡(eth ...
  • 一、簡介 1、認識 加密網頁(https): tcp:443 明文網頁(http): tcp:80 survey.netcraft.net --這個網站上可以查到最新的網站伺服器的使用率 超文本傳輸協議(HTTP,HyperText Transfer Protocol)是互聯網上應用最為廣泛的一種網 ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...