《UNIX網路程式設計》筆記 - select和poll
IO複用:讓程序等待一系列IO條件而不是一個IO條件
通過select
和poll
函式我們可以同時監聽多個描述符,在描述符就緒時進行對應的操作。
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 { longtv_sec;/* seconds */ longtv_usec;/* and microseconds */ }; //每個fds_bit的每一位對應一個描述符 typedef struct fd_set { intfds_bits[FD_SETSIZE/sizeof(int)/NBBY]; /* NBBY=bits in a byte ; usually 8*/ } fd_set; #define FD_SETSIZE1024/* 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),這時讀取操作返回大於0
- 讀半關閉,也就是對端發來了FIN,這時返回0,也就是EOF
- 當前套接字是監聽套接字,而且已完成連線數不為0,這時可以進行accept操作
- 描述符上有套接字錯誤需要處理
描述符寫就緒條件
- 傳送緩衝區資料位元組數大於低水位(通常為2048);
- 套接字已連線或不需要連線(UDP)
-
寫半關閉,這時如果再寫會收到
SIGPIPE
訊號 -
使用非阻塞式
connect
的套接字已建立連線或者connect
失敗 - 描述符上有套接字錯誤需要處理
shutdown&close
有兩個函式可以關閉套接字:shutdown
和close
,它們的區別如下:
-
close
會將引用計數減1,當計數為0時關閉套接字;shutdown
可以直接觸發關閉。 -
close
會終止讀和寫兩個方向;shutdown
可以通過引數howto
指定關閉某個方向
int close(int fd); int shutdown(int fd, int howto); /* * howto arguments for shutdown(2), specified by Posix.1g. */ #define SHUT_RD0/* shut down the reading side */ #define SHUT_WR1/* shut down the writing side */ #define SHUT_RDWR2/* shut down both sides */ 複製程式碼
poll
poll
和select
的功能類似,也支援IO複用,但是poll
沒有使用描述符集,而是使用pollfd
這種結構來表示描述符的狀態。
//nfds:array的長度,受程序能開啟的最大檔案數限制 //返回就緒描述符的個數,若超時則為0, 若出錯則為-1 int poll(struct pollfd *fdarray, unsigned long nfds, int timeout); struct pollfd { intfd;/* descriptor to check */ shortevents;/* events of intrests on fd */ shortrevents;/* 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
的完成視為使對應的套接字可寫。
總結
select
和poll
都支援IO複用,其思路都是呼叫函式監聽多個描述符,當有描述符就緒或者超時的時候函式呼叫就會返回,對應的描述符集合狀態也會改變,這時候再遍歷描述符集合,處理其中就緒的部分即可。
這種方式在需要監聽的描述符比較小,或者是每次就緒的描述符很多的情況下比較有效;但當描述符很多而且每次只有少數描述符就緒時,效率就比較低了。後面出現的epoll
就避免了這種線性遍歷的問題。
另外select
還受FD_SETSIZE
的限制,只能處理較少的描述符,而poll
則沒有這個限制。poll
監聽的集合大小隻受程序能開啟的檔案數量(RLIMIT_NOFILE
)的限制。