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;
}


相关文章
|
缓存 NoSQL Java
面试官:如何保证本地缓存的一致性?
面试官:如何保证本地缓存的一致性?
2295 1
|
Java 物联网 Linux
在Linux上明明用rpm成功安装了软件,在卸载时却提示未安装
在Linux上明明用rpm成功安装了软件,在卸载时却提示未安装
1322 0
|
存储 算法 Java
【DFS(深度优先搜索)详解】看这一篇就够啦
本文介绍了深度优先搜索(DFS)算法及其应用。DFS从某个顶点出发,深入探索图的每条路径,直到无法前进为止,然后回溯。文章详细解释了DFS的基本思想,并通过示例图展示了其执行过程。此外,文中还探讨了三种枚举方式:指数型枚举、排列型枚举和组合型枚举,并提供了具体的代码实现。最后,文章通过几道练习题帮助读者更好地理解和应用DFS算法。
8394 19
【DFS(深度优先搜索)详解】看这一篇就够啦
|
Dubbo 网络协议 Java
RPC框架:一文带你搞懂RPC
这篇文章全面介绍了RPC(远程过程调用)的概念、原理和应用场景,解释了RPC如何工作以及为什么在分布式系统中广泛使用,并探讨了几种常用的RPC框架如Thrift、gRPC、Dubbo和Spring Cloud,同时详细阐述了RPC调用流程和实现透明化远程服务调用的关键技术,包括动态代理和消息的编码解码过程。
RPC框架:一文带你搞懂RPC
|
存储 算法 Java
深入剖析HashMap:理解Hash、底层实现与扩容机制
【9月更文挑战第6天】在Java编程中,`HashMap`是一个常用的数据结构,其高效性和可靠性依赖于深入理解哈希、底层实现及扩容机制。哈希通过散列算法将键映射到数组索引,采用链表或红黑树处理冲突;底层实现结合数组与链表,利用2的幂次方长度加快定位;扩容机制在元素数量超过负载因子与数组长度乘积时触发,通过调整初始容量和负载因子可优化性能。
239 3
|
Shell
wandb.errors.UsageError: api_key not configured (no-tty). call wandb.login(key=[your_api_key])
wandb.errors.UsageError: api_key not configured (no-tty). call wandb.login(key=[your_api_key])
4209 0
wandb.errors.UsageError: api_key not configured (no-tty). call wandb.login(key=[your_api_key])
|
Java Linux
linux 安装配置 jdk8
linux 安装配置 jdk8
840 3
|
数据库
脏读,幻读,不可重复读
脏读,幻读,不可重复读
|
存储 缓存 JSON
详解HTTP四种请求:POST、GET、DELETE、PUT
【4月更文挑战第3天】
64620 3
详解HTTP四种请求:POST、GET、DELETE、PUT
|
SQL 关系型数据库 MySQL
MySQL 聚合函数深入讲解与实战演练
MySQL 聚合函数深入讲解与实战演练