天天看點

select函數詳解

select函數詳解

        • 背景
        • 說明
          • 定義
          • 介紹、
          • 參數說明
          • 原理
          • 傳回值
          • pselect
        • 總結
        • 案例
          • 案例1
          • 案例2

說明:本文整合網絡資源和man幫助文檔,請酌情參考。

select函數詳解

背景

select函數是實作IO多路複用的一種方式。

什麼是IO多路複用?

舉一個簡單地網絡伺服器的例子,如果你的伺服器需要和多個用戶端保持連接配接,處理用戶端的請求,屬于多程序的并發問題,如果建立很多個程序來處理這些IO流,會導緻CPU占有率很高。是以人們提出了I/O多路複用模型:一個線程,通過記錄I/O流的狀态來同時管理多個I/O。

select隻是IO複用的一種方式,其他的還有:poll,epoll等。

說明

定義
/* According to POSIX.1-2001 */
#include <sys/select.h>

/* According to earlier standards */
#include <sys/time.h>
#include <sys/types.h>
#include <unistd.h>

int select(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, struct timeval *timeout);

void FD_CLR(int fd, fd_set *set);
int  FD_ISSET(int fd, fd_set *set);
void FD_SET(int fd, fd_set *set);
void FD_ZERO(fd_set *set);

           
介紹、

select()函數允許程式監視多個檔案描述符,等待所監視的一個或者多個檔案描述符變為“準備好”的狀态。所謂的”準備好“狀态是指:檔案描述符不再是阻塞狀态,可以用于某類IO操作了,包括可讀,可寫,發生異常三種。

我們使用select來監視檔案描述符時,要向核心傳遞的資訊包括:

​ 1、我們要監視的檔案描述符個數

​ 2、每個檔案描述符,我們可以監視它的一種或多種狀态,包括:可讀,可寫,發生異常三種。

​ 3、要等待的時間,監視是一個過程,我們希望核心監視多長時間,然後傳回給我們監視結果呢?

​ 4、監視結果包括:準備好了的檔案描述符個數,對于讀,寫,異常,分别是哪兒個檔案描述符準備好了。

參數說明

**nfds:**是一個整數值, 表示集合中所有檔案描述符的範圍,即所有檔案描述符的最大值+1。在windows中不需要管這個。

linux select第一個參數的函數: 待測試的描述集的總個數。 但要注意, 待測試的描述集總是從0, 1, 2, …開始的。 是以, 假如你要檢測的描述符為8, 9, 10, 那麼系統實際也要監測0, 1, 2, 3, 4, 5, 6, 7, 此時真正待測試的描述符的個數為11個, 也就是max(8, 9, 10) + 1

注意:

​ 1、果你要檢測描述符8, 9, 10, 但是你把select的第一個參數定為8, 實際上隻檢測0到7, 是以select不會感覺到8, 9, 10描述符的變化。

​ 2、果你要檢測描述符8, 9, 10, 且你把select的第一個參數定為11, 實際上會檢測0-10, 但是, 如果你不把描述如0 set到描述符中, 那麼select也不會感覺到0描述符的變化。

​ 是以, select感覺到描述符變化的必要條件是, 第一個參數要合理, 比如定義為fdmax+1, 且把需要檢測的描述符set到描述集中。

fd_set:

一個檔案描述符集合儲存在fd_set變量中,可讀,可寫,異常這三個描述符集合需要使用三個變量來儲存,分别是 readfds,writefds,exceptfds。我們可以認為一個fd_set變量是由很多個二進制構成的數組,每一位表示一個檔案描述符是否需要監視。

對于fd_set類型的變量,我們隻能使用相關的函數來操作。

void FD_CLR(int fd, fd_set *set);//清除某一個被監視的檔案描述符。
int  FD_ISSET(int fd, fd_set *set);//測試一個檔案描述符是否是集合中的一員
void FD_SET(int fd, fd_set *set);//添加一個檔案描述符,将set中的某一位設定成1;
void FD_ZERO(fd_set *set);//清空集合中的檔案描述符,将每一位都設定為0;
           

使用案例:

fd_set readfds;
int fd;
FD_ZERO(&readfds)//新定義的變量要清空一下。相當于初始化。
FD_SET(fd,&readfds);//把檔案描述符fd加入到readfds中。
//select 傳回
if(FD_ISSET(fd,&readset))//判斷是否成功監視
{
    //dosomething
}
           

readfds:

監視檔案描述符的一個集合,我們監視其中的檔案描述符是不是可讀,或者更準确的說,讀取是不是不阻塞了。

writefds:

監視檔案描述符的一個集合,我們監視其中的檔案描述符是不是可寫,或者更準确的說,寫入是不是不阻塞了。

exceptfds:

用來監視發生錯誤異常檔案

timeout

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

timeout表示select傳回之前的時間上限。

如果timeout==NULL,無期限等待下去,這個等待可以被一個信号中斷,隻有當一個描述符準備好,或者捕獲到一個信号時函數才會傳回。如果是捕獲到信号,select傳回-1,并将變量errno設定成EINTR。

如果timeout->tv_sec0 && timeout->tv_sec0 ,不等待直接傳回,加入的描述符都會被測試,并且傳回滿足要求的描述符個數,這種方法通過輪詢,無阻塞地獲得了多個檔案描述符狀态。

如果timeout->tv_sec!=0 || timeout->tv_sec!=0 ,等待指定的時間。當有描述符複合條件或者超過逾時時間的話,函數傳回。等待總是會被信号中斷。

原理

了解select模型的關鍵在于了解fd_set,為說明友善,取fd_set長度為1位元組,fd_set中的每一bit可以對應一個檔案描述符fd。則1位元組長的fd_set最大可以對應8個fd。

​ 執行fd_set set;FD_ZERO(&set);則set用位表示是0000,0000。

​ 若fd=5,執行FD_SET(fd,&set);後set變為0001,0000(第5位置為1)

​ 若再加入fd=2,fd=1,則set變為0001,0011

​ 執行select(6,&set,0,0,0)阻塞等待

​ 若fd=1,fd=2上都發生可讀事件,則select傳回,此時set變為0000,0011。注意:沒有事件發生的fd=5被清空。

傳回值

成功時:傳回三中描述符集合中”準備好了“的檔案描述符數量。

逾時:傳回0

錯誤:傳回-1,并設定 errno

EBADF:集合中包含無效的檔案描述符。(檔案描述符已經關閉了,或者檔案描述符上已經有錯誤了)。

EINTR:捕獲到一個信号。

EINVAL:nfds是負的或者timeout中包含的值無效。

ENOMEM:無法為内部表配置設定記憶體。

pselect
#include <sys/select.h>

int pselect(int nfds, fd_set *readfds, fd_set *writefds,
           fd_set *exceptfds, const struct timespec *timeout,
           const sigset_t *sigmask);


           

select和pselect有三個主要的差別:

1、select逾時使用的是struct timeval,用秒和微秒計時,而pselect使用struct timespec ,用秒和納秒。

struct timespec{
    time_t tv_sec;//秒
    long tv_nsec;//納秒
}
           

2、select會更新逾時參數timeout 以訓示還剩下多少時間,pselect不會。

3、select沒有sigmask參數.

sigmask:這個參數儲存了一組核心應該打開的信号(即:從調用線程的信号掩碼中删除)

當pselect的sigmask==NULL時pselect和select一樣

當sigmask!=NULL時,等效于以下原子操作:

sigset_t origmask;
sigprocmask(SIG_SETMASK, &sigmask, &origmask);
ready = select(nfds, &readfds, &writefds, &exceptfds, timeout);
sigprocmask(SIG_SETMASK, &origmask, NULL);
           

接收信号的程式通常隻使用信号處理程式來引發全局标志。全局标志将訓示事件必須被處理。在程式的主循環中。一個信号将導緻select和pselect傳回-1 并将erron=EINTR。

我們經常要在主循環中處理信号,主循環的某個位置将會檢查全局标志,那麼我們會問:如果信号在條件之後,select之前到達怎麼辦。答案是select會無限期阻塞。

這種情況很少見,但是這就是為什麼出現了pselect。因為他是類似原子操作的。

舉個栗子:

static volatile sig_atomic_t got_SIGCHLD = 0;

       static void
       child_sig_handler(int sig)
       {
           got_SIGCHLD = 1;
       }

       int
       main(int argc, char *argv[])
       {
           sigset_t sigmask, empty_mask;
           struct sigaction sa;
           fd_set readfds, writefds, exceptfds;
           int r;

           sigemptyset(&sigmask);
           sigaddset(&sigmask, SIGCHLD);
           if (sigprocmask(SIG_BLOCK, &sigmask, NULL) == -1) {
               perror("sigprocmask");
               exit(EXIT_FAILURE);
           }

           sa.sa_flags = 0;
           sa.sa_handler = child_sig_handler;
           sigemptyset(&sa.sa_mask);
           if (sigaction(SIGCHLD, &sa, NULL) == -1) {
               perror("sigaction");
               exit(EXIT_FAILURE);
           }

           sigemptyset(&empty_mask);

           for (;;) {          /* main loop */
               /* Initialize readfds, writefds, and exceptfds
                  before the pselect() call. (Code omitted.) */

               r = pselect(nfds, &readfds, &writefds, &exceptfds,
                           NULL, &empty_mask);
               if (r == -1 && errno != EINTR) {
                   /* Handle error */
               }

               if (got_SIGCHLD) {
                   got_SIGCHLD = 0;

                   /* Handle signalled event here; e.g., wait() for all
                      terminated children. (Code omitted.) */
               }

               /* main body of program */
           }
       }

           

總結

select()可以同時監視多個描述符,如果他們沒有活動,則正确地将程序置于休眠狀态。Unix程式員們經常要處理多個檔案描述符的I/O,他們的資料流可能是間歇性的。如果隻建立read或者write會導緻程式阻塞。

在我們使用select的時候,需要注意:

1、我們應該總是設定timeout=0,因為如果沒有可用的資料,程式在運作時間裡将無視可做。依賴逾時的代碼通常是不可移植,并且很難調試。

2、nfds的值一要準備且适當。

3、如果在調用完select之後,你不想檢查結果,也不想做出适當的響應,那麼檔案描述符不需要添加到集合中。

4、select傳回後,所有的檔案描述符都應該被檢查,看看他們是否準備好了。

5、read,recv,write,send,這幾個函數不一定讀/寫你所請求的全部資料。如果他們讀/寫全部資料,是因為低流量負載和快速流。情況并非重視如此,應該處理你的函數僅管理發送或接收單個位元組的情況。

6、除非你真的确信你有少量的資料要處理,否則不要一次隻讀一個位元組,當你每次都能緩沖的時候,盡可能多的讀取資料是非常低效的。

7、read,recv,write,send和select都會有傳回-1的情況,并set errno的值。這些errno必須被恰當的處理。如果你的程式不會接收到任何信号,那麼errno永遠都不會等于EINTR,如果你的程式并不會設定非阻塞IO,那麼errno就不會等于EAGAIN。

8、調用read,recv,write,send,不要使buffer的長度為0;

9、如果read,recv,write,send調用失敗,并且傳回的errno不是7中說的那兩種情況,或者傳回0,意思是“end-of-file”,這種情況下我們不應再将檔案描述符傳遞給select。

10、每次調用select之前,timeout都用重新設定。

11、由于select()修改其檔案描述符集,如果調用在循環中使用,則必須在每次調用之前重新初始化這些集。

大多數的作業系統都支援select。相比于試圖用線程,程序,IPCS,信号,記憶體共享等方式來解決問題,select函數更有效且輕松。系統調用poll和select相似,在監視稀疏檔案集合的時候更加有效。poll現在也在被廣泛的使用,但沒有select簡便。linux專用的epoll在監視大連資料時比select和poll更加有效。

案例

案例1

下面是"man select "幫助文檔中案例:

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

int main(void)
{
    fd_set rfds;//定義一個能儲存檔案描述符集合的變量
    struct timeval tv;//定義逾時時間
    int retval;//儲存傳回值

    /* Watch stdin (fd 0) to see when it has input. */
    /* 監測标準輸入流(fd=0)看什麼時候又輸入*/
    FD_ZERO(&rfds);//初始化集合
    FD_SET(0, &rfds);//把檔案描述符0加入到監測集合中。

    /* Wait up to five seconds. */
    /* 設定逾時時間為5s */
    tv.tv_sec = 5;
    tv.tv_usec = 0;

    /*調用select函數,将檔案描述符集合設定成讀取監測 */
    retval = select(1, &rfds, NULL, NULL, &tv);
    /* Don't rely on the value of tv now! */
    /* 這時候的tv值是不可依賴的 */

    /*根據傳回值類型判斷select函數 */
    if (retval == -1)
        perror("select()");
    else if (retval)
        printf("Data is available now.\n");
    /* FD_ISSET(0, &rfds) will be true. */
    /* 因為值增加了一個fd,如果傳回值>0,則說明fd=0在集合中。*/
    else
        printf("No data within five seconds.\n");

    exit(EXIT_SUCCESS);
}

           
案例2

下面是"man select_tut "幫助文檔中案例:

這個例子更好的說明了select函數的作用,這是一個TCP轉發相關的程式,從一個端口轉發到另一個端口

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

static int forward_port;

#undef max
#define max(x,y) ((x) > (y) ? (x) : (y))

static int listen_socket(int listen_port)
{
    struct sockaddr_in a;
    int s;
    int yes;

    if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        return -1;
    }
    yes = 1;
    if (setsockopt(s, SOL_SOCKET, SO_REUSEADDR,
                   (char *) &yes, sizeof(yes)) == -1) {
        perror("setsockopt");
        close(s);
        return -1;
    }
    memset(&a, 0, sizeof(a));
    a.sin_port = htons(listen_port);
    a.sin_family = AF_INET;
    if (bind(s, (struct sockaddr *) &a, sizeof(a)) == -1) {
        perror("bind");
        close(s);
        return -1;
    }
    printf("accepting connections on port %d\n", listen_port);
    listen(s, 10);
    return s;
}

static int connect_socket(int connect_port, char *address)
{
    struct sockaddr_in a;
    int s;

    if ((s = socket(AF_INET, SOCK_STREAM, 0)) == -1) {
        perror("socket");
        close(s);
        return -1;
    }

    memset(&a, 0, sizeof(a));
    a.sin_port = htons(connect_port);
    a.sin_family = AF_INET;

    if (!inet_aton(address, (struct in_addr *) &a.sin_addr.s_addr)) {
        perror("bad IP address format");
        close(s);
        return -1;
    }

    if (connect(s, (struct sockaddr *) &a, sizeof(a)) == -1) {
        perror("connect()");
        shutdown(s, SHUT_RDWR);
        close(s);
        return -1;
    }
    return s;
}

#define SHUT_FD1 do {                                \
                            if (fd1 >= 0) {                 \
                                shutdown(fd1, SHUT_RDWR);   \
                                close(fd1);                 \
                                fd1 = -1;                   \
                            }                               \
                        } while (0)

#define SHUT_FD2 do {                                \
                            if (fd2 >= 0) {                 \
                                shutdown(fd2, SHUT_RDWR);   \
                                close(fd2);                 \
                                fd2 = -1;                   \
                            }                               \
                        } while (0)

#define BUF_SIZE 1024

int main(int argc, char *argv[])
{
    int h;
    int fd1 = -1, fd2 = -1;
    char buf1[BUF_SIZE], buf2[BUF_SIZE];
    int buf1_avail, buf1_written;
    int buf2_avail, buf2_written;

    //我們希望調用主函數的時候,要指明,本地端口,發送端口,還有發送的ip位址
    if (argc != 4) {
        fprintf(stderr, "Usage\n\tfwd <listen-port> "
                "<forward-to-port> <forward-to-ip-address>\n");
        exit(EXIT_FAILURE);
    }

    // 忽略SIGPIPE這個信号,這個信号常出現在網絡程式設計中,通路一個已經關閉的檔案描述符時候出現。
    signal(SIGPIPE, SIG_IGN);

    //确定發送端口
    forward_port = atoi(argv[2]);

    //監聽本地端口
    h = listen_socket(atoi(argv[1]));
    if (h == -1)
        exit(EXIT_FAILURE);

    for (;;) {
        int r, nfds = 0;
        fd_set rd, wr, er;

        FD_ZERO(&rd);
        FD_ZERO(&wr);
        FD_ZERO(&er);
        FD_SET(h, &rd);
        // 擷取nfds的值。并把fd1,fd2分别加入到,可讀,可寫,異常監視集合中去。
        nfds = max(nfds, h);
        if (fd1 > 0 && buf1_avail < BUF_SIZE) {
            FD_SET(fd1, &rd);
            nfds = max(nfds, fd1);
        }
        if (fd2 > 0 && buf2_avail < BUF_SIZE) {
            FD_SET(fd2, &rd);
            nfds = max(nfds, fd2);
        }
        if (fd1 > 0 && buf2_avail - buf2_written > 0) {
            FD_SET(fd1, &wr);
            nfds = max(nfds, fd1);
        }
        if (fd2 > 0 && buf1_avail - buf1_written > 0) {
            FD_SET(fd2, &wr);
            nfds = max(nfds, fd2);
        }
        if (fd1 > 0) {
            FD_SET(fd1, &er);
            nfds = max(nfds, fd1);
        }
        if (fd2 > 0) {
            FD_SET(fd2, &er);
            nfds = max(nfds, fd2);
        }
        
        //開始監視
        r = select(nfds + 1, &rd, &wr, &er, NULL);

        if (r == -1 && errno == EINTR)
            continue;

        if (r == -1) {
            perror("select()");
            exit(EXIT_FAILURE);
        }

        if (FD_ISSET(h, &rd)) {
            unsigned int l;
            struct sockaddr_in client_address;

            memset(&client_address, 0, l = sizeof(client_address));
            r = accept(h, (struct sockaddr *) &client_address, &l);
            if (r == -1) {
                perror("accept()");
            } else {
                SHUT_FD1;
                SHUT_FD2;
                buf1_avail = buf1_written = 0;
                buf2_avail = buf2_written = 0;
                fd1 = r;
                fd2 = connect_socket(forward_port, argv[3]);
                if (fd2 == -1)
                    SHUT_FD1;
                else
                    printf("connect from %s\n",
                           inet_ntoa(client_address.sin_addr));
            }
        }

        /* NB: read oob data before normal reads */

        if (fd1 > 0)
            if (FD_ISSET(fd1, &er)) {
                char c;

                r = recv(fd1, &c, 1, MSG_OOB);
                if (r < 1)
                    SHUT_FD1;
                else
                    send(fd2, &c, 1, MSG_OOB);
            }
        if (fd2 > 0)
            if (FD_ISSET(fd2, &er)) {
                char c;

                r = recv(fd2, &c, 1, MSG_OOB);
                if (r < 1)
                    SHUT_FD2;
                else
                    send(fd1, &c, 1, MSG_OOB);
            }
        if (fd1 > 0)
            if (FD_ISSET(fd1, &rd)) {
                r = read(fd1, buf1 + buf1_avail,
                         BUF_SIZE - buf1_avail);
                if (r < 1)
                    SHUT_FD1;
                else
                    buf1_avail += r;
            }
        if (fd2 > 0)
            if (FD_ISSET(fd2, &rd)) {
                r = read(fd2, buf2 + buf2_avail,
                         BUF_SIZE - buf2_avail);
                if (r < 1)
                    SHUT_FD2;
                else
                    buf2_avail += r;
            }
        if (fd1 > 0)
            if (FD_ISSET(fd1, &wr)) {
                r = write(fd1, buf2 + buf2_written,
                          buf2_avail - buf2_written);
                if (r < 1)
                    SHUT_FD1;
                else
                    buf2_written += r;
            }
        if (fd2 > 0)
            if (FD_ISSET(fd2, &wr)) {
                r = write(fd2, buf1 + buf1_written,
                          buf1_avail - buf1_written);
                if (r < 1)
                    SHUT_FD2;
                else
                    buf1_written += r;
            }

        /* check if write data has caught read data */

        if (buf1_written == buf1_avail)
            buf1_written = buf1_avail = 0;
        if (buf2_written == buf2_avail)
            buf2_written = buf2_avail = 0;

        /* one side has closed the connection, keep
                  writing to the other side until empty */

        if (fd1 < 0 && buf1_avail - buf1_written == 0)
            SHUT_FD2;
        if (fd2 < 0 && buf2_avail - buf2_written == 0)
            SHUT_FD1;
    }
    exit(EXIT_SUCCESS);
}

           

上面的程式可以應用于大多數類型的TCP連接配接,包括telnet伺服器對OOB信号的轉發。它處理了同時在兩個方向上流動這一棘手問題。你可能會想,使用連個程序不是更有效嗎?事實上使用兩個程序會更複雜。另一個想法是使用fcntl設定非阻塞的I/O使用,這也有弊端,因為它使用非常低效的逾時。

這個程式不能處理同時有多個連接配接的情況,但很容易擴充。你隻需要為每個連接配接建立一個buffer。目前的程式中,新的連接配接會導緻舊的連接配接被覆寫丢棄。

繼續閱讀