epoll的LT模式(水平触发)和ET模式(边沿触发)

前言

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触发模式测试的基础代码,也不算太长,直接拿来就可以测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
#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 进行编译,编译后通过 ./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会被一直触发。
1
2
3
4
5
6
7
8
9
10
11
12
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].
...
  1. 注释包含 EPOLLOUT event triggered for client fd 输出内容的第152行代码,编译后 ./epoll运行,然后在另一个命令行窗口用 nc -v 127.0.0.1 51741 模拟一次连接后,输入abcd回车,可以看到服务器./epoll输出内容,EPOLLIN被触发多次,每次读取一个字节。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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 [
].
  1. 还原刚才注释的那行代码,编译后执行 ./epoll et 启动服务器,然后在另一个命令行窗口用 nc -v 127.0.0.1 51741 模拟一次连接后,然后在另一个命令行窗口用 nc -v 127.0.0.1 51741 模拟一次连接,服务器窗口显示触发了EPOLLOUT事件
1
2
3
4
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].

在此基础上,从刚刚运行 nc命令的窗口中输入回车、输入回车、输出回车,那么epoll服务器窗口看到的是触发了三次EPOLLIN事件,每次收到一个回车:

1
2
3
4
5
6
7
8
9
10
11
12
13
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服务器窗口触发一次EPOLLIN事件接收到一个a之后便再也不会触发EPOLLIN了,即使你在nc客户端在此输入也没有用,那是因为在接受的缓冲区中一直还有数据,新数据来时没有出现缓冲区从空到有数据的情况,所以在ET模式下也注意这种情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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触发了一次就不再触发了

改代码呗,ET模式在连接后触发一次EPOLLOUT,接收到数据时触发一次EPOLLIN,如果数据没收完,以后这两个事件就再也不会被触发了,要想改变这种情况可以再次注册一下这两个事件,时机可以选择接收到数据的时候,所以可以修改这部分代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
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;
}

添加再次注册的逻辑:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
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 方式启动服务器,使用 nc -v 127.0.0.1 51741 模拟客户端,输入abc回车发现,epoll服务器输出显示触发的事件变了:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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].

总结

  • LT模式会一直触发EPOLLOUT,当缓冲区有数据时会一直触发EPOLLIN
  • ET模式会在连接建立后触发一次EPOLLOUT,当收到数据时会触发一次EPOLLIN
  • LT模式触发EPOLLIN时可以按需读取数据,残留了数据还会再次通知读取
  • ET模式触发EPOLLIN时必须把数据读取完,否则即使来了新的数据也不会再次通知了
  • LT模式的EPOLLOUT会一直触发,所以发送完数据记得删除,否则会产生大量不必要的通知
  • ET模式的EPOLLOUT事件若数据未发送完需再次注册,否则不会再有发送的机会
  • 通常发送网络数据时不会依赖EPOLLOUT事件,只有在缓冲区满发送失败时会注册这个事件,期待被通知后再次发送

==>> 反爬链接,请勿点击,原地爆炸,概不负责!<<==

即使是在灿烂的阳光下也会有黑暗的角落,不能因为角落的阴暗就忽略阳光下的美好,我们要做的不是把黑暗面放大,而是要做阳光的传递者,哪怕是一面面镜子,通过反射来照亮那星星点点的黑暗,认清自己,不与黑暗为伍,那绝不是你自甘堕落的借口。

两千光束已然出发~

2022-4-4 18:25:50

Albert Shi wechat
欢迎您扫一扫上面的微信公众号,订阅我的博客