一、IO多路複用處理數據報文
二、select
1. 簡介
select系統調用是用來讓我們的程序監視多個文件描述符的狀態變化的。程序會阻塞到select這裏,直到被監聽的文件描述符一個或者多個發生了狀態變化。
2. 函數原型
int select(int nfds, fd_set* read_fds, fd_set* write_fds,
fd_set* except_fds, struct timeval* timeout);
2.1 參數說明
- nfds:是需要監視的最大的文件描述符值+1
- read_sets:對應於需要檢測的可讀文件描述符的集合
- write_sets:對應於需要檢測的可寫文件描述符的集合
- except_sets:對應於需要檢測的異常文件描述符的集合
- timeout:用來設置select的等待時間
- NULL:表示不設置select的等待時間,select在監聽到描述符狀態變化之前將一直阻塞
- 0:表示僅檢測描述符集合的狀態,然後立即返回,並不等待外部事件的發生
- 特定的時間值:如果在這個設置的時間值內沒有事件發生,select將超時返回
2.2 fd_set結構說明
首先,我們可以看一下這個結構的定義:
/* The fd_set member is required to be an array of longs. */
typedef long int __fd_mask;
typedef struct{
/* something */
__fd_mask fds_bits[__FD_SETSIZE / __NFDBITS];
/* something */
};
注:上面代碼節選自<sys/select.h>中,其中我只保留了便於理解的部分。
從定義中我們可以看出,其實fd_set
結構就是一個long型數組,或者說,它代表一種數據結構----“位圖”。使用位圖中對應的位來表示要監視的文件描述符。
select提供了一組操縱位圖的接口:
void FD_CLR(int fd, fd_set *set); // 用來清除描述位圖set中相關fd的位
int FD_ISSET(int fd, fd_set *set); // 用來測試描述位圖set中相關fd的位是否爲真
void FD_SET(int fd, fd_set *set); // 用來設置描述位圖set中相關fd的位
void FD_ZERO(fd_set* set); // 用來清空位圖set中的所有位
2.3 timeval結構說明
這個結構的定義:
/* A time value that is accurate to the nearest
microsecond but also has a range of years.
*/
struct timeval{
__time_t tv_sec; /* Second. */
__suseconds_t tv_usec; /* Microseconds. */
};
注:上面代碼節選自<time.h>中,其中我只保留了便於理解的部分。
2.4 返回值說明
執行成功
- 返回文件描述符中狀態已經改變的描述符個數
其他結果
- 返回0,表示在參數傳入的timeout時間內沒有文件描述符狀態發生變化
- 返回-1,表示在執行中發生錯誤,錯誤原因存儲於errno中,errno可能的結果有以下幾種:
- EBADF:文件描述符無效或文件已關閉
- EINTR:此次select調用被信號打斷
- EINVAL:參數n爲負值
- ENOMEM:內存不足
3. 就緒條件
3.1 讀就緒
- socket內核中, 接收緩衝區中的字節數, 大於等於低水位標記SO_RCVLOWAT;
- socket TCP通信中, 對端關閉連接, 此時對該socket讀, 則返回0;
- 監聽的socket上有新的連接請求;
- socket上有未處理的錯誤。
3.2 寫就緒
-
socket內核中, 發送緩衝區中的可用字節數(發送緩衝區的空閒位置大小), 大於等於低水位標記
SO_SNDLOWAT;
-
socket的寫操作被關閉(close或者shutdown). 對一個寫操作被關閉的socket進行寫操作, 會觸發SIGPIPE
信號;
-
socket使用非阻塞connect連接成功或失敗之後;
-
socket上有未處理的錯誤。
4. 函數使用
使用select實現一個本地回顯程序。
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/select.h>
int main() {
fd_set read_fds;
FD_ZERO(&read_fds); //初始化fd_set結構
FD_SET(STDIN_FILENO, &read_fds); //監聽標準輸入
while(1){
printf("> ");
fflush(stdout);
int ret = select(STDIN_FILENO+1, &read_fds, NULL, NULL, NULL);
if(ret < 0){
perror("select");
continue;
}
if(FD_ISSET(STDIN_FILENO, &read_fds)){
char buf[1024] = {0};
read(STDIN_FILENO, buf, sizeof(buf) - 1); //讀取鍵盤輸入
printf("Echo: %s\n", buf);
} else {
printf("error! invalid fd\n");
continue;
}
FD_ZERO(&read_fds);
FD_SET(STDIN_FILENO, &read_fds);
}
return 0;
}
5. 函數特點
- 可監控的文件描述符個數取決與sizeof(fd_set)的值。
- 將fd加入select監控集的同時,還要再使用一個數據結構array保存放到select監控集中的fd,
- 一是用於再select返回後,array作爲源數據和fd_set進行FD_ISSET判斷。
- 二是select返回後會把以前加入的但並無事件發生的fd清空,則每次開始select前都要重新從array取得fd逐一加入(FD_ZERO最先),掃描array的同時取得fd最大值maxfd,用於select的第一個參數。
6. 函數缺點
- 每次調用select, 都需要手動設置fd集合, 從接口使用角度來說也非常不便.
- 每次調用select,都需要把fd集合從用戶態拷貝到內核態,這個開銷在fd很多時會很大
- 同時每次調用select都需要在內核遍歷傳遞進來的所有fd,這個開銷在fd很多時也很大
- select支持的文件描述符數量太小
三、poll
1. 函數原型
int poll(struct pollfd* fds, nfds_t nfds, int timeout);
1.1 參數說明
- fds:fds是一個poll函數監聽的結構列表
- nfds:表示fds數組的長度
- timeout:表示poll函數的超時時間, 單位是毫秒(ms)
1.2 pollfd結構說明
struct pollfd{
int fd; /* File descriptor to poll. */
short int events; /* Types of events poller cares about. */
short int revents; /* Types of events that actually occurred. */
};
注:上面代碼節選自<time.h>中
events和revents的取值列表:
事件 | 描述 | 是否可作爲輸入 | 是否可作爲輸出 |
---|---|---|---|
POLLIN | 數據可讀(包括普通數據和優先數據) | 是 | 是 |
POLLRDNORM | 普通數據可讀 | 是 | 是 |
POLLPRI | 高優先級數據可讀,eg: TCP帶外數據 | 是 | 是 |
POLLOUT | 數據可寫(包括普通數據和優先數據) | 是 | 是 |
POLLWRNORM | 普通數據可寫 | 是 | 是 |
POLLWRBAND | 優先級帶數據可寫 | 是 | 是 |
POLLRDHUP | TCP連接被對方關閉,或對方關閉了寫操作 | 是 | 是 |
POLLERR | 錯誤 | 否 | 是 |
POLLHUP | 掛起 | 否 | 是 |
POLLNVAL | 文件描述符未打開 | 否 | 是 |
1.3 返回值說明
執行成功
- 返回文件描述符中狀態已經改變的描述符個數
其他結果
- 返回0,表示在參數傳入的timeout時間內沒有文件描述符狀態發生變化
- 返回-1,表示在執行中發生錯誤
2. 就緒條件
同select
3. 函數使用
使用poll實現一個本地回顯程序。
#include <sys/poll.h>
#include <unistd.h>
#include <stdio.h>
int main(){
struct pollfd poll_fd;
poll_fd.fd = STDIN_FILEIN;
poll_fd.events = POLLIN;
while(1){
printf("> ");
fflush(stdout);
int ret = poll(&poll_fd, 1, -1);
if(0 == ret){
printf("poll timeout\n");
continue;
}else if(ret < 0){
perror("poll");
continue;
}
if(POLLIN == poll_fd.revents){
char buf[1024] = {0};
read(STDIN_FILENO, buf, sizeof(buf) - 1);
printf("stdin: %s", buf);
}
}
return 0;
}
4. 優點
- poll的接口使用比select更方便了一些
- poll監聽的文件描述符的數目理論上沒有上限,但是由於poll底層是採用遍歷檢測文件描述符是否就緒的方式,所以數量過大後性能還是會下降的
5. 缺點
- 和select函數一樣,poll返回後,需要輪詢pollfd來獲取就緒的描述符,監視的文件描述符超過一定數量,輪詢的時間開銷會線性增長
四、epoll
1. 函數原型
int epoll_create(int size);
1.1 epoll_create說明、
1.1.1 參數說明
在Linux2.6.8版本後,這個參數會被忽略。但是還是要注意,這個參數不可以傳入小於0的數。
1.1.2 返回值說明
epoll_create返回一個操作epoll的句柄。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
1.2 epoll_ctl說明
1.2.1 參數說明
- epfd:是epoll_create的返回值,即epoll的句柄。
- op:表示epoll_ctl函數的動作,有三個取值:
- EPOLL_CTL_ADD:註冊新的文件描述符fd到epfd中
- EPOLL_CTL_MOD:修改已經註冊到epfd的監聽事件
- EPOLL_CTL_DEL:從epfd中註銷一個文件描述符fd
- fd:需要監聽的文件描述符
- event:告訴內核要監聽什麼類型的事件
1.2.2 epoll_event結構說明
這個結構體的定義是這樣的:
typedef union epoll_data{
void* ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
struct epoll_event{
uint32_t events; /* Epoll events. */
epoll_data_t data; /* User data variable. */
} __EPOLL_PACKED;
上面代碼節選自<sys/epoll.h>中,我只保留了便於理解的部分。
其中events可以是以下幾個宏的集合:
- EPOLLIN:表示對應的文件描述符可以讀(包括對端socket正常關閉)
- EPOLLOUT:表示對應的文件描述符可以寫
- EPOLLPRI:表示對應的文件描述符有緊急數據可讀(帶外數據)
- EPOLLERR:表示對應的文件描述符發生錯誤
- EPOLLHUP:表示對應的文件描述符被掛斷
- EPOLLET:將EPOLL設置爲邊緣觸發(ET)模式,EPOLL默認爲水平觸發(LT)模式
- EPOLLONESHOT:只監聽一次事件,當監聽完這次事件之後,如果還要繼續監聽這個事件,需要重新把這個事件添加到EPOLL隊列中
int epoll_wait(int epfd, struct epoll_event* events, int max_events, int timeout);
1.3 epoll_wait說明
1.3.1 參數說明
- epfd:是epoll_create的返回值,即epoll的句柄。
- events:epoll會把已經發生的事件拷貝到events數組中(events不可以是空指針,內核只負責把數據拷貝到這個數組中,不會主動在用戶態分配內存存儲)
- max_events:events的大小,這個參數不能大於調用epoll_create時的傳入的size
- timeout:超時時間,單位是毫秒
- 這個參數填0,epoll_wait會立即返回
- 這個參數填-1,epoll_wait會永久阻塞,直到有事件發生
1.3.1 返回值說明
執行成功
- 返回文件描述符中狀態已經改變的描述符個數
其他結果
- 返回0,表示在參數傳入的timeout時間內沒有文件描述符狀態發生變化
- 返回<0,表示函數執行過程中發生錯誤
2. 工作原理
當進程調用epoll_create時,Linux內核會創建一個eventpoll結構體,這個結構體中有兩個成員和epoll的使用密切相關:
struct eventpoll{
/*something*/
/* 紅黑樹的根節點,這棵樹中存儲着所有添加到epoll中需要監控的事件。 */
struct rb_root rbr;
/* 雙鏈表的頭節點,雙鏈表中存儲着要通過epoll_wait返回給用戶的滿足條件的事件。 */
struct list_head rblist;
/*something*/
};
每一個epoll對象都會在內核中創建一個eventpoll結構體,通過epoll_ctl方法向epoll對象添加進來的事件,這些事件都會掛載到eventpoll中的紅黑樹上,事件的結構類型是這樣的:
struct epitem{
struct rb_node rbn; /* 紅黑樹節點 */
struct list_head rdllink; /* 雙向鏈表節點 */
struct epoll_filefd ffd; /* 事件句柄信息 */
struct eventpoll* ep; /* 指向其所屬的 eventpoll 對象 */
struct epoll_event event; /* 期待發生的事件類型 */
};
所有添加到epoll對象中的事件都會與設備驅動程序建立回調關係,當事件發生響應,就會調用這個回調方法。這個回調方法在內核中叫ep_poll_callback,它主要的作用是將發生響應的事件添加到eventpoll中的雙鏈表上。
當調用epoll_wait檢查事件是否發生的時候,只需要檢查eventpoll對象中的rdlist雙鏈表中是否存在epitem(即事件)元素即可。如果存在,則把rdlist中的事件拷貝到用戶態,同時通過返回值將rdlist中的元素數目(事件發生的個數)返回給用戶。
3. 工作模式
先看一下這個例子:
已經把一個tcp socket添加到epoll描述符中,這個時候socket的客戶端寫入了2KB數據,服務器端調用epoll_wait,epoll_wait返回,然後調用read,可在服務器端分配的緩衝區只有1KB,所以一次只讀取了1KB數據,繼續調用epoll_wait…
3.1 LT模式
當epoll檢測到socket上事件就緒的時候,可以選擇只處理一部分數據,或者不立即進行處理。例如上面的例子,由於第一次調用epoll_wait,服務器端只讀取了1KB數據,在第二次調用epoll_wait時,epoll_wait仍然會立即返回並通知socket讀事件就緒,直到socket緩衝區中所有的數據都讀取,epoll_wait纔不會因爲這個socket而立即返回。
LT模式支持阻塞讀寫和非阻塞讀寫。
注:select和poll其實就相當於是epoll的LT模式。
3.2 ET模式
當epoll_wait返回通知socket讀時間就緒時,必須立即處理,而且要一次處理完。例如上面的例子,第一次調用epoll_wait時,只讀取了1KB數據,那麼第二次調用epoll_wait時,epoll_wait就不會再返回。
ET模式支持非阻塞讀寫。注意:使用ET模式的時候,需要將監聽的文件描述符設置爲非阻塞。
注:Nginx默認用的就是epoll的ET模式。
4. 函數使用
/* epoll LT模式的回顯服務器 */
#include <stdio.h>
#include <string.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/epoll.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#define CHECK_ERROR(str, n) do{if(n < 0){ perror(str); exit(EXIT_FAILURE); }}while(0)
void ProcessConnect(int listen_fd, int epoll_fd){
struct sockaddr_in client_addr;
socklen_t len = sizeof(client_addr);
int connect_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &len);
if(connect_fd < 0){
perror("accept");
exit(EXIT_FAILURE);
}
printf("client %s:%d connect\n", inet_ntoa(client_addr.sin_addr), ntohs(client_addr.sin_port));
struct epoll_event ev;
ev.data.fd = connect_fd;
ev.events = EPOLLIN;
int ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, connect_fd, &ev);
if(ret < 0){
perror("epoll_ctl");
exit(EXIT_FAILURE);
}
}
void ProcessRequest(int connect_fd, int epoll_fd){
char buf[1024] = {0};
ssize_t read_size = read(connect_fd, buf, sizeof(buf) - 1);
if(read_size < 0){
perror("read");
return;
}
if(0 == read_size){
close(connect_fd);
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, connect_fd, NULL);
printf("client say: goodbye\n");
return;
}
printf("client say: %s\n", buf);
write(connect_fd, buf, strlen(buf));
}
void CorrectUsage(){
printf("Usage: ./epoll_server [ip] [port]\n");
}
int main(int argc, char* argv[]) {
if(argc != 3){
CorrectUsage();
exit(EXIT_FAILURE);
}
struct sockaddr_in addr;
addr.sin_family = AF_INET;
addr.sin_port = htons(atoi(argv[2]));
addr.sin_addr.s_addr = inet_addr(argv[1]);
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
CHECK_ERROR("socket", listen_fd);
int ret = bind(listen_fd, (struct sockaddr*)&addr, sizeof(addr));
CHECK_ERROR("bind", ret);
ret = listen(listen_fd, 10);
CHECK_ERROR("listen", ret);
int epoll_fd = epoll_create(5);
CHECK_ERROR("epoll_create", epoll_fd);
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = listen_fd;
ret = epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &event);
CHECK_ERROR("epoll_ctl", ret);
while(1){
struct epoll_event events[10];
int size = epoll_wait(epoll_fd, events, sizeof(events) / sizeof(events[0]), -1);
if(size < 0){
perror("epoll_wait");
continue;
}
if(0 == size){
printf("epoll timeout\n");
continue;
}
for(int i = 0; i < size; ++i){
if(!(events[i].events & EPOLLIN)){
continue;
}
if(events[i].data.fd == listen_fd){
ProcessConnect(listen_fd, epoll_fd);
}else{
ProcessRequest(events[i].data.fd, epoll_fd);
}
}
}
return 0;
}
5. 驚羣效應
參考:Epoll的驚羣效應
6. 優點
前面介紹了一下epoll的底層原理,可以看出它相對於之前的IO多路複用接口做了很多優化:
- 不用再通過輪詢的方式來檢測事件是否就緒,而是通過回調機制,大大縮短了輪詢的時間開銷
- 不用每次使用檢測事件是否就緒接口前都要拷貝之前的事件集合,大大減少了拷貝開銷
- 文件描述符的數目理論上沒有上限,不像select,“文件描述符的數目取決於 ’位圖‘ 的大小,想要更改 ‘位圖‘ 大小甚至還要重新編譯內核”
- 接口使用也方便了很多,不需要每次循環都要重新設置關注的文件描述符(eg:select)
7. 使用場景
對於多連接,並且多連接中只有一部分連接比較活躍時,比較適合用epoll。例如:各種互聯網APP的入口服務器,就很適合用epoll。