Select,Poll,Epoll是學習I/O多路復用必不可少的一節(jié),這章我將借用對這三個系統(tǒng)調(diào)用的講解,進一步加深對于I/O多路復用的理解
首先我們要知道為什么要有I/O多路復用,可以通過一次系統(tǒng)調(diào)用,檢查多個文件描述符的狀態(tài)。這是 I/O 多路復用的主要優(yōu)點,相比于非阻塞 I/O,在文件描述符較多的場景下,避免了頻繁的用戶態(tài)和內(nèi)核態(tài)的切換,減少了系統(tǒng)調(diào)用的開銷。
I/O 多路復用相當于將「遍歷所有文件描述符、通過非阻塞 I/O 查看其是否就緒」的過程從用戶線程移到了內(nèi)核中,由內(nèi)核來負責輪詢。
首先回憶一下,I/O多路復用是一種使用少數(shù)線程來監(jiān)聽多數(shù)網(wǎng)絡(luò)Socket的一種I/O方法,那么我們怎么使用I/O多路復用呢
Select
select使用一個固定大小的位圖來表示文件描述符fd的集合,調(diào)用select檢查這些fd的狀態(tài),每次調(diào)用select時都會重新構(gòu)建位圖,并將其傳遞給內(nèi)核,內(nèi)核來判斷是否有I/O已經(jīng)就緒
select的核心數(shù)據(jù)結(jié)構(gòu)是一個fd_set, 這是一個文件描述符集合,用來管理需要監(jiān)視的文件描述符
fd_set的核心是一個位圖(大小位1024),每一位對應著文件描述符的狀態(tài),1表示該描述符需要監(jiān)視,0表示該描述符不需要監(jiān)視
typedef __kernel_fd_set fd_set;
#define __FD_SETSIZE 1024
typedef struct {
unsigned long fds_bits[__FD_SETSIZE / (8 * sizeof(long))];
} __kernel_fd_set;
select的函數(shù)原型如下
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
可以看到,有三種fd_set
● readfds -- 可讀事件,用來監(jiān)視事件是否可讀
● writefds -- 可寫事件,用來監(jiān)視事件是否可寫
● exceptfds -- 描述符是否有異常情況
● nfds -- 要監(jiān)視的文件描述符的最大值+1
● timeout -- 可以選擇的監(jiān)視時間 可以是 阻塞(NULL),立即返回(0),或者指定的等待時間
1. 在調(diào)用select的是否我們需要把需要監(jiān)視的事件通過函數(shù)加入到對應的隊列中
2. 進入內(nèi)核態(tài)進行檢查,程序在調(diào)用select之后,內(nèi)核會歷遍對應的fd查看是否符合對應的狀態(tài)
a. 符合:如果符合就把事件加入到readfds當中去
b. 不符合:如果所有的都不符合就根據(jù)timeout來選擇等待的方式和事件
3. 返回,最后會返回符合要求的fd的數(shù)量
while (1) {
fd_set rfds;
fd_set wfds;
int32_t maxfd = 0, res = 0;
struct timeval timeout;
timeout.tv_sec = 0;
timeout.tv_usec = 500;
FD_ZERO(&rfds);
FD_ZERO(&wfds);
FD_SET(socket1, &rfds);
FD_SET(socket2, &rfds);
maxfd = socket1 > socket2 ? socket1 : socket2;
res = select(maxfd + 1, &rfds, NULL, NULL, &timeout);
if (res < 0 && errno != EINTR && errno != 0) {
// log it
return;
}
if (FD_ISSET(socket1, &rfds)) {
// do something
}
if (FD_ISSET(socket2, &rfds)) {
// do something
}
}
上面大致講解了如何使用select,這里其實我們可以很明顯的看出一個缺點的,
1. 就是我們并不返回符合要求的fd,而是把所有的fd都返回,所以返回到用戶態(tài)之后我們要進行fd的歷遍最終才能找到有相應的fd,這顯然是比較耗費事件的 |