《UNIX網路程式設計》筆記 - select和poll

熊紀元發表於2019-05-03

簡介

IO複用:讓程式等待一系列IO條件而不是一個IO條件

通過selectpoll函式我們可以同時監聽多個描述符,在描述符就緒時進行對應的操作。

select

定義:

//maxfdpl: 待測試的描述符個數
//返回就緒描述符的個數,若超時則為0, 若出錯則為-1
int select(int maxfdpl, 
    fd_set *readset, 
    fd_set *writeset, 
    fd_set *exceptset, 
    struct timeval *timeout);
 
//超時選項    
//NULL:wait forever;0:don't wait
struct timeval {
	long    tv_sec;         /* seconds */
	long    tv_usec;        /* and microseconds */
};

//每個fds_bit的每一位對應一個描述符
typedef struct fd_set {
	int     fds_bits[FD_SETSIZE/sizeof(int)/NBBY]; /* NBBY=bits in a byte ; usually 8*/
} fd_set;

#define FD_SETSIZE  1024        /* fd_set中描述符的總數 */
void FD_ZERO(fd_set *fdset);            /* clear all bits in fdset */
void FD_SET(int fd, fd_set *fdset);     /* turn on the bit for fd in fdset */
void FD_CLR(int fd, fd_set *fdset);     /* turn off the bit for fd in fdset */
void FD_ISSET(int fd, fd_set *fdset);   /* is the bit for fd on in fdset ? */
複製程式碼

select的使用方法:

    int fds[FD_SETSIZE]; 儲存當前所有描述符
    fd_set rset, wset, eset; //定義讀、寫、異常對應的fd_set
    //初始化fd_set,非常重要且不能省略,因為如果不初始化可能會影響FD_ISSET的呼叫結果
    FD_ZERO(&rset); 
    FD_ZERO(&wset);
    FD_ZERO(&eset);
    for (;;) {
        //在迴圈中呼叫select
        select(FD_SETSIZE, &rset, &wset, &eset, NULL);
        //遍歷當前所有的fd,處理就緒的fd
        for (int i = 0; i < FD_SETSIZE; i++)
        {
            if (FD_ISSET(fds[i], &rset))
            {
                //handle read
            }
            if (FD_ISSET(fds[i], &wset))
            {
                //handle write
            }
            if (FD_ISSET(fds[i], &eset))
            {
                //handle exception
            }
        }
    }
複製程式碼

fd_set的限制

在很早之前就看到網上的介紹說select在描述符個數上是有限制的,現在終於知道這個限制是從哪來的了,這實際上跟fd_set的實現機制有關。

fd_set中使用int陣列中的各個位來儲存多個描述符的狀態,這個陣列稱為描述符集,比如陣列的第一個數有32位,那麼第一個數的每一位就表示第0~31個描述符的狀態,這樣一來當我們呼叫FD_ISSET來判斷某一個描述符狀態時,我們只需要找到其對應的位判斷其是0或者1就行了;同理當我們需要設定某個描述符狀態時,只需要設定對應的位的狀態即可。而fd_set中陣列的大小是通過FD_SETSIZE這個值算出來的,FD_SETSIZE是一個巨集定義,通常它的預設值比較小,在我的mac上檢視其預設值是1024,也就是說在我的mac上select能夠支援的最大的描述符數量是1024個。當然FD_SETSIZE也可以重新定義,但如果要調整需要重新編譯核心。

描述符讀就緒條件

  1. 接收緩衝區資料位元組數大於低水位(預設是1),這時讀取操作返回大於0
  2. 讀半關閉,也就是對端發來了FIN,這時返回0,也就是EOF
  3. 當前套接字是監聽套接字,而且已完成連線數不為0,這時可以進行accept操作
  4. 描述符上有套接字錯誤需要處理

描述符寫就緒條件

  1. 傳送緩衝區資料位元組數大於低水位(通常為2048);
  2. 套接字已連線或不需要連線(UDP)
  3. 寫半關閉,這時如果再寫會收到SIGPIPE訊號
  4. 使用非阻塞式connect的套接字已建立連線或者connect失敗
  5. 描述符上有套接字錯誤需要處理

shutdown&close

有兩個函式可以關閉套接字:shutdownclose,它們的區別如下:

  1. close會將引用計數減1,當計數為0時關閉套接字;shutdown可以直接觸發關閉。
  2. close會終止讀和寫兩個方向;shutdown可以通過引數howto指定關閉某個方向
int close(int fd);
int shutdown(int fd, int howto);

/*
 * howto arguments for shutdown(2), specified by Posix.1g.
 */
#define SHUT_RD         0               /* shut down the reading side */
#define SHUT_WR         1               /* shut down the writing side */
#define SHUT_RDWR       2               /* shut down both sides */

複製程式碼

poll

pollselect的功能類似,也支援IO複用,但是poll沒有使用描述符集,而是使用pollfd這種結構來表示描述符的狀態。

//nfds:array的長度,受程式能開啟的最大檔案數限制
//返回就緒描述符的個數,若超時則為0, 若出錯則為-1
int poll(struct pollfd *fdarray, unsigned long nfds, int timeout);

struct pollfd {
	int     fd;         /* descriptor to check */
	short   events;     /* events of intrests on fd */
	short   revents;    /* events that occurred on fd */
};
複製程式碼

poll的使用方法:

    struct pollfd pollfds[OPEN_MAX]; //定義pollfd陣列,將需要監聽的描述符儲存起來
    for (;;)
    {
        //在迴圈中呼叫poll
        poll(pollfds, OPEN_MAX, INFTIM);
        for (int i = 0; i < OPEN_MAX; i++)
        {
            //遍歷pollfd陣列處理就緒的描述符
            struct pollfd pfd = pollfds[i];
            if (pfd.revents & POLLIN) {
                //handle read
            }
            if (pfd.revents & POLLOUT) {
                //handle write
            }
        }
    }
複製程式碼

poll識別的資料型別:普通(normal)、優先順序帶(priority band)、高優先順序(high priority); 這些術語來自基於流的實現。(沒太明白,先標記下)

events常量列舉:

常量 出現在events 出現在revents 說明
POLLIN y y 普通或優先順序帶資料可讀
POLLRDNORM y y 普通資料可讀
POLLRDBAND y y 優先順序帶資料可讀
POLLPRI y y 高優先順序帶資料可讀
POLLOUT y y 普通資料可寫
POLLWRNORM y y 普通資料可寫
POLLWRBAND y y 優先順序帶資料可寫
POLLERR n y 發生錯誤
POLLHUP n y 發生掛起
POLLNVAL n y 描述符不是一個開啟的檔案

poll的就緒條件:

  • 所有正規TCP資料和所有UDP資料視為普通資料
  • TCP帶外資料視為優先順序帶資料
  • 當TCP讀半關閉時(收到對端傳來的FIN),也視為普通資料,隨後的讀操作將返回0
  • TCP連線存在錯誤既可以視為普通資料,也可以視為錯誤(POLLERR)。隨後的讀操作將返回-1,並設定全域性的errno變數。
  • 監聽套接字上有新的連線既可以視為普通資料,也可以視為優先順序資料。
  • 非阻塞式的connect的完成視為使對應的套接字可寫。

總結

selectpoll都支援IO複用,其思路都是呼叫函式監聽多個描述符,當有描述符就緒或者超時的時候函式呼叫就會返回,對應的描述符集合狀態也會改變,這時候再遍歷描述符集合,處理其中就緒的部分即可。

這種方式在需要監聽的描述符比較小,或者是每次就緒的描述符很多的情況下比較有效;但當描述符很多而且每次只有少數描述符就緒時,效率就比較低了。後面出現的epoll就避免了這種線性遍歷的問題。

另外select還受FD_SETSIZE的限制,只能處理較少的描述符,而poll則沒有這個限制。poll監聽的集合大小隻受程式能開啟的檔案數量(RLIMIT_NOFILE)的限制。

相關文章