前言
epoll的觸發(fā)模式是個引發(fā)討論非常多的話題,網(wǎng)絡(luò)上這方面總結(jié)的文章也很多,首先從名字上就不是很統(tǒng)一,LT模式常被稱為水平觸發(fā)、電平觸發(fā)、條件觸發(fā),而ET模式常被稱為邊緣觸發(fā)、邊沿觸發(fā)等,這些都是從英文翻譯過來的,只不過翻譯的時候有些差異,LT全稱 level-triggered,ET全稱 edge-triggered。
雖然這個知識點(diǎn)熱度很高,但很多人對于它的理解總是差那么一點(diǎn),特別是在面試的時候,很多面試者總是處于一種回憶和背誦的狀態(tài),其實(shí)這兩種模式真的不需要去死記硬背,下面說說我個人對這兩種模式的理解和記憶方法。
名稱的記憶
每次提到ET(邊沿觸發(fā))首先映入我腦海的是大學(xué)里《數(shù)字邏輯電路》這門課程,里面會提到低電平、高電平,當(dāng)電平從低到高時會有一個上升沿,而電平從高到低時會有一個下降沿,這個“沿”就是邊沿觸發(fā)時提到的“邊沿”,跟馬路邊的馬路牙子是同一種概念,也就是指狀態(tài)變化的時候。提起上升沿和下降沿我還是印象很深的,當(dāng)時我可是占用了好幾節(jié)課的時間用Verilog語言寫了一個顯示“HELLO WORLD”的仿真波形,依靠的就是電平變化中的“沿”。
狀態(tài)變化
LT模式和ET模式可以類比電平變化來學(xué)習(xí),但是在實(shí)際應(yīng)用中概念卻不是完全一樣的,在epoll的應(yīng)用中涉及到關(guān)于IO的讀寫,而讀寫的狀態(tài)變化有哪些呢?可讀、不可讀、可寫、不可寫,其實(shí)就是這四種狀態(tài)而已,以socket為例。
可讀:socket上有數(shù)據(jù)
不可讀:socket上沒有數(shù)據(jù)了
可寫:socket上有空間可寫
不可寫:socket上無空間可寫
對于水平觸發(fā)模式,一個事件只要有,就會一直觸發(fā)。
對于邊緣觸發(fā)模式,只有一個事件從無到有才會觸發(fā)。
LT模式
對于讀事件 EPOLLIN
,只要socket上有未讀完的數(shù)據(jù),EPOLLIN
就會一直觸發(fā);對于寫事件 EPOLLOUT
,只要socket可寫(一說指的是 TCP 窗口一直不飽和,我覺得是TCP緩沖區(qū)未滿時,這一點(diǎn)還需驗(yàn)證),EPOLLOUT
就會一直觸發(fā)。
在這種模式下,大家會認(rèn)為讀數(shù)據(jù)會簡單一些,因?yàn)榧词箶?shù)據(jù)沒有讀完,那么下次調(diào)用epoll_wait()時,它還會通知你在上沒讀完的文件描述符上繼續(xù)讀,也就是人們常說的這種模式不用擔(dān)心會丟失數(shù)據(jù)。
而寫數(shù)據(jù)時,因?yàn)槭褂?LT 模式會一直觸發(fā) EPOLLOUT 事件,那么如果代碼實(shí)現(xiàn)依賴于可寫事件觸發(fā)去發(fā)送數(shù)據(jù),一定要在數(shù)據(jù)發(fā)送完之后移除檢測可寫事件,避免沒有數(shù)據(jù)發(fā)送時無意義的觸發(fā)。
ET模式
對于讀事件 EPOLLIN
,只有socket上的數(shù)據(jù)從無到有,EPOLLIN
才會觸發(fā);對于寫事件 EPOLLOUT
,只有在socket寫緩沖區(qū)從不可寫變?yōu)榭蓪懀?code>EPOLLOUT 才會觸發(fā)(剛剛添加事件完成調(diào)用epoll_wait時或者緩沖區(qū)從滿到不滿)
這種模式聽起來清爽了很多,只有狀態(tài)變化時才會通知,通知的次數(shù)少了自然也會引發(fā)一些問題,比如觸發(fā)讀事件后必須把數(shù)據(jù)收取干凈,因?yàn)槟悴灰欢ㄓ邢乱淮螜C(jī)會再收取數(shù)據(jù)了,即使不采用一次讀取干凈的方式,也要把這個激活狀態(tài)記下來,后續(xù)接著處理,否則如果數(shù)據(jù)殘留到下一次消息來到時就會造成延遲現(xiàn)象。
這種模式下寫事件觸發(fā)后,后續(xù)就不會再觸發(fā)了,如果還需要下一次的寫事件觸發(fā)來驅(qū)動發(fā)送數(shù)據(jù),就需要再次注冊一次檢測可寫事件。
數(shù)據(jù)的讀取和發(fā)送
關(guān)于數(shù)據(jù)的讀比較好理解,無論是LT模式還是ET模式,監(jiān)聽到讀事件從socket開始讀數(shù)據(jù)就好了,只不過讀的邏輯有些差異,LT模式下,讀事件觸發(fā)后,可以按需收取想要的字節(jié)數(shù),不用把本次接收到的數(shù)據(jù)收取干凈,ET模式下,讀事件觸發(fā)后通常需要數(shù)據(jù)一次性收取干凈。
而數(shù)據(jù)的寫不太容易理解,因?yàn)閿?shù)據(jù)的讀是對端發(fā)來數(shù)據(jù)導(dǎo)致的,而數(shù)據(jù)的寫其實(shí)是自己的邏輯層觸發(fā)的,所以在通過網(wǎng)絡(luò)發(fā)數(shù)據(jù)時通常都不會去注冊監(jiān)可寫事件,一般都是調(diào)用 send 或者 write 函數(shù)直接發(fā)送,如果發(fā)送過程中, 函數(shù)返回 -1,并且錯誤碼是 EWOULDBLOCK 表明發(fā)送失敗,此時才會注冊監(jiān)聽可寫事件,并將剩余的服務(wù)存入自定義的發(fā)送緩沖區(qū)中,等可寫事件觸發(fā)后再接著將發(fā)送緩沖區(qū)中剩余的數(shù)據(jù)發(fā)送出去。
代碼實(shí)踐
基礎(chǔ)代碼
以下為一個epoll觸發(fā)模式測試的基礎(chǔ)代碼,也不算太長,直接拿來就可以測試:
#include < sys/socket.h > //for socket
#include < arpa/inet.h > //for htonl htons
#include < sys/epoll.h > //for epoll_ctl
#include < unistd.h > //for close
#include < fcntl.h > //for fcntl
#include < errno.h > //for errno
#include < iostream > //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
進(jìn)行編譯,編譯后通過 ./epoll
運(yùn)行為LT模式,通過 ./epoll et
模式運(yùn)行為ET模式,我們用編譯好的epoll程序作為服務(wù)器,使用nc命令來模擬一個客戶端。
測試分類
1.編譯后直接./epoll,然后在另一個命令行窗口用 nc -v 127.0.0.1 51741 命令模擬一次連接,此時 ./epoll 會產(chǎn)生大量的 EPOLLOUT event triggered for client fd ...,那是因?yàn)樵贚T模式下,EPOLLOUT會被一直觸發(fā)。
albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll
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 輸出內(nèi)容的第152行代碼,編譯后 ./epoll運(yùn)行,然后在另一個命令行窗口用 nc -v 127.0.0.1 51741 模擬一次連接后,輸入abcd回車,可以看到服務(wù)器./epoll輸出內(nèi)容,EPOLLIN被觸發(fā)多次,每次讀取一個字節(jié)。
albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll
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.還原剛才注釋的那行代碼,編譯后執(zhí)行 ./epoll et
啟動服務(wù)器,然后在另一個命令行窗口用 nc -v 127.0.0.1 51741
模擬一次連接后,然后在另一個命令行窗口用 nc -v 127.0.0.1 51741
模擬一次連接,服務(wù)器窗口顯示觸發(fā)了EPOLLOUT
事件
albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll et
start server at port [51741] with [ET] mode.
accept a new client fd [5].
EPOLLOUT event triggered for client fd [5].
在此基礎(chǔ)上,從剛剛運(yùn)行nc
命令的窗口中輸入回車、輸入回車、輸出回車,那么epoll服務(wù)器窗口看到的是觸發(fā)了三次EPOLLIN
事件,每次收到一個回車:
albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll 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 [
].
但是如果在nc模擬的客戶端里輸出abcd回車,那么在epoll服務(wù)器窗口觸發(fā)一次EPOLLIN事件接收到一個a之后便再也不會觸發(fā)EPOLLIN了,即使你在nc客戶端在此輸入也沒有用,那是因?yàn)樵诮邮艿木彌_區(qū)中一直還有數(shù)據(jù),新數(shù)據(jù)來時沒有出現(xiàn)緩沖區(qū)從空到有數(shù)據(jù)的情況,所以在ET模式下也注意這種情況。
albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll 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觸發(fā)了一次就不再觸發(fā)了
改代碼唄,ET模式在連接后觸發(fā)一次EPOLLOUT
,接收到數(shù)據(jù)時觸發(fā)一次EPOLLIN
,如果數(shù)據(jù)沒收完,以后這兩個事件就再也不會被觸發(fā)了,要想改變這種情況可以再次注冊一下這兩個事件,時機(jī)可以選擇接收到數(shù)據(jù)的時候,所以可以修改這部分代碼:
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
{
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
方式啟動服務(wù)器,使用nc -v 127.0.0.1 51741
模擬客戶端,輸入abc回車發(fā)現(xiàn),epoll服務(wù)器輸出顯示觸發(fā)的事件變了:
albert@home-pc:/mnt/d/data/cpp/testepoll$ ./epoll 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 [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].
總結(jié)
- LT模式會一直觸發(fā)EPOLLOUT,當(dāng)緩沖區(qū)有數(shù)據(jù)時會一直觸發(fā)EPOLLIN
- ET模式會在連接建立后觸發(fā)一次EPOLLOUT,當(dāng)收到數(shù)據(jù)時會觸發(fā)一次EPOLLIN
- LT模式觸發(fā)EPOLLIN時可以按需讀取數(shù)據(jù),殘留了數(shù)據(jù)還會再次通知讀取
- ET模式觸發(fā)EPOLLIN時必須把數(shù)據(jù)讀取完,否則即使來了新的數(shù)據(jù)也不會再次通知了
- LT模式的EPOLLOUT會一直觸發(fā),所以發(fā)送完數(shù)據(jù)記得刪除,否則會產(chǎn)生大量不必要的通知
- ET模式的EPOLLOUT事件若數(shù)據(jù)未發(fā)送完需再次注冊,否則不會再有發(fā)送的機(jī)會
- 通常發(fā)送網(wǎng)絡(luò)數(shù)據(jù)時不會依賴EPOLLOUT事件,只有在緩沖區(qū)滿發(fā)送失敗時會注冊這個事件,期待被通知后再次發(fā)送
-
邏輯電路
+關(guān)注
關(guān)注
13文章
492瀏覽量
42535 -
數(shù)據(jù)
+關(guān)注
關(guān)注
8文章
6808瀏覽量
88743 -
電平
+關(guān)注
關(guān)注
5文章
357瀏覽量
39812 -
epoll
+關(guān)注
關(guān)注
0文章
28瀏覽量
2941
發(fā)布評論請先 登錄
相關(guān)推薦
評論