簡介
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 {
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),這時讀取操作返回大於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_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
poll
和select
的功能類似,也支援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
的完成視為使對應的套接字可寫。
總結
select
和poll
都支援IO複用,其思路都是呼叫函式監聽多個描述符,當有描述符就緒或者超時的時候函式呼叫就會返回,對應的描述符集合狀態也會改變,這時候再遍歷描述符集合,處理其中就緒的部分即可。
這種方式在需要監聽的描述符比較小,或者是每次就緒的描述符很多的情況下比較有效;但當描述符很多而且每次只有少數描述符就緒時,效率就比較低了。後面出現的epoll
就避免了這種線性遍歷的問題。
另外select
還受FD_SETSIZE
的限制,只能處理較少的描述符,而poll
則沒有這個限制。poll
監聽的集合大小隻受程式能開啟的檔案數量(RLIMIT_NOFILE
)的限制。