Linux epoll 模型
綜合 select 和 poll 的一些優缺點,Linux 從內核 2.6 版本開始引入了更高效的 epoll 模型,本節我們來詳細介紹 epoll 模型。
要想使用 epoll 模型,必須先需要創建一個 epollfd,這需要使用 epoll_create 函數去創建:
#include <sys/epoll.h>
int epoll_create(int size);
參數 size 從 Linux 2.6.8 以後就不再使用,但是必須設置一個大於 0 的值。epoll_create 函數調用成功返回一個非負值的 epollfd,調用失敗返回 -1。
有了 epollfd 之後,我們需要將我們需要檢測事件的其他 fd 綁定到這個 epollfd 上,或者修改一個已經綁定上去的 fd 的事件類型,或者在不需要時將 fd 從 epollfd 上解綁,這都可以使用 epoll_ctl 函數:
int epoll_ctl(int epfd, int op, int fd, struct epoll_event* event);
參數說明:
-
參數 epfd 即上文提到的 epollfd;
-
參數 op,操作類型,取值有 EPOLL_CTL_ADD、EPOLL_CTL_MOD 和 EPOLL_CTL_DEL,分別表示向 epollfd 上添加、修改和移除一個其他 fd,當取值是 EPOLL_CTL_DEL,第四個參數 event 忽略不計,可以設置爲 NULL;
-
參數 fd,即需要被操作的 fd;
-
參數 event,這是一個 epoll_event 結構體的地址,epoll_event 結構體定義如下:
struct epoll_event { uint32_t events; /* 需要檢測的 fd 事件,取值與 poll 函數一樣 */ epoll_data_t data; /* 用戶自定義數據 */ };
epoll_event 結構體的 data 字段的類型是 epoll_data_t,我們可以利用這個字段設置一個自己的自定義數據,它本質上是一個 Union 對象,在 64 位操作系統中其大小是 8 字節,其定義如下:
typedef union epoll_data { void* ptr; int fd; uint32_t u32; uint64_t u64; } epoll_data_t;
-
函數返回值:epoll_ctl 調用成功返回 0,調用失敗返回 -1,你可以通過 errno 錯誤碼獲取具體的錯誤原因。
創建了 epollfd,設置好某個 fd 上需要檢測事件並將該 fd 綁定到 epollfd 上去後,我們就可以調用 epoll_wait 檢測事件了,epoll_wait 函數簽名如下:
int epoll_wait(int epfd, struct epoll_event* events, int maxevents, int timeout);
參數的形式和 poll 函數很類似,參數 events 是一個 epoll_event 結構數組的首地址,這是一個輸出參數,函數調用成功後,events 中存放的是與就緒事件相關 epoll_event 結構體數組;參數 maxevents 是數組元素的個數;timeout 是超時時間,單位是毫秒,如果設置爲 0,epoll_wait 會立即返回。
當 epoll_wait 調用成功會返回有事件的 fd 數目;如果返回 0 表示超時;調用失敗返回 -1。
epoll_wait 使用示例如下:
while (true)
{
epoll_event epoll_events[1024];
int n = epoll_wait(epollfd, epoll_events, 1024, 1000);
if (n < 0)
{
//被信號中斷
if (errno == EINTR)
continue;
//出錯,退出
break;
}
else if (n == 0)
{
//超時,繼續
continue;
}
for (size_t i = 0; i < n; ++i)
{
// 處理可讀事件
if (epoll_events[i].events & POLLIN)
{
}
// 處理可寫事件
else if (epoll_events[i].events & POLLOUT)
{
}
//處理出錯事件
else if (epoll_events[i].events & POLLERR)
{
}
}
}
epoll_wait 與 poll 的區別
通過前面介紹 poll 與 epoll_wait 函數的介紹,我們可以發現:
epoll_wait 函數調用完之後,我們可以直接在 event 參數中拿到所有有事件就緒的 fd,直接處理即可(event 參數僅僅是個出參);而 poll 函數的事件集合調用前後數量都未改變,只不過調用前我們通過 pollfd 結構體的 events 字段設置待檢測事件,調用後我們需要通過 pollfd 結構體的 revents 字段去檢測就緒的事件( 參數 fds 既是入參也是出參)。
舉個生活中的例子,某人不斷給你一些蘋果,這些蘋果有生有熟,調用 epoll_wait 相當於:
1. 你把蘋果挨個投入到 epoll 機器中(調用 epoll_ctl);
2. 調用 epoll_wait 加工,你直接通過另外一個袋子就能拿到所有熟蘋果。
調用 poll 相當於:
1. 把收到的蘋果裝入一個袋子裏面然後調用 poll 加工;
2. 調用結束後,拿到原來的袋子,袋子中還是原來那麼多蘋果,只不過熟蘋果被貼上了標籤紙,你還是需要挨個去查看標籤紙挑選熟蘋果。
當然,這並不意味着,poll 函數的效率不如 epoll_wait,一般在 fd 數量比較多,但某段時間內,就緒事件 fd 數量較少的情況下,epoll_wait 纔會體現出它的優勢,也就是說 socket 連接數量較大時而活躍連接較少時 epoll 模型更高效。
LT 模式和 ET 模式
與 poll 的事件宏相比,epoll 新增了一個事件宏 EPOLLET,這就是所謂的邊緣觸發模式(Edge Trigger,ET),而默認的模式我們稱爲 水平觸發模式(Level Trigger,LT)。這兩種模式的區別在於:
- 對於水平觸發模式,一個事件只要有,就會一直觸發;
- 對於邊緣觸發模式,只有一個事件從無到有才會觸發。
這兩個詞彙來自電學術語,你可以將 fd 上有數據認爲是高電平,沒有數據認爲是低電平,將 fd 可寫認爲是高電平,fd 不可寫認爲是低電平。那麼水平模式的觸發條件是狀態處於高電平,而邊緣模式是狀態改爲高電平,即:
水平模式的觸發條件
1. 低電平 => 高電平
2. 高電平 => 高電平
邊緣模式的觸發條件
1. 低電平 => 高電平
說的有點抽象,以 socket 的讀事件爲例,對於水平模式,只要 socket 上有未讀完的數據,就會一直產生 POLLIN 事件;而對於邊緣模式,socket 上第一次有數據會觸發一次,後續 socket 上存在數據也不會再觸發,除非把數據讀完後,再次產生數據纔會繼續觸發。對於 socket 寫事件,如果 socket 的 TCP 窗口一直不飽和,會一直觸發 POLLOUT 事件;而對於邊緣模式,只會觸發一次,除非 TCP 窗口由不飽和變成飽和再一次變成不飽和,纔會再次觸發 POLLOUT 事件。
socket 可讀事件水平模式觸發條件:
1. socket上無數據 => socket上有數據
2. socket上有數據 => socket上有數據
socket 可讀事件邊緣模式觸發條件:
1. socket上無數據 => socket上有數據
socket 可寫事件水平模式觸發條件:
1. socket可寫 => socket可寫
2. socket不可寫 => socket可寫
socket 可寫事件邊緣模式觸發條件:
1. socket不可寫 => socket可寫
也就是說,如果對於一個非阻塞 socket,如果使用 epoll 邊緣模式去檢測數據是否可讀,觸發可讀事件以後,一定要一次性把 socket 上的數據收取乾淨才行,也就是一定要循環調用 recv 函數直到 recv 出錯,錯誤碼是EWOULDBLOCK(EAGAIN 一樣);如果使用水平模式,則不用,你可以根據業務一次性收取固定的字節數,或者收完爲止。邊緣模式下收取數據的代碼示例如下:
bool TcpSession::RecvEtMode()
{
//每次只收取256個字節
char buff[256];
while (true)
{
int nRecv = ::recv(clientfd_, buff, 256, 0);
if (nRecv == -1)
{
if (errno == EWOULDBLOCK)
return true;
else if (errno == EINTR)
continue;
return false;
}
//對端關閉了socket
else if (nRecv == 0)
return false;
inputBuffer_.add(buff, (size_t)nRecv);
}
return true;
}
最後,我們來看一個 epoll 模型的完整例子:
/**
* 演示 epoll 通信模型,epoll_server.cpp
* zhangyl 2019.03.16
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <unistd.h>
#include <fcntl.h>
#include <sys/epoll.h>
#include <poll.h>
#include <iostream>
#include <string.h>
#include <vector>
#include <errno.h>
int main(int argc, char* argv[])
{
//創建一個偵聽socket
int listenfd = socket(AF_INET, SOCK_STREAM, 0);
if (listenfd == -1)
{
std::cout << "create listen socket error." << std::endl;
return -1;
}
//將偵聽socket設置爲非阻塞的
int oldSocketFlag = fcntl(listenfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if (fcntl(listenfd, F_SETFL, newSocketFlag) == -1)
{
close(listenfd);
std::cout << "set listenfd to nonblock error." << std::endl;
return -1;
}
//初始化服務器地址
struct sockaddr_in bindaddr;
bindaddr.sin_family = AF_INET;
bindaddr.sin_addr.s_addr = htonl(INADDR_ANY);
bindaddr.sin_port = htons(3000);
if (bind(listenfd, (struct sockaddr *)&bindaddr, sizeof(bindaddr)) == -1)
{
std::cout << "bind listen socket error." << std::endl;
close(listenfd);
return -1;
}
//啓動偵聽
if (listen(listenfd, SOMAXCONN) == -1)
{
std::cout << "listen error." << std::endl;
close(listenfd);
return -1;
}
//複用地址和端口號
int on = 1;
setsockopt(listenfd, SOL_SOCKET, SO_REUSEADDR, (char *)&on, sizeof(on));
setsockopt(listenfd, SOL_SOCKET, SO_REUSEPORT, (char *)&on, sizeof(on));
//創建epollfd
int epollfd = epoll_create(1);
if (epollfd == -1)
{
std::cout << "create epollfd error." << std::endl;
close(listenfd);
return -1;
}
epoll_event listen_fd_event;
listen_fd_event.events = POLLIN;
listen_fd_event.data.fd = listenfd;
//將偵聽socket綁定到epollfd上去
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, listenfd, &listen_fd_event) == -1)
{
std::cout << "epoll_ctl error." << std::endl;
close(listenfd);
return -1;
}
int n;
while (true)
{
epoll_event epoll_events[1024];
n = epoll_wait(epollfd, epoll_events, 1024, 1000);
if (n < 0)
{
//被信號中斷
if (errno == EINTR)
continue;
//出錯,退出
break;
}
else if (n == 0)
{
//超時,繼續
continue;
}
for (size_t i = 0; i < n; ++i)
{
// 事件可讀
if (epoll_events[i].events & POLLIN)
{
if (epoll_events[i].data.fd == listenfd)
{
//偵聽socket,接受新連接
struct sockaddr_in clientaddr;
socklen_t clientaddrlen = sizeof(clientaddr);
//接受客戶端連接, 並加入到fds集合中
int clientfd = accept(listenfd, (struct sockaddr *)&clientaddr, &clientaddrlen);
if (clientfd != -1)
{
//將客戶端socket設置爲非阻塞的
int oldSocketFlag = fcntl(clientfd, F_GETFL, 0);
int newSocketFlag = oldSocketFlag | O_NONBLOCK;
if (fcntl(clientfd, F_SETFL, newSocketFlag) == -1)
{
close(clientfd);
std::cout << "set clientfd to nonblock error." << std::endl;
}
else
{
epoll_event client_fd_event;
client_fd_event.events = POLLIN;
client_fd_event.data.fd = clientfd;
if(epoll_ctl(epollfd, EPOLL_CTL_ADD, clientfd, &client_fd_event) != -1)
{
std::cout << "new client accepted, clientfd: " << clientfd << std::endl;
}
else
{
std::cout << "add client fd to epollfd error." << std::endl;
close(clientfd);
}
}
}
}
else
{
//普通clientfd,收取數據
char buf[64] = { 0 };
int m = recv(epoll_events[i].data.fd, buf, 64, 0);
if (m == 0)
{
//對端關閉了連接,從epollfd上移除clientfd
if(epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)
{
std::cout << "client disconnected, clientfd: " << epoll_events[i].data.fd << std::endl;
}
close(epoll_events[i].data.fd);
}
else if (m < 0)
{
//出錯,從epollfd上移除clientfd
if (errno != EWOULDBLOCK && errno != EINTR)
{
if(epoll_ctl(epollfd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)
{
std::cout << "client disconnected, clientfd: " << epoll_events[i].data.fd << std::endl;
}
close(epoll_events[i].data.fd);
}
}
else
{
//正常收到數據
std::cout << "recv from client: " << buf << ", clientfd: " << epoll_events[i].data.fd << std::endl;
}
}
}
else if (epoll_events[i].events & POLLERR)
{
//TODO: 暫且不處理
}
}// end outer-for-loop
}// end while-loop
//關閉偵聽socket
//(理論上應該關閉包括所有clientfd在內的fd,但這裏只是爲了演示問題,就不寫額外的代碼來處理啦)
close(listenfd);
return 0;
}
編譯上述程序生成 epoll_server 並啓動,然後使用 nc 命令啓動三個客戶端給服務器發數據效果如下圖所示:
本文首發於『easyserverdev』公衆號,歡迎關注,轉載請保留版權信息。
歡迎加入高性能服務器開發 QQ 羣一起交流: 578019391 。