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


相关文章
|
监控 网络协议 Java
I/O多路复用【Linux/网络】(C++实现select、poll和epoll服务器)(上)
I/O多路复用【Linux/网络】(C++实现select、poll和epoll服务器)
267 0
浅谈select,poll和epoll的区别
云栖号资讯:【点击查看更多行业资讯】在这里您可以找到不同行业的第一手的上云资讯,还在等什么,快来! select,poll和epoll其实都是操作系统中IO多路复用实现的方法。 select select方法本质其实就是维护了一个文件描述符(fd)数组,以此为基础,实现IO多路复用的功能。
浅谈select,poll和epoll的区别
|
6月前
|
监控 Linux
LT模式下epoll一直通知可写怎么办?
LT模式下epoll一直通知可写怎么办?
47 1
|
存储 监控 网络协议
I/O多路复用【Linux/网络】(C++实现select、poll和epoll服务器)(下)
I/O多路复用【Linux/网络】(C++实现select、poll和epoll服务器)
240 0
|
7月前
|
Linux
Linux网络编程(epoll的ET模式和LT模式)
Linux网络编程(epoll的ET模式和LT模式)
169 0
|
传感器 监控 Unix
Linux多路复用Select()与poll()函数
Linux多路复用Select()与poll()函数
93 0
|
Linux
一文搞懂select、poll和epoll区别
一文搞懂select、poll和epoll区别
854 1
一文搞懂select、poll和epoll区别
|
存储 监控 Linux
一文搞懂select、poll和epoll区别(下)
一文搞懂select、poll和epoll区别
328 0
一文搞懂select、poll和epoll区别(下)
一文搞懂select、poll和epoll区别(上)
一文搞懂select、poll和epoll区别
194 0
一文搞懂select、poll和epoll区别(上)
|
缓存 网络协议 程序员
举源码实例来说明epoll之LT和ET模式的区别
举源码实例来说明epoll之LT和ET模式的区别
150 0