epoll的觸發模式是個引發討論非常多的話題,網絡上這方面總結的文章也很多,首先從名字上就不是很統一,LT模式常被稱為水平觸發、電平觸發、條件觸發,而ET模式常被稱為邊緣觸發、邊沿觸發等,這些都是從英文翻譯過來的,只不過翻譯的時候有些差異,LT全稱 level-triggered,ET全稱 edge-triggered。
雖然這個知識點熱度很高,但很多人對于它的理解總是差那么一點,特別是在面試的時候,很多面試者總是處于一種回憶和背誦的狀態,其實這兩種模式真的不需要去死記硬背,下面說說我個人對這兩種模式的理解和記憶方法。
名稱的記憶
每次提到ET(邊沿觸發)首先映入我腦海的是大學里《數字邏輯電路》這門課程,里面會提到低電平、高電平,當電平從低到高時會有一個上升沿,而電平從高到低時會有一個下降沿,這個“沿”就是邊沿觸發時提到的“邊沿”,跟馬路邊的馬路牙子是同一種概念,也就是指狀態變化的時候。提起上升沿和下降沿我還是印象很深的,當時我可是占用了好幾節課的時間用Verilog語言寫了一個顯示“HELLO WORLD”的仿真波形,依靠的就是電平變化中的“沿”。
狀態變化
LT模式和ET模式可以類比電平變化來學習,但是在實際應用中概念卻不是完全一樣的,在epoll的應用中涉及到關于IO的讀寫,而讀寫的狀態變化有哪些呢?可讀、不可讀、可寫、不可寫,其實就是這四種狀態而已,以socket為例。
可讀:socket上有數據
不可讀:socket上沒有數據了
可寫:socket上有空間可寫
不可寫:socket上無空間可寫
對于水平觸發模式,一個事件只要有,就會一直觸發。對于邊緣觸發模式,只有一個事件從無到有才會觸發。
LT模式
對于讀事件 EPOLLIN,只要socket上有未讀完的數據,EPOLLIN 就會一直觸發;對于寫事件 EPOLLOUT,只要socket可寫(一說指的是 TCP 窗口一直不飽和,我覺得是TCP緩沖區未滿時,這一點還需驗證),EPOLLOUT 就會一直觸發。
在這種模式下,大家會認為讀數據會簡單一些,因為即使數據沒有讀完,那么下次調用epoll_wait()時,它還會通知你在上沒讀完的文件描述符上繼續讀,也就是人們常說的這種模式不用擔心會丟失數據。
而寫數據時,因為使用 LT 模式會一直觸發 EPOLLOUT 事件,那么如果代碼實現依賴于可寫事件觸發去發送數據,一定要在數據發送完之后移除檢測可寫事件,避免沒有數據發送時無意義的觸發。
ET模式
對于讀事件 EPOLLIN,只有socket上的數據從無到有,EPOLLIN 才會觸發;對于寫事件 EPOLLOUT,只有在socket寫緩沖區從不可寫變為可寫,EPOLLOUT 才會觸發(剛剛添加事件完成調用epoll_wait時或者緩沖區從滿到不滿)
這種模式聽起來清爽了很多,只有狀態變化時才會通知,通知的次數少了自然也會引發一些問題,比如觸發讀事件后必須把數據收取干凈,因為你不一定有下一次機會再收取數據了,即使不采用一次讀取干凈的方式,也要把這個激活狀態記下來,后續接著處理,否則如果數據殘留到下一次消息來到時就會造成延遲現象。
這種模式下寫事件觸發后,后續就不會再觸發了,如果還需要下一次的寫事件觸發來驅動發送數據,就需要再次注冊一次檢測可寫事件。
數據的讀取和發送
關于數據的讀比較好理解,無論是LT模式還是ET模式,監聽到讀事件從socket開始讀數據就好了,只不過讀的邏輯有些差異,LT模式下,讀事件觸發后,可以按需收取想要的字節數,不用把本次接收到的數據收取干凈,ET模式下,讀事件觸發后通常需要數據一次性收取干凈。
而數據的寫不太容易理解,因為數據的讀是對端發來數據導致的,而數據的寫其實是自己的邏輯層觸發的,所以在通過網絡發數據時通常都不會去注冊監可寫事件,一般都是調用 send 或者 write 函數直接發送,如果發送過程中, 函數返回 -1,并且錯誤碼是 EWOULDBLOCK 表明發送失敗,此時才會注冊監聽可寫事件,并將剩余的服務存入自定義的發送緩沖區中,等可寫事件觸發后再接著將發送緩沖區中剩余的數據發送出去。
代碼實踐
基礎代碼
以下為一個epoll觸發模式測試的基礎代碼,也不算太長,直接拿來就可以測試:
#include //for htonl htons
#include //for epoll_ctl
#include //for close
#include //for fcntl
#include //for errno
#include //for cout
class fd_object
{
public:
fd_object(int fd) { listen_fd = fd; }
~fd_object() { close(listen_fd); }
private:
int listen_fd;
};
/*
./epoll for lt mode
and
./epoll 1 for et mode
*/
int main(int argc, char* argv[])
{
//create a socket fd
int listen_fd = socket(AF_INET, SOCK_STREAM, 0);
if (listen_fd == -1)
{
std::cout << "create listen socket fd error." << std::endl;
return -1;
}
fd_object obj(listen_fd);
//set socket to non-block
int socket_flag = fcntl(listen_fd, F_GETFL, 0);
socket_flag |= O_NONBLOCK;
if (fcntl(listen_fd, F_SETFL, socket_flag) == -1)
{
std::cout << "set listen fd to nonblock error." << std::endl;
return -1;
}
//init server bind info
int port = 51741;
struct sockaddr_in bind_addr;
bind_addr.sin_family = AF_INET;
bind_addr.sin_addr.s_addr = htonl(INADDR_ANY);
bind_addr.sin_port = htons(port);
if (bind(listen_fd, (struct sockaddr *)&bind_addr, sizeof(bind_addr)) == -1)
{
std::cout << "bind listen socket fd error." << std::endl;
return -1;
}
//start listen
if (listen(listen_fd, SOMAXCONN) == -1)
{
std::cout << "listen error." << std::endl;
return -1;
}
else
std::cout << "start server at port [" << port << "] with [" << (argc <= 1 ? "LT" : "ET") << "] mode." << std::endl;
//create a epoll fd
int epoll_fd = epoll_create(88);
if (epoll_fd == -1)
{
std::cout << "create a epoll fd error." << std::endl;
return -1;
}
epoll_event listen_fd_event;
listen_fd_event.data.fd = listen_fd;
listen_fd_event.events = EPOLLIN;
if (argc > 1) listen_fd_event.events |= EPOLLET;
//add epoll event for listen fd
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, listen_fd, &listen_fd_event) == -1)
{
std::cout << "epoll ctl error." << std::endl;
return -1;
}
while (true)
{
epoll_event epoll_events[1024];
int n = epoll_wait(epoll_fd, epoll_events, 1024, 1000);
if (n < 0)
break;
else if (n == 0) //timeout
continue;
for (int i = 0; i < n; ++i)
{
if (epoll_events[i].events & EPOLLIN)//trigger read event
{
if (epoll_events[i].data.fd == listen_fd)
{
//accept a new connection
struct sockaddr_in client_addr;
socklen_t client_addr_len = sizeof(client_addr);
int client_fd = accept(listen_fd, (struct sockaddr*)&client_addr, &client_addr_len);
if (client_fd == -1)
continue;
socket_flag = fcntl(client_fd, F_GETFL, 0);
socket_flag |= O_NONBLOCK;
if (fcntl(client_fd, F_SETFL, socket_flag) == -1)
{
close(client_fd);
std::cout << "set client fd to non-block error." << std::endl;
continue;
}
epoll_event client_fd_event;
client_fd_event.data.fd = client_fd;
client_fd_event.events = EPOLLIN | EPOLLOUT;
if (argc > 1) client_fd_event.events |= EPOLLET;
if (epoll_ctl(epoll_fd, EPOLL_CTL_ADD, client_fd, &client_fd_event) == -1)
{
std::cout << "add client fd to epoll fd error." << std::endl;
close(client_fd);
continue;
}
std::cout << "accept a new client fd [" << client_fd << "]." << std::endl;
}
else
{
std::cout << "EPOLLIN event triggered for client fd [" << epoll_events[i].data.fd << "]." << std::endl;
char recvbuf[1024] = { 0 };
int m = recv(epoll_events[i].data.fd, recvbuf, 1, 0); // only read 1 bytes when read event triggered
if (m == 0 || (m < 0 && errno != EWOULDBLOCK && errno != EINTR))
{
if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)
std::cout << "the client fd [" << epoll_events[i].data.fd << "] disconnected." << std::endl;
close(epoll_events[i].data.fd);
}
std::cout << "recv data from client fd [" << epoll_events[i].data.fd << "] and data is [" << recvbuf << "]." << std::endl;
}
}
else if (epoll_events[i].events & EPOLLOUT)
{
if (epoll_events[i].data.fd == listen_fd) //trigger write event
continue;
std::cout << "EPOLLOUT event triggered for client fd [" << epoll_events[i].data.fd << "]." << std::endl;
}
}
}
return 0;
}
簡單說下這段代碼的測試方法,可以使用 g++ testepoll.cpp -o epoll 進行編譯,編譯后通過 ./epoll 運行為LT模式,通過 ./epoll et模式運行為ET模式,我們用編譯好的epoll程序作為服務器,使用nc命令來模擬一個客戶端。
測試分類
1.編譯后直接./epoll,然后在另一個命令行窗口用 nc -v 127.0.0.1 51741 命令模擬一次連接,此時 ./epoll 會產生大量的 EPOLLOUT event triggered for client fd ...,那是因為在LT模式下,EPOLLOUT會被一直觸發。
start server at port [51741] with [LT] mode.
accept a new client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLOUT event triggered for client fd [5].
...
2.注釋包含 EPOLLOUT event triggered for client fd 輸出內容的第152行代碼,編譯后 ./epoll運行,然后在另一個命令行窗口用 nc -v 127.0.0.1 51741 模擬一次連接后,輸入abcd回車,可以看到服務器./epoll輸出內容,EPOLLIN被觸發多次,每次讀取一個字節。
start server at port [51741] with [LT] mode.
accept a new client fd [5].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [a].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [b].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [c].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [d].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].
3.還原剛才注釋的那行代碼,編譯后執行 ./epoll et 啟動服務器,然后在另一個命令行窗口用 nc -v 127.0.0.1 51741 模擬一次連接后,然后在另一個命令行窗口用 nc -v 127.0.0.1 51741 模擬一次連接,服務器窗口顯示觸發了EPOLLOUT事件
start server at port [51741] with [ET] mode.
accept a new client fd [5].
EPOLLOUT event triggered for client fd [5].
在此基礎上,從剛剛運行nc命令的窗口中輸入回車、輸入回車、輸出回車,那么epoll服務器窗口看到的是觸發了三次EPOLLIN事件,每次收到一個回車:
start server at port [51741] with [ET] mode.
accept a new client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].
但是如果在nc模擬的客戶端里輸出abcd回車,那么在epoll服務器窗口觸發一次EPOLLIN事件接收到一個a之后便再也不會觸發EPOLLIN了,即使你在nc客戶端在此輸入也沒有用,那是因為在接受的緩沖區中一直還有數據,新數據來時沒有出現緩沖區從空到有數據的情況,所以在ET模式下也注意這種情況。
start server at port [51741] with [ET] mode.
accept a new client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [a].
怎么解決ET觸發了一次就不再觸發了
改代碼唄,ET模式在連接后觸發一次EPOLLOUT,接收到數據時觸發一次EPOLLIN,如果數據沒收完,以后這兩個事件就再也不會被觸發了,要想改變這種情況可以再次注冊一下這兩個事件,時機可以選擇接收到數據的時候,所以可以修改這部分代碼:
{
std::cout << "EPOLLIN event triggered for client fd [" << epoll_events[i].data.fd << "]." << std::endl;
char recvbuf[1024] = { 0 };
int m = recv(epoll_events[i].data.fd, recvbuf, 1, 0); // only read 1 bytes when read event triggered
if (m == 0 || (m < 0 && errno != EWOULDBLOCK && errno != EINTR))
{
if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)
std::cout << "the client fd [" << epoll_events[i].data.fd << "] disconnected." << std::endl;
close(epoll_events[i].data.fd);
}
std::cout << "recv data from client fd [" << epoll_events[i].data.fd << "] and data is [" << recvbuf << "]." << std::endl;
}
添加再次注冊的邏輯:
{
std::cout << "EPOLLIN event triggered for client fd [" << epoll_events[i].data.fd << "]." << std::endl;
char recvbuf[1024] = { 0 };
int m = recv(epoll_events[i].data.fd, recvbuf, 1, 0); // only read 1 bytes when read event triggered
if (m == 0 || (m < 0 && errno != EWOULDBLOCK && errno != EINTR))
{
if (epoll_ctl(epoll_fd, EPOLL_CTL_DEL, epoll_events[i].data.fd, NULL) != -1)
std::cout << "the client fd [" << epoll_events[i].data.fd << "] disconnected." << std::endl;
close(epoll_events[i].data.fd);
}
epoll_event client_fd_event;
client_fd_event.data.fd = epoll_events[i].data.fd;
client_fd_event.events = EPOLLIN | EPOLLOUT;
if (argc > 1) client_fd_event.events |= EPOLLET;
epoll_ctl(epoll_fd, EPOLL_CTL_MOD, epoll_events[i].data.fd, &client_fd_event);
std::cout << "recv data from client fd [" << epoll_events[i].data.fd << "] and data is [" << recvbuf << "]." << std::endl;
}
這次以./epoll et方式啟動服務器,使用nc -v 127.0.0.1 51741模擬客戶端,輸入abc回車發現,epoll服務器輸出顯示觸發的事件變了:
start server at port [51741] with [ET] mode.
accept a new client fd [5].
EPOLLOUT event triggered for client fd [5].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [a].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [b].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [c].
EPOLLIN event triggered for client fd [5].
recv data from client fd [5] and data is [
].
EPOLLOUT event triggered for client fd [5].
總結
- LT模式會一直觸發EPOLLOUT,當緩沖區有數據時會一直觸發EPOLLIN
- ET模式會在連接建立后觸發一次EPOLLOUT,當收到數據時會觸發一次EPOLLIN
- LT模式觸發EPOLLIN時可以按需讀取數據,殘留了數據還會再次通知讀取
- ET模式觸發EPOLLIN時必須把數據讀取完,否則即使來了新的數據也不會再次通知了
- LT模式的EPOLLOUT會一直觸發,所以發送完數據記得刪除,否則會產生大量不必要的通知
- ET模式的EPOLLOUT事件若數據未發送完需再次注冊,否則不會再有發送的機會
- 通常發送網絡數據時不會依賴EPOLLOUT事件,只有在緩沖區滿發送失敗時會注冊這個事件,期待被通知后再次發送
-
LT
+關注
關注
0文章
168瀏覽量
30703 -
電平
+關注
關注
5文章
361瀏覽量
39969 -
代碼
+關注
關注
30文章
4823瀏覽量
68894 -
Verilog語言
+關注
關注
0文章
113瀏覽量
8288 -
epoll
+關注
關注
0文章
28瀏覽量
2974
發布評論請先 登錄
相關推薦
評論