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 socket
#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. < 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. < 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. < 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,当缓冲区有数据时会一直触发epollinet模式会在连接建立后触发一次epollout,当收到数据时会触发一次epollinlt模式触发epollin时可以按需读取数据,残留了数据还会再次通知读取et模式触发epollin时必须把数据读取完,否则即使来了新的数据也不会再次通知了lt模式的epollout会一直触发,所以发送完数据记得删除,否则会产生大量不必要的通知et模式的epollout事件若数据未发送完需再次注册,否则不会再有发送的机会通常发送网络数据时不会依赖epollout事件,只有在缓冲区满发送失败时会注册这个事件,期待被通知后再次发送
华为首款5G折叠屏手机MateX,可否成为下一个手机发展方向?
第三代局域I/O总线的要求及使用PCI Express交换器提高性能
指针式万用表测二极管的方法
俄乌冲突 新能源车涨价潮殃及中国
腾龙发布索尼 E 卡口 APS-C 画幅用变焦镜头
epoll的LT模式总结
不同碳化硅晶面带来新的可能
一文详解多芯片组件MCM技术
小米6定4.19发布!雷军:小米6,你等了203天,我们等了7年,售价2199元起,这次不耍猴
详解:什么是发射波束赋形(Transmit beamforming)技术?
巴帝电信或将用诺基亚替代中兴通讯 成为4G网络主要设备供应商
桥式整流电路工作原理(动画图)
工业互联网是什么?为什么要发展工业互联网?怎么发展工业互联网?
吉方工控工业主板的优势与特点
Docker使用的优点和十一条使用守则的详细概述
ubuntu系统命令大全
关于讯飞智能硬件解决方案发布会的分析和介绍
柴油发电机中性点接地电阻柜的特点介绍
鹏鼎控股6月合并营业17.06亿元!
c语言中怎么使用HTTP代理