0
  • 聊天消息
  • 系统消息
  • 评论与回复
登录后你可以
  • 下载海量资料
  • 学习在线课程
  • 观看技术视频
  • 写文章/发帖/加入社区
会员中心
创作中心

完善资料让更多小伙伴认识你,还能领取20积分哦,立即完善>

3天内不再提示

epoll的LT模式总结

科技绿洲 来源:Linux开发架构之路 作者:Linux开发架构之路 2023-11-10 15:35 次阅读

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." << 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事件,只有在缓冲区满发送失败时会注册这个事件,期待被通知后再次发送
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉
  • LT
    LT
    +关注

    关注

    0

    文章

    168

    浏览量

    30648
  • 电平
    +关注

    关注

    5

    文章

    359

    浏览量

    39851
  • 代码
    +关注

    关注

    30

    文章

    4744

    浏览量

    68344
  • Verilog语言
    +关注

    关注

    0

    文章

    113

    浏览量

    8213
  • epoll
    +关注

    关注

    0

    文章

    28

    浏览量

    2947
收藏 人收藏

    评论

    相关推荐

    epoll的使用

    以下内容是参考华清远见《linux/unix系统编程手册》对epoll的一个个人总结,是我在华清远见比较全面的总结。一、epoll的优点同I/O多路复用和信号驱动I/O一样,linux
    发表于 05-11 13:22

    我读过的最好的epoll讲解

    =EAGAIN。而epoll只关心缓冲区非满和缓冲区非空事件)。 一个epoll模式的代码大概的样子是:while true { active_stream[] =epoll_wait
    发表于 05-12 15:30

    epoll使用方法与poll的区别

    因为epoll的触发机制是在内核中直接完成整个功能 那个事件准备就绪我就直接返回这个IO事件
    发表于 07-31 10:03

    epoll_wait的事件返回的fd为错误是怎么回事?

    netlink 的 socket 连接 的 fd 为18,但是添加到epollepoll_wait()返回的fd 为 0为什么会出现这样的现象?补充 说明:1、 epoll_wait返回
    发表于 06-12 09:03

    揭示EPOLL一些原理性的东西

    =EAGAIN。而epoll只关心缓冲区非满和缓冲区非空事件)。一个epoll模式的代码大概的样子是:while true {active_stream[] = epoll
    发表于 08-24 16:32

    【米尔王牌产品MYD-Y6ULX-V2开发板试用体验】socket通信和epoll

    ;gt;#include &lt;sys/epoll.h>#include "ssd1306.h"const int PORT = 8888
    发表于 11-10 15:31

    poll&&epollepoll实现

    poll&&epollepoll实现
    发表于 05-14 14:34 2771次阅读
    poll&&<b class='flag-5'>epoll</b>之<b class='flag-5'>epoll</b>实现

    Linux中epoll IO多路复用机制

    epoll 是Linux内核中的一种可扩展IO事件处理机制,最早在 Linux 2.5.44内核中引入,可被用于代替POSIX select 和 poll 系统调用,并且在具有大量应用程序请求时能够
    发表于 05-16 16:07 699次阅读
    Linux中<b class='flag-5'>epoll</b> IO多路复用机制

    epoll LT和ET方式下的读写差别

    epoll接口是为解决Linux内核处理大量文件描述符而提出的方案。该接口属于Linux下多路I/O复用接口中select/poll的增强。
    的头像 发表于 07-07 10:34 2096次阅读

    一文详解epoll的实现原理

    本文以四个方面介绍epoll的实现原理,1.epoll的数据结构;2.协议栈如何与epoll通信;3.epoll线程安全如何加锁;4.ET与LT
    的头像 发表于 08-01 13:28 4012次阅读

    epoll来实现多路复用

    本人用epoll来实现多路复用,epoll触发模式有两种: ET(边缘模式LT(水平模式
    的头像 发表于 11-09 10:15 483次阅读
    用<b class='flag-5'>epoll</b>来实现多路复用

    epoll 的实现原理

    今儿我们就从源码入手,来帮助大家简单理解一下 epoll 的实现原理,并在后边分析一下,大家都说 epoll 性能好,那到底是好在哪里。 epoll 简介 1、epoll 的简单使用
    的头像 发表于 11-09 11:14 503次阅读
    <b class='flag-5'>epoll</b> 的实现原理

    epoll的基础数据结构

    一、epoll的基础数据结构 在开始研究源代码之前,我们先看一下 epoll 中使用的数据结构,分别是 eventpoll、epitem 和 eppoll_entry。 1、eventpoll 我们
    的头像 发表于 11-10 10:20 764次阅读
    <b class='flag-5'>epoll</b>的基础数据结构

    epoll的触发模式介绍

    前言 epoll的触发模式是个引发讨论非常多的话题,网络上这方面总结的文章也很多,首先从名字上就不是很统一,LT模式常被称为水平触发、电平触
    的头像 发表于 11-10 14:54 625次阅读

    epoll源码分析

    Linux内核提供了3个关键函数供用户来操作epoll,分别是: epoll_create(), 创建eventpoll对象 epoll_ctl(), 操作eventpoll对象
    的头像 发表于 11-13 11:49 990次阅读
    <b class='flag-5'>epoll</b>源码分析