一、select
select函數
int select(int nfds, fd_set *readfds, fd_set *writefds,
fd_set *exceptfds, struct timeval *timeout);
參數和返回值
maxfd:監視對象文件描述符數量。
readset: 將所有關注“是否存在待讀取數據”的文件描述符註冊到fd_set變量,並傳遞其地址值。
writeset: 將所有關注“是否可傳輸無阻塞數據”的文件描述符註冊到fd_set變量,並傳遞其地址值。
exceptset: 將所有關注“是否發生異常”的文件描述符註冊到fd_set變量,並傳遞其地址值。
timeout: 調用select後,爲防止陷入無限阻塞狀態,傳遞超時信息。
返回值:錯誤返回-1,超時返回0。因關注的事件返回時,返回大於0的值,該值是發生事件的文件描述符數。
四個宏定義
FD_ZERO(fd_set* fdset): 將fd_set變量的所有位初始化爲0。
FD_SET(int fd, fd_set* fdset):在參數fdset指向的變量中註冊文件描述符fd的信息。
FD_CLR(int fd, fd_set* fdset):參數fdset指向的變量中清除文件描述符fd的信息。
FD_ISSET(int fd, fd_set* fdset): 若參數fdset指向的變量中包含文件描述符fd的信息,則返回真。
select方法底層
我們可以把三個參數都看成三個數組:讀事件數組,寫事件數組,異常事件數組
我們需要維護這三個數組,這三個數組存的都是fd,socket的文件描述符
在timeout時間裏,當有其中一個文件描述符有讀寫的io事件發生,內核就會把這三個數組都返回給我們用戶
並且用戶必須無差別輪詢判斷,到底哪個io發生了讀寫事件,再去進行對應的操作(accpet從數組中添加fd,disconnected從數組中刪除fd等...)
然後我們再去把三個數組交給內核去管理
缺點
-
每次都要把這三個數組從用戶態拷貝到內核態
-
每次都要無差別輪詢這三個數組。我們如果有500個連接,那麼就得輪詢500次
-
支持最多1024個連接。
-
只有水平觸法一種方式
二、poll
poll函數
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
參數與返回值
1)第一個參數:一個結構數組,struct pollfd結構如下:
struct pollfd{
int fd; //文件描述符
short events; //請求的事件
short revents; //返回的事件
};
2)第二個參數, 要監視的描述符的數目
3)第三個參數,超時時間
4)返回值,返回的事件個數,=0超時,-1失敗
與select相同,我們需要管理 fds鏈表,這個結構體裏面存着我們感興趣的事件和我們的對應的文件描述符
同樣我們需要無差別地輪詢 fds鏈表。但是沒有最大鏈接數限制了
缺點:
-
與select不同的是不受最大連接數限制了
-
還是需要去無差別輪詢,從內核態拷貝到用戶態,再從內核態到用戶態。效率低
三、epoll
int epoll_create(int size);
參數返回值:
1.size:最大連接數,即內核最大監聽數目
2.返回值:epoll_fd epoll文件描述符
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
參數返回值:
1.epfd:epoll_create獲取到的文件描述符
2.op:操作,有三個宏定義
EPOLL_CTL_ADD:註冊新的fd到epfd中;(添加到紅黑樹上)
EPOLL_CTL_MOD:修改已經註冊的fd的監聽事件;(譬如讀/寫事件修改)
EPOLL_CTL_DEL:從epfd中刪除一個fd;(從紅黑樹上剔除)
3.fd:要操作的文件描述符(server_fd和accpet_fd)
4.event:
struct epoll_event {
__uint32_t events; /* Epoll events */ epoll事件
epoll_data_t data; /* User data variable */ 用戶傳參
};
typedef union epoll_data {
void *ptr; // 用戶自定義數據結構
int fd; // 文件描述符
uint32_t u32;
uint64_t u64;
} epoll_data_t;
5.返回值:0成功 -1失敗
int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);
參數返回值
1.返回值:本次通知返回的事件數目
2.epfd:epoll_create返回值
3.events:返回的所有事件
4.maxevents:用戶設置內核最大事件返回數目
5.timeout:超時,-1不超時
與前兩個io多路複用不同的是,所有的用戶要交給內核管理的文件描述符,都存在於內核,
當文件描述符有事件發生時,內核哪個文件描述符發生的events事件給用戶,而不是將所有的文件描述符的所有events事件都交給用戶
好處是:
-
用戶不需要再無差別輪詢,遍歷的events數組都是發生了事件的
-
不需要頻繁地從用戶態拷貝大量文件描述符到內核態,再從內核態拷貝到用戶態
-
epoll底層內核用的是紅黑樹,效率極高
-
epoll提供水平模式(用戶不處理events會不斷觸發事件)和邊緣觸發模式(用戶不處理本次事件,本次事件內核不再提醒)
-
select和poll每次要拷貝數組到內核中。而epoll,所有的event都存在於內核的紅黑樹上,用戶通過epoll_ctl接口來操作紅黑樹上的節點
用戶通過epoll_create獲取ep_fd,然後用戶通過epoll_ctl來操作,用戶填充epoll_event結構體,結構體中由epoll_data中的void* ptr給用戶提供傳參(一般傳入一個結構體,結構體中有回調方法)。
epoll_ctl提供三種操作:註冊/修改/刪除
epoll_wait等待內核返回events事件
內核的epoll數據結構:紅黑樹+雙向鏈表
每當我們用epoll_ctl註冊時,都會在紅黑樹上添加一個event節點(紅黑樹極快定位哪個節點發生了讀寫事件)
當一個紅黑樹上的節點發生了事件,會被添加到雙向鏈表中,拷貝給用戶。
總結:
1)select和poll每次都要用戶無差別輪詢到底哪個發生了讀寫事件,內核只管通知,不會告訴用戶到底哪個事件就緒了。無差別輪詢:fd越多,遍歷次數越多,效率越低
2)select和poll每次都要將數組從用戶拷貝到內核中,用戶態/內核態來回切換,內存拷貝,效率極低。
3)poll用的是鏈表形式,而select則用的是數組形式,select的監聽數目受限制,最大1024,而poll不受到限制
4)epoll的所有fd都交給內核去管理,用戶通過epoll_ctl接口去操作紅黑樹上的節點,內核返回的events都是就緒的events。效率因爲充斥大量未就緒事件受影響
5)epoll由於所有fd都交給了內核去管理,少了用戶態向內核態數據的拷貝,效率提高不少
雖然在epoll的性能最好,但是在連接數非常少並且連接都十分活躍的情況下,select/poll的性能可能比epoll高。因爲epoll的通知機制需要很多函數的回調
最後附上一張epoll調用原理