前言
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 < 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会被一直触发。
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 输出内容的第152行代码,编译后 ./epoll运行,然后在另一个命令行窗口用 nc -v 127.0.0.1 51741 模拟一次连接后,输入abcd回车,可以看到服务器./epoll输出内容,EPOLLIN被触发多次,每次读取一个字节。
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.还原刚才注释的那行代码,编译后执行 ./epoll et
启动服务器,然后在另一个命令行窗口用 nc -v 127.0.0.1 51741
模拟一次连接后,然后在另一个命令行窗口用 nc -v 127.0.0.1 51741
模拟一次连接,服务器窗口显示触发了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].
在此基础上,从刚刚运行nc
命令的窗口中输入回车、输入回车、输出回车,那么epoll服务器窗口看到的是触发了三次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服务器窗口触发一次EPOLLIN事件接收到一个a之后便再也不会触发EPOLLIN了,即使你在nc客户端在此输入也没有用,那是因为在接受的缓冲区中一直还有数据,新数据来时没有出现缓冲区从空到有数据的情况,所以在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触发了一次就不再触发了
改代码呗,ET模式在连接后触发一次EPOLLOUT
,接收到数据时触发一次EPOLLIN
,如果数据没收完,以后这两个事件就再也不会被触发了,要想改变这种情况可以再次注册一下这两个事件,时机可以选择接收到数据的时候,所以可以修改这部分代码:
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
方式启动服务器,使用nc -v 127.0.0.1 51741
模拟客户端,输入abc回车发现,epoll服务器输出显示触发的事件变了:
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事件,只有在缓冲区满发送失败时会注册这个事件,期待被通知后再次发送
-
逻辑电路
+关注
关注
13文章
494浏览量
42569 -
数据
+关注
关注
8文章
6867浏览量
88800 -
电平
+关注
关注
5文章
359浏览量
39845 -
epoll
+关注
关注
0文章
28浏览量
2947
发布评论请先 登录
相关推荐
评论