概述
在前兩篇文章中,我們介紹了如何使用select和poll進(jìn)行IO多路復(fù)用。select通常有一個固定的文件描述符數(shù)量上限(通常是1024),poll雖然沒有嚴(yán)格的文件描述符數(shù)量限制,但在實際使用中也可能受到系統(tǒng)資源的限制。相比之下,epoll支持非常大的文件描述符數(shù)量(理論上可以達(dá)到系統(tǒng)文件描述符的最大值),因此更適合高并發(fā)場景。在本篇中,我們將重點介紹epoll。
epoll
epoll是Linux特有的IO多路復(fù)用接口,專為大規(guī)模并發(fā)場景設(shè)計。epoll相比于select和poll有更高的性能,因為它可以處理大量的文件描述符,并且在獲取事件時不需要遍歷整個文件描述符集合。epoll的內(nèi)部實現(xiàn)中使用了紅黑樹和就緒鏈表兩種數(shù)據(jù)結(jié)構(gòu),以便高效地管理和查詢事件。
epoll的核心思想是:創(chuàng)建一個事件表,這個事件表會將所有需要監(jiān)控的文件描述符和它們對應(yīng)的事件關(guān)聯(lián)起來。當(dāng)某個文件描述符上的事件發(fā)生時,內(nèi)核會將該事件添加到就緒列表中。用戶程序只需要從就緒列表中讀取事件即可,而不需要像select或poll那樣每次都需要遍歷整個文件描述符集來查找哪些事件已經(jīng)就緒。
與epoll相關(guān)的系統(tǒng)API主要有三個:epoll_create、epoll_ctl、epoll_wait,下面分別進(jìn)行介紹。
1、epoll_create函數(shù)用于創(chuàng)建一個epoll實例,并返回一個新的文件描述符。在較新的Linux版本中,推薦使用epoll_create1函數(shù)。epoll_create1是epoll_create的改進(jìn)版,允許指定標(biāo)志位。
int epoll_create(int size);
int epoll_create1(int flags);
size:這個參數(shù)在現(xiàn)代內(nèi)核中已經(jīng)不再使用,但在調(diào)用時仍需要傳遞一個大于零的值。它是歷史遺留下來的,用于向后兼容。
flags:可以是0或EPOLL_CLOEXEC。EPOLL_CLOEXEC標(biāo)志表示執(zhí)行exec函數(shù)族時,關(guān)閉此文件描述符。
返回值:成功時返回新的文件描述符(非負(fù)整數(shù)),失敗時返回-1,并設(shè)置errno。
2、epoll_ctl函數(shù)用于向epoll實例添加、修改或刪除感興趣的文件描述符。
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
epfd:由epoll_create或epoll_create1創(chuàng)建的epoll文件描述符。
op:操作類型,可以是以下之一:
(1)EPOLL_CTL_ADD:將文件描述符fd添加到epoll實例中。
(2)EPOLL_CTL_MOD:修改文件描述符fd在epoll實例中的事件。
(3)EPOLL_CTL_DEL:從epoll實例中移除文件描述符fd。
fd:要操作的文件描述符。
event:指向epoll_event結(jié)構(gòu)體的指針,定義了監(jiān)聽的事件類型和關(guān)聯(lián)的數(shù)據(jù)。
返回值:成功時返回0,失敗時返回-1,并設(shè)置errno。
3、epoll_wait函數(shù)用于等待并獲取就緒的IO事件。
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
epfd:由epoll_create或epoll_create1創(chuàng)建的epoll文件描述符。
events:指向epoll_event數(shù)組的指針,用來存放發(fā)生的事件。
maxevents:events數(shù)組的最大長度。
timeout:等待時間,單位為毫秒。-1表示無限等待,0表示立即返回,不阻塞。
返回值:成功時返回發(fā)生的事件數(shù)量,失敗時返回-1,并設(shè)置errno。
邊緣觸發(fā)模式
epoll支持邊緣觸發(fā)(Edge-Triggered,即ET)模式,select和poll不支持,它們僅支持水平觸發(fā)模式(Level-Triggered,即LT)。邊緣觸發(fā)模式是一種高效的IO事件通知機(jī)制,它與水平觸發(fā)模式有所不同。在邊緣觸發(fā)模式下,epoll只會在文件描述符的狀態(tài)發(fā)生變化時通知應(yīng)用程序一次。這意味著,如果應(yīng)用程序沒有處理完所有數(shù)據(jù),內(nèi)核將不會再次通知該事件,直到新的數(shù)據(jù)到達(dá)或狀態(tài)再次發(fā)生變化。
epoll的邊緣觸發(fā)模式具有以下幾個特點。
1、僅在狀態(tài)變化時通知。當(dāng)文件描述符從不可讀變?yōu)榭勺x,或者從不可寫變?yōu)榭蓪憰r,epoll會通知應(yīng)用程序。如果應(yīng)用程序沒有完全讀取或?qū)懭胨袛?shù)據(jù),那么剩余的數(shù)據(jù)將不會再次觸發(fā)事件,直到有新的數(shù)據(jù)到來或狀態(tài)再次變化。
2、非阻塞IO。在邊緣觸發(fā)模式下,必須使用非阻塞IO。這是因為當(dāng)read或write調(diào)用返回EAGAIN或EWOULDBLOCK時,表示當(dāng)前沒有更多數(shù)據(jù)可以讀取或?qū)懭?,但并不意味著文件描述符已?jīng)不可讀或不可寫。應(yīng)用程序需要繼續(xù)嘗試讀取或?qū)懭?,直到所有?shù)據(jù)處理完畢。
3、更高的性能。由于邊緣觸發(fā)模式減少了內(nèi)核和用戶空間之間的上下文切換次數(shù),因此通常比水平觸發(fā)模式更高效,特別是在高并發(fā)場景中。
4、更復(fù)雜的編程模型。使用邊緣觸發(fā)模式需要更加小心地處理IO操作,以確保不會遺漏任何數(shù)據(jù)。
實戰(zhàn)代碼
在下面的示例代碼中,我們使用epoll函數(shù)實現(xiàn)了TCP服務(wù)器的IO多路復(fù)用。
首先,我們創(chuàng)建一個監(jiān)聽套接字listen_sock,將其綁定到指定的端口8888,并開始監(jiān)聽連接請求。
然后,我們使用epoll_create1創(chuàng)建一個epoll實例,并檢查是否成功。接著,將監(jiān)聽套接字listen_sock添加到 epoll實例中,并設(shè)置為邊緣觸發(fā)模式(EPOLLET)和可讀事件(EPOLLIN)。
在主循環(huán)中,我們使用epoll_wait函數(shù)等待IO事件的發(fā)生。epoll_wait會阻塞,直到有事件發(fā)生或超時,返回值nfds是就緒事件的數(shù)量。遍歷events數(shù)組中的每一個就緒事件,檢查每個事件的文件描述符fd是否為監(jiān)聽套接字listen_sock。
如果事件的文件描述符是監(jiān)聽套接字listen_sock,則表示有新的連接請求。調(diào)用accept接受新連接,并創(chuàng)建一個新的客戶端套接字conn_sock。將新連接的套接字添加到epoll實例中,并設(shè)置為邊緣觸發(fā)模式(EPOLLET)和可讀事件(EPOLLIN)。
如果事件的文件描述符不是監(jiān)聽套接字listen_sock,則表示該文件描述符上有數(shù)據(jù)可讀。調(diào)用read函數(shù)讀取數(shù)據(jù),并回顯給客戶端。如果讀取到0字節(jié),表示客戶端已斷開連接。如果讀取失敗,也認(rèn)為客戶端斷開連接。在這兩種情況下,關(guān)閉套接字,并從epoll實例中移除該套接字。
最后,當(dāng)程序退出時,關(guān)閉所有打開的套接字和epoll實例。
#include <iostream>
#include <sys/epoll.h>
#include <unistd.h>
#include <fcntl.h>
#include <cstring>
#include <sys/socket.h>
#include <netinet/in.h>
using namespace std;
#define MAX_EVENTS 10
int main()
{
int listen_sock = socket(AF_INET, SOCK_STREAM, 0);
if (listen_sock == -1)
{
cout << "Create socket failed" << endl;
return 1;
}
struct sockaddr_in server_addr;
memset(&server_addr, 0, sizeof(server_addr));
server_addr.sin_family = AF_INET;
server_addr.sin_port = htons(8888);
server_addr.sin_addr.s_addr = INADDR_ANY;
if (bind(listen_sock, (struct sockaddr*)&server_addr, sizeof(server_addr)) == -1)
{
cout << "Bind failed" << endl;
close(listen_sock);
return 1;
}
if (listen(listen_sock, 5) == -1)
{
cout << "Listen failed" << endl;
close(listen_sock);
return 1;
}
// 創(chuàng)建epoll實例
int epoll_fd = epoll_create1(0);
if (epoll_fd == -1)
{
cout << "Create epoll failed" << endl;
close(listen_sock);
return 1;
}
// 添加監(jiān)聽Socket
struct epoll_event ev, events[MAX_EVENTS];
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = listen_sock;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_sock, &ev);
while (true)
{
// 等待并獲取就緒的IO事件
int nfds = epoll_wait(epoll_fd, events, MAX_EVENTS, -1);
if (nfds == -1)
{
cout << "Wait epoll failed" << endl;
break;
}
for (int i = 0; i < nfds; ++i)
{
if (events[i].data.fd == listen_sock)
{
// 新連接
struct sockaddr_in client_addr;
socklen_t client_len = sizeof(client_addr);
int conn_sock = accept(listen_sock, (struct sockaddr*)&client_addr,
&client_len);
if (conn_sock != -1)
{
ev.events = EPOLLIN | EPOLLET;
ev.data.fd = conn_sock;
epoll_ctl(epoll_fd, EPOLL_CTL_ADD, conn_sock, &ev);
}
else
{
cout << "New connection" << endl;
}
}
else
{
// 處理數(shù)據(jù)
char buf[1024];
ssize_t nread = read(events[i].data.fd, buf, sizeof(buf));
if (nread > 0)
{
// 接收并回顯數(shù)據(jù)
cout << "Received data: " << string(buf, nread) << endl;
write(events[i].data.fd, buf, nread);
}
else if (nread == 0)
{
// 客戶端關(guān)閉連接
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, nullptr);
close(events[i].data.fd);
}
else
{
// 發(fā)生錯誤
cout << "Read error" << endl;
epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd, nullptr);
close(events[i].data.fd);
}
}
}
}
close(listen_sock);
close(epoll_fd);
return 0;
}