epoll中的ET和LT模式区别

简介: epoll中的ET和LT模式区别

一、水平触发(LT)和边沿触发(ET)

在电路中的有水平触发和边沿触发的概念,在epoll读取事件下,水平触发可以理解为,蓝色那一部分,只要存在可读的情况,就会一直读取。而边沿触发,可以理解为红色箭头所指向,发生跳变的部分,就会触发一次。

在epoll中

events=EPOLLIN 为读取事件,LT模式

events=EPOLLIN|EPOLLET 为读取事件,ET模式

events=EPOLLOUT 为写事件,LT模式

events=EPOLLOUT|EPOLLET 为写事件,ET模式

  • recv的时候
    如果设置为LT,只要 接受缓冲 不为空,就会一直触发EPOLLIN,直到 接受缓冲 为空
    如果设置为ET,只要 客户端 发送一次数据,就会触发一次EPOLLIN
  • send的时候
    如果设置为LT,只要 发送缓冲 不满,就会一直触发EPOLLOUT
    如果设置为ET,有注册EPOLLOUT事件,才会一次触发一次EPOLLOUT

ET模式 效率要比 LT模式高

小数据使用边沿触发,大数据使用水平触发

比如listenfd,接受缓冲区 可能存放多个客户端连接请求的信息,这时候要使用水平触发(LT),因为accept每次只能处理一个,需要多次触发。如果用边沿触发(ET)可能会漏掉一些连接。

二、例子

1.例子:水平触发(LT)

下面是epoll实现的简单tcp服务器,用LT的触发方式

完整代码

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<fcntl.h>
#define EPOLL_SIZE 1024   
#define BUFFER_SIZE 4096
int main(int argc,char** argv){
    int listenfd=socket(AF_INET,SOCK_STREAM,0);
    sockaddr_in serveraddr;
    memset(&serveraddr,0,sizeof(sockaddr_in));
    serveraddr.sin_family=AF_INET;
    serveraddr.sin_port=htons(8888);
    serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
    bind(listenfd,(sockaddr*)&serveraddr,sizeof(sockaddr_in));
    listen(listenfd,10);
    int epfd=epoll_create(1);
    epoll_event events[EPOLL_SIZE]={0};
    epoll_event ev;
    ev.data.fd=listenfd;
    ev.events=EPOLLIN;
    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
    char buffer[BUFFER_SIZE]={0};
    while(1){
        int nready=epoll_wait(epfd,events,EPOLL_SIZE,5);
        if(nready==-1) continue;
        for(int i=0;i<nready;i++){
            int fd=events[i].data.fd;
            if(fd==listenfd){
                sockaddr_in clientaddr;
                memset(&clientaddr,0,sizeof(sockaddr_in));
                socklen_t clientLen=sizeof(sockaddr_in);
                int clientfd=accept(listenfd,(sockaddr*)&clientaddr,&clientLen);
                ev.data.fd=clientfd;
                ev.events=EPOLLIN;
                epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev);
            }
            else {
                int n=recv(fd,buffer,BUFFER_SIZE,0);
                if(n==0){
                    ev.data.fd=fd;
                    ev.events=EPOLLIN;
                    epoll_ctl(epfd,EPOLL_CTL_DEL,fd,&ev);
                    close(fd);
                    break;
                }
                else if(n>0){
                    printf("Recv:%s\n",buffer);
                }
            }
        }
    }
    return 0;
}

其中,只设置了EPOLLIN,就代表读取。并且默认为水平触发模式(LT)

现在利用 客户端向服务器发送这么一条数据,可以看到接受到了数据了

ev.events=EPOLLIN;

现在将其int n=recv(fd,buffer,BUFFER_SIZE,0);修改为int n=recv(fd,buffer,5,0);

意思是现在每次只能读取长度为5的数据了

再来测试下,得到结果

可以发现,读取EPOLLIN这个事件,被多次触发,直至读完

因此LT模式,在EPOLLIN事件下,只要 读取缓冲 不为空 就会一直读取

2.例子:边沿触发(ET)

还是保持上一个例子,代码的基础上,每次只读取长度为5的数据

将clientfd的 事件ev的触发设置成 ET模式 ,ev.events=EPOLLIN|EPOLLET;

注意不是修改listenfd的触发模式

让客户端发送一条 数据,发现只有 长度为5的数据,剩下一部分,没有发出来

于是让客户端继续发送一条数据:“nihao”

结果客户端,没有输出“nihao”,而是把之前的未输出完的数据给输出了。

因此可以理解为,每当 接受缓冲 有新的数据时,就会触发一次。

完整代码

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<fcntl.h>
#define EPOLL_SIZE 1024   
#define BUFFER_SIZE 4096
int main(int argc,char** argv){
    int listenfd=socket(AF_INET,SOCK_STREAM,0);
    sockaddr_in serveraddr;
    memset(&serveraddr,0,sizeof(sockaddr_in));
    serveraddr.sin_family=AF_INET;
    serveraddr.sin_port=htons(8888);
    serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
    bind(listenfd,(sockaddr*)&serveraddr,sizeof(sockaddr_in));
    listen(listenfd,10);
    int epfd=epoll_create(1);
    epoll_event events[EPOLL_SIZE]={0};
    epoll_event ev;
    ev.data.fd=listenfd;
    ev.events=EPOLLIN;
    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
    char buffer[BUFFER_SIZE]={0};
    while(1){
        int nready=epoll_wait(epfd,events,EPOLL_SIZE,5);
        if(nready==-1) continue;
        for(int i=0;i<nready;i++){
            int fd=events[i].data.fd;
            if(fd==listenfd){
                sockaddr_in clientaddr;
                memset(&clientaddr,0,sizeof(sockaddr_in));
                socklen_t clientLen=sizeof(sockaddr_in);
                int clientfd=accept(listenfd,(sockaddr*)&clientaddr,&clientLen);
                ev.data.fd=clientfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev);
            }
            else {
                int n=recv(fd,buffer,5,0);
                if(n==0){
                    ev.data.fd=fd;
                    ev.events=EPOLLIN;
                    epoll_ctl(epfd,EPOLL_CTL_DEL,fd,&ev);
                    close(fd);
                    break;
                }
                else if(n>0){
                    printf("Recv:%s\n",buffer);
                }
            }
        }
    }
    return 0;
}

3.例子:边沿触发(ET)并设置非阻塞io

既然边沿触发,执行效率高,但是又不能读完数据该怎么办呢?

在ET模式下,一般会设置非阻塞io

当被监控的文件描述符上有可读写事件发生时,epoll_wait()会通知处理程序去读写。如果这次没有把数据全部读写完(如读写缓冲区太小),那么下次调用epoll_wait()时,它不会通知你,也就是它只会通知你一次,直到该文件描述符上出现第二次可读写事件才会通知你!!!这种模式比水平触发效率高,系统不会充斥大量你不关心的就绪文件描述符

所以ET所以循环处理,保证能将数据读取完毕,即同时要保证非阻塞IO,不然最后会被阻塞

也就是说,

在当前没有可读取数据的情况下

  • 如果是阻塞io,recv()会阻塞
  • 如果是非阻塞io,recv()会返回-1

现在 沿用例子2的代码

对新添加的客户端的clientfd设置为非阻塞

recv的外面设置了一层while(1)循环

在非阻塞情况下,如果recv没有收到数据就会返回-1,因此if(n<0) break,表明数据读取完成了。

然后运行测试,让客户端再次发送数据

可以发现完整的都运行出来了。

另外,如果不设置非阻塞(也就是 阻塞模式)

由于 recv不可读的时候会阻塞(而不会像非阻塞那样输出-1),导致下面死循环在while(1)内,虽然当前客户端可以再次发送数据,但是其他客户端就不能再连入服务器了。

完整代码

#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<sys/socket.h>
#include<sys/epoll.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#include<fcntl.h>
#define EPOLL_SIZE 1024   
#define BUFFER_SIZE 4096
int main(int argc,char** argv){
    int listenfd=socket(AF_INET,SOCK_STREAM,0);
    sockaddr_in serveraddr;
    memset(&serveraddr,0,sizeof(sockaddr_in));
    serveraddr.sin_family=AF_INET;
    serveraddr.sin_port=htons(8888);
    serveraddr.sin_addr.s_addr=htonl(INADDR_ANY);
    bind(listenfd,(sockaddr*)&serveraddr,sizeof(sockaddr_in));
    listen(listenfd,10);
    int epfd=epoll_create(1);
    epoll_event events[EPOLL_SIZE]={0};
    epoll_event ev;
    ev.data.fd=listenfd;
    ev.events=EPOLLIN;
    epoll_ctl(epfd,EPOLL_CTL_ADD,listenfd,&ev);
    char buffer[BUFFER_SIZE]={0};
    while(1){
        int nready=epoll_wait(epfd,events,EPOLL_SIZE,5);
        if(nready==-1) continue;
        for(int i=0;i<nready;i++){
            int fd=events[i].data.fd;
            if(fd==listenfd){
                sockaddr_in clientaddr;
                memset(&clientaddr,0,sizeof(sockaddr_in));
                socklen_t clientLen=sizeof(sockaddr_in);
                int clientfd=accept(listenfd,(sockaddr*)&clientaddr,&clientLen);
                fcntl(clientfd,F_SETFL,O_NONBLOCK);//设置非阻塞
                ev.data.fd=clientfd;
                ev.events=EPOLLIN|EPOLLET;
                epoll_ctl(epfd,EPOLL_CTL_ADD,clientfd,&ev);
            }
            else {
                while(1){
                    int n=recv(fd,buffer,5,0);
                    if(n<0) break;
                    if(n==0){
                        ev.data.fd=fd;
                        ev.events=EPOLLIN;
                        epoll_ctl(epfd,EPOLL_CTL_DEL,fd,&ev);
                        close(fd);
                        break;
                    }
                    else if(n>0){
                        printf("Recv:%s\n",buffer);
                    }
                } 
            }
        }
    }
    return 0;
}


相关文章
|
10月前
|
C++
C++编程技巧:sort()函数中的greater<int>()参数使用讲解
所以你看,`sort()`函数和 `greater<int>()`的组合就像一个魔法工具箱,可以轻松地完成从大到小的排序任务。希望这个小小的技巧能为你的C++编程之路增添一份乐趣。
436 23
|
编解码 网络协议 数据安全/隐私保护
计网 - 图解OSI 七层模型 和 TCP/IP 四层模型
计网 - 图解OSI 七层模型 和 TCP/IP 四层模型
7324 0
|
敏捷开发 存储 数据可视化
产品经理的效率秘籍:科学梳理产品需求
产品梳理旨在解决信息混乱、需求不清等问题,使产品架构清晰、目标明确、执行高效。通过厘清产品定位、优化需求管理、提高执行效率和加强团队协作,企业可以减少沟通成本,提升整体效率。关键步骤包括确定产品架构、规范需求管理和建立任务管理机制。借助工具如板栗看板,可实现需求可视化、高效任务拆解及顺畅的团队协作,确保产品梳理顺利落地。定期复盘和优化,引导团队使用协同工具,并加强跨部门协同,是成功的关键。
|
缓存 Linux 编译器
共享库soname机制
【7月更文挑战第4天】Linux共享库的soname机制管理版本,通过libname.so.x的形式区分主版本。soname(如libname.so.x)在程序编译时被记录,运行时动态链接器依据soname找对应的.so.x文件。linkname(libname.so)用于编译时链接。更新库时,soname不变则不影响已编译程序,新soname则需新旧版本共存。`ldconfig`用于更新系统共享库缓存。
494 3
|
Java Linux
linux 安装配置 jdk8
linux 安装配置 jdk8
1455 3
|
SQL 分布式计算 Oracle
CDH 搭建_Hive_安装和创建用户|学习笔记
快速学习 CDH 搭建_Hive_安装和创建用户
1102 0
|
Ubuntu Linux 芯片
Linux 驱动开发基础知识——设备树的语法驱动开发基础知识(九)
Linux 驱动开发基础知识——设备树的语法驱动开发基础知识(九)
973 1
Linux 驱动开发基础知识——设备树的语法驱动开发基础知识(九)
|
数据处理
epoll的水平触发(LT)和边缘触发模式(ET)详解
epoll的水平触发(LT)和边缘触发模式(ET)详解
867 0
|
算法 C语言
海量数据中找出前k大数(topk问题),一篇文章教会你
海量数据中找出前k大数(topk问题),一篇文章教会你
1699 0
|
安全 算法 数据安全/隐私保护
【C++入门到精通】智能指针 shared_ptr 简介及C++模拟实现 [ C++入门 ]
【C++入门到精通】智能指针 shared_ptr 简介及C++模拟实现 [ C++入门 ]
759 0

热门文章

最新文章