Select,Poll,Epoll是學習I/O多路復用必不可少的一節,這章我將借用對這三個系統調用的講解,進一步加深對于I/O多路復用的理解
首先我們要知道為什么要有I/O多路復用,可以通過一次系統調用,檢查多個文件描述符的狀態。這是 I/O 多路復用的主要優點,相比于非阻塞 I/O,在文件描述符較多的場景下,避免了頻繁的用戶態和內核態的切換,減少了系統調用的開銷。
I/O 多路復用相當于將「遍歷所有文件描述符、通過非阻塞 I/O 查看其是否就緒」的過程從用戶線程移到了內核中,由內核來負責輪詢。
首先回憶一下,I/O多路復用是一種使用少數線程來監聽多數網絡Socket的一種I/O方法,那么我們怎么使用I/O多路復用呢
Select
select使用一個固定大小的位圖來表示文件描述符fd的集合,調用select檢查這些fd的狀態,每次調用select時都會重新構建位圖,并將其傳遞給內核,內核來判斷是否有I/O已經就緒
select的核心數據結構是一個fd_set, 這是一個文件描述符集合,用來管理需要監視的文件描述符
fd_set的核心是一個位圖(大小位1024),每一位對應著文件描述符的狀態,1表示該描述符需要監視,0表示該描述符不需要監視
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的函數原型如下
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
可以看到,有三種fd_set
● readfds -- 可讀事件,用來監視事件是否可讀
● writefds -- 可寫事件,用來監視事件是否可寫
● exceptfds -- 描述符是否有異常情況
● nfds -- 要監視的文件描述符的最大值+1
● timeout -- 可以選擇的監視時間 可以是 阻塞(NULL),立即返回(0),或者指定的等待時間
1. 在調用select的是否我們需要把需要監視的事件通過函數加入到對應的隊列中
2. 進入內核態進行檢查,程序在調用select之后,內核會歷遍對應的fd查看是否符合對應的狀態
a. 符合:如果符合就把事件加入到readfds當中去
b. 不符合:如果所有的都不符合就根據timeout來選擇等待的方式和事件
3. 返回,最后會返回符合要求的fd的數量
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都返回,所以返回到用戶態之后我們要進行fd的歷遍最終才能找到有相應的fd,這顯然是比較耗費事件的 |