Linux NIO 系列(04-1) select

Linux NIO 系列(04-1) select編程

Netty 系列目錄(http://www.javashuo.com/article/p-hskusway-em.html)api

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

1、select 機制的優點

爲何會出現 select 模型?網絡

先看一下下面的這句代碼:數據結構

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

這是用來接收數據的,在默認的阻塞模式下的套接字裏,recv 會阻塞在那裏,直到套接字鏈接上有數據可讀,把數據讀到 buffer 裏後 recv 函數纔會返回,否則就會一直阻塞在那裏。在單線程的程序裏出現這種狀況會致使主線程(單線程程序裏只有一個默認的主線程)被阻塞,這樣整個程序被鎖死在這裏,若是永 遠沒數據發送過來,那麼程序就會被永遠鎖死。這個問題能夠用多線程解決,可是在有多個套接字鏈接的狀況下,這不是一個好的選擇,擴展性不好。多線程

再看代碼:socket

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

這一次 recv 的調用無論套接字鏈接上有沒有數據能夠接收都會立刻返回。緣由就在於咱們用 ioctlsocket 把套接字設置爲非阻塞模式了。不過你跟蹤一下就會發現,在沒有數據的狀況下,recv 確實是立刻返回了,可是也返回了一個錯誤:WSAEWOULDBLOCK,意思就是請求的操做沒有成功完成。函數

看到這裏不少人可能會說,那麼就重複調用 recv 並檢查返回值,直到成功爲止,可是這樣作效率很成問題,開銷太大。測試

select 模型的出現就是爲了解決上述問題。

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);
            }
        }
    }
}

2、select API 介紹與使用

2.1 select

#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 的整數,這個整數表示就緒描述符的數目。

2.2 fd_set 集合操做

如下介紹與 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);    // 測試某個位是否被置位

2.3 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  
}

3、深刻理解 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 判斷是否有事件發生。

4、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 的數據結構,這樣會使得用戶空間和內核空間在傳遞該結構時複製開銷大。

固然 select 也有優勢:兼容性好,不論是 Linux 仍是 Windows 都支持 select。

附1:select 網絡編程代碼

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

#define SERVER_PORT 8888
#define OPEN_MAX  3000
#define BACKLOG   10
#define BUF_SIZE  1024

void main() {
    int i, j, maxi;
    int listenfd, connfd, sockfd; // 定義套接字描述符
    int nready;     // 接受 pool 返回值
    int recvbytes;  // 接受 recv 返回值

    char recv_buf[BUF_SIZE];        // 發送緩衝區
    fd_set readSet, totalSet;       // 定義讀集合,備份集合

    // 定義 IPV4 套接口地址結構
    struct sockaddr_in seraddr;     // service 地址
    struct sockaddr_in cliaddr;     // client 地址
    int cliaddr_len;

    // 初始化IPV4套接口地址結構
    seraddr.sin_family = AF_INET;   // 指定該地址家族
    seraddr.sin_port = htons(SERVER_PORT);    // 端口
    seraddr.sin_addr.s_addr = INADDR_ANY;   // IPV4的地址
    bzero(&(seraddr.sin_zero), 8);

    // 啓動 server
    listenfd = socket(AF_INET, SOCK_STREAM, 0);
    bind(listenfd, (struct sockaddr *)&seraddr, sizeof(struct sockaddr));
    listen(listenfd, BACKLOG);

    // select 模型處理過程  
    // 1. 初始化套接字集合,添加監聽 socket 到這個集合
    FD_ZERO(&totalSet);
    FD_SET(listenfd, &totalSet);
    maxi = listenfd;

    while(1) {
        // 2. 將集合的一個拷貝傳遞給 select 函數。當有事件發生時,select 移除未決的 socket 而後返回。
        //    也就是說 select 返回時,集合 readSet 中就是發生事件的 readSet
        readSet = totalSet;
        int nready = select(maxi + 1, &readSet, NULL, NULL, NULL);
        if (nready > 0) {
            if (FD_ISSET(listenfd, &readSet)) {
                cliaddr_len = sizeof(cliaddr);
                connfd = accept(listenfd, (struct sockaddr *) &cliaddr, &cliaddr_len);
                printf("client IP: %s\t PORT : %d\n", inet_ntoa(cliaddr.sin_addr), ntohs(cliaddr.sin_port));

                FD_SET(connfd, &totalSet);
                maxi = connfd;
                if (--nready == 0) {
                    continue;
                }
            }

            for (i = listenfd + 1; i <= maxi; i++) {
                sockfd = i;
                if (FD_ISSET(sockfd, &readSet)) {
                    recvbytes = read(sockfd, recv_buf, sizeof(recv_buf));
                    if (recvbytes == 0) {           // 客戶端關閉
                        close(sockfd);
                        FD_CLR(sockfd, &totalSet);
                    } else if (recvbytes == -1) {   // read 異常
                        perror("read error");
                        exit(1);
                    } else {                        // 正常讀取數據
                        write(sockfd, recv_buf, recvbytes);
                        printf("receive %s\n", recv_buf);
                    }
                }
            }
        }
    }
}

參考:

  1. Linux編程之select

天天用心記錄一點點。內容也許不重要,但習慣很重要!

相關文章
相關標籤/搜索