LT/ET模式
创始人
2024-03-30 22:58:03
0

前面我有讲了select和poll都是LT模式,而epoll有LT和ET两种模式,有的人就很懵,那么这一节我们就来聊聊什么是ET/LT模式

目录

改成ET模式

步骤:

EPOLLONSHORT事件


epoll 对文件描述符有两种操作模式:LT(Level Trigger,电平触发)模式和 ET(Edge Trigger,边沿触发)模式。

LT 模式是默认的工作模式。当往 epoll 内核事件表中注册一个文件描述符上的 EPOLLET 事件时,epoll 将以高效的 ET 模式来操作该文件描述符

  • 对于 LT 模式操作的文件描述符,当 epoll_wait 检测到其上有事件发生并将此事件通知 应用程序后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用 epoll_wait 时, 还会再次向应用程序通告此事件,直到该事件被处理。

在LT模式下,当epoll检测到事件就绪的时候,可以不处理或处理一部分,但是可以连续多次调用epoll_wait对事件进行处理,简单点来说的话就是如果事件来了,不管来了几个,只要仍然有未处理的事件,epoll都会通知你

  • 对于 ET 模式操作的文件描述符,当 epoll_wait 检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理该事件,因为后续的 epoll_wait 调用将不再向应用程序 通知这一事件。所以 ET 模式在很大程度上降低了同一个 epoll 事件被重复触发的次数,因 此效率比 LT 模式高。

在ET模式下,当epoll检测到事件就绪的时候,会立即进行处理,并且只会处理一次,换句话说就是文件描述符上的事件就绪之后,只有一次处理机会。 简单来说就是如果事件来了,不管来了几个,你若不处理或者没有处理完,除非下一个事件到来,否则epoll将不会再通知你。ET的性能比LT性能更高( epoll_wait 返回的次数少了很多). Nginx默认采用ET模式使用epoll。只支持非阻塞的读写。

画张图就很好理解了:

  • 大白话举个例子:当你在中午饭点玩游戏的时候,如果这个时候饭刚好做好了。
  • LT:家里人第一次通知的时候,你没有动,那他们还会通知第二次、第三次…
  • ET:家里人在第一次通知的时候,你没有动,那么他们就不会在通知你了。

那么既然要用ET,ET只会通知你一次,我们要做到高效就需要通过这一次提醒就把所有数据读完,怎么做嘞?

就需要循环去读数据,循环读的话又会引入阻塞问题,因此我们还需要将读取改为非阻塞的模式

所以epoll 工作在 ET 模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读 / 阻塞写操作把处理多个文件描述符的任务饿死。

改成ET模式

对于上节epoll的代码呢我们把epoll的接收数据的函数进行稍加修改,改成每次只读一个数据去测试,会发现输入一个hello,每次只读一个,每次回复一个确认收到信息,epoll_wait调用多次。

下面我们改为ET模式,依旧是一次读一个字节,但是通过一次通知完成所有数据的读取

步骤:

我们往epoll内核事件表中注册一个文件描述符上的EPOLLET事件:

ev.events=EPOLLIN|EPOLLET;

只改这个的话我们会发现

输入一个hello,他第一次只读取了一个h,剩下的还在服务端的缓冲区中,当客户端再次发送一个数据,服务端就会接着去读e而不是新发的数据。ET模式下epoll_wait()被调用2次,只处理一次数据,其他未处理的数据都在服务器缓冲区,后面客户端再发送数据,数据都存储到缓冲区,再从缓冲区将上次未处理完的数据给客户端,这样一直不能读到实时信息,都是缓冲区的。

那么我们就需要再次根据ET的特征去更改代码:

首先要让 它一次性把就绪事件都处理完。那么我们在处理数据那加个循环,一直处理。但如果hello五次都读完之后recv就会阻塞退不出循环,除非客户端关闭,因为他没办法退出,这时I/O复用已经无作用了。那么我们就要设计如何让它处理完所有就绪事件可以退出,可以继续处理下一个可读事件。

我们需要 设计在获取事件描述符c后,将c设置为非阻塞的运行,如果c设置为非阻塞方式,则recv不会阻塞,如果没有阻塞,则recv返回-1,并且会设置全局的errno。这样当没有数据可以读或者数据已经读取完毕,epoll就能再次触发sockfd上的EPOLLIN事件,以驱动下一次读操作。我们需要用到fcntl,用来对文件描述符进行操作。
 

void setnonblock(int fd)//设置描述符为非阻塞
{int oldfl=fcntl(fd,F_GETFL);//旧状态int newfl=oldfl|O_NONBLOCK;//新的标志位设为非阻塞状态if( fcntl(fd,F_SETFL,newfl)==-1)//设置新的状态{printf("set nonblock fd error\n");}
}

那么我们在处理数据时就需要进行判断

引入头文件#include

 if(errno==EAGAIN ||errno==EWOULDBLOCK)//因为缓冲区没数据阻塞{//recv非阻塞的错误返回码send(c,"ok",2,0);}else//如果不是,就是有异常{printf("recv err\n");}

如果 成立的话就表示没有数据可以读或者数据已经读取完毕,这时我们就可以退出循环。

依旧是给出服务端代码:

#include
#include
#include
#include
#include
#include
//网络头文件
#include
#include
#include
#include
#include
#include
#define MAX 10//定义最大连接数为10个
void setnonblock(int fd)//设置描述符为非阻塞
{int oldfl=fcntl(fd,F_GETFL);int newfl=oldfl|O_NONBLOCK;//非阻塞if( fcntl(fd,F_SETFL,newfl)==-1){printf("set nonblock fd error\n");}}
int InitSocket()
{int sockfd = socket(AF_INET,SOCK_STREAM,0);if(sockfd == -1) return -1;struct sockaddr_in ser;//指明地址信息,是一种通用的套接字地址memset(&ser,0,sizeof(ser));ser.sin_family = AF_INET;//设置地址家族ser.sin_port = htons(6000);//设置端口ser.sin_addr.s_addr = inet_addr("127.0.0.1");//设置地址int res = bind(sockfd,(struct sockaddr*)&ser,sizeof(ser));//绑定端口号和地址if(res == -1)   return -1;res = listen(sockfd,5);if(res == -1) return -1;return sockfd;
}
void epoll_add(int epfd,int fd)
{struct epoll_event ev;ev.events=EPOLLIN|EPOLLET;//读,ETev.data.fd=fd;setnonblock(fd);//设置非阻塞if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1){printf("epoll add failed\n");}
}
void epoll_del(int epfd,int fd)
{if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)==-1){printf("epoll del failed\n");}}
void accept_client(int epfd,int sockfd)
{struct sockaddr_in caddr;int len=sizeof(caddr);int c=accept(sockfd,(struct sockaddr*)&caddr,&len);if(c<0){return ;}printf("accpet c=%d ip=%s\n",c,inet_ntoa(caddr.sin_addr));epoll_add(epfd,c);
}
void recv_data(int epfd,int c)
{/*char buff[128]={0};int num=recv(c,buff,1,0);if(num<=0)//如果num==0说明客户端结束了描述符号{epoll_del(epfd,c);//移除改客户端对应的描述符close(c);printf("client close\n");return ;}printf("buff (%d)=%s\n",c,buff);send(c,"ok",2,0);*/while(1){char buff[128]={0};int n=recv(c,buff,1,0);if(n==-1){if(errno==EAGAIN ||errno==EWOULDBLOCK)//因为缓冲区没数据阻塞{send(c,"ok",2,0);}else//如果不是,就是有异常{printf("recv err\n");}break;}else if(n==0){epoll_del(epfd,c);close(c);printf("close\n");break;}else{printf("read ET:%s\n",buff);}}}int main()
{int sockfd = InitSocket();//监听套接字,有客户端链接时就会触发读事件。assert(sockfd != -1);//创建内核事件表int epfd=epoll_create(MAX);//底层,红黑树if(epfd==-1){exit(1);}epoll_add(epfd,sockfd);//将监听套接子添加到内核事件表struct epoll_event evs[MAX];//用来接收就绪的文件描述符while(1){int n=epoll_wait(epfd,evs,MAX,5000);if(n==-1){printf("err\n");}else if(n==0){printf("time out\n");}else{//前n个元素是数据就绪的for(int i=0;i

EPOLLONSHORT事件

在上一节说epoll的时候也提过这个事件,这里我粘一张截图去回顾一下

epoll比poll多很多事件,说到ET模式就不得不说这个事件,它一般用在并发处理上,多个线程就会出现下面的情形。
即使我们使用ET模式,一个socket上的某个事件还是可能被触发多次,这在并发程序中就会引起一个问题,比如一个线程在读取某个socket上的数据后开始处理这些数据,而在数据的处理过程中该socket上又有新的数据可读,此时另外一个线程被唤醒来读取这些新的数据,于是就出现两个线程同时操作一个socket的局面,这当然不是我们期望的,我们期望的是一个socket连接在任一时刻都只被一个线程处理,这就要用到EPOLLONSHORT。

对于注册了EPOLLONSHORT事件的文件描述符,操作系统最多触发其上注册的一个可读,可写或者异常事件,且只触发一次,除非我们使用epoll_ctl函数重置该文件描述符上注册的EPOLLONSHORT事件。这样,当一个线程在处理某个socket时,其他线程是不可能有机会操作该socket的。

注册了EPOLLONSHORT事件的socket一旦被某个线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONSHORT事件,以确保socket下一次可读时,其他EPOLLIN事件能被触发,进而让其他线程有机会继续处理这个socket。

下面我把上面的ET模式改写一下加入消息队列,线程去演示一下EPOLLONSHORT

代码如下:

#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#include 
#define MAXFD  10struct mess
{long type;//类型int c;
};
int msgid = -1;//消息队列的id
int epfd = -1;
int socket_init();
void setnonblock(int fd)
{int oldfl = fcntl(fd,F_GETFL);int newfl = oldfl | O_NONBLOCK;if(fcntl(fd,F_SETFL,newfl) == -1 ){printf("fcntl err\n");}
}
void epoll_add(int epfd, int fd)
{struct epoll_event ev;ev.events = EPOLLIN|EPOLLET|EPOLLONESHOT;//ev.data.fd = fd;setnonblock(fd);if ( epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev) == -1 ){printf("epoll add err\n");}
}void epoll_reset(int epfd, int fd)
{struct epoll_event ev;ev.events = EPOLLIN|EPOLLET|EPOLLONESHOT;ev.data.fd = fd;if ( epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev) == -1 ){printf("epoll mod err\n");}
}
void epoll_del(int epfd, int fd)
{if ( epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL) == -1 ){printf("epoll del err\n");}
}
void* work_thread(void* arg)
{int index = (int)arg;while( 1 ){struct mess m;msgrcv(msgid,&m,sizeof(int),1,0);//消息队列空,阻塞printf("msgrcv c\n");int c = m.c;while(1 ){char buff[128] = {0};int n = recv(c,buff,1,0);if( n == -1 ){if( errno == EAGAIN  || errno == EWOULDBLOCK ){//send(c,"ok",2,0);epoll_reset(epfd,c);}else{//printf("recv err\n");}break;}else if ( n == 0 ){epoll_del(epfd,c);close(c);printf("close\n");break;}else{printf("index=%d,buff=%s\n",index,buff);send(c,"ok",2,0);sleep(2);}}}
}int socket_init()
{int sockfd = socket(AF_INET,SOCK_STREAM,0);if ( sockfd == -1 ){return -1;}struct sockaddr_in saddr;memset(&saddr,0,sizeof(saddr));saddr.sin_family = AF_INET;saddr.sin_port = htons(6000);saddr.sin_addr.s_addr = inet_addr("0.0.0.0");int res = bind(sockfd,(struct sockaddr*)&saddr,sizeof(saddr));if ( res == -1 ){printf("bind err\n");return -1;}res = listen(sockfd,5);if ( res == -1 ){return -1;}return sockfd;
}
int main()
{int sockfd = socket_init();if ( sockfd == -1 ){exit(1);}epfd = epoll_create(MAXFD);//创建内核时间表--红黑树if ( epfd == -1 ){exit(1);}epoll_add(epfd,sockfd);//添加描述符到红黑树msgid = msgget((key_t)1235,IPC_CREAT|0600);if ( msgid == -1 ){printf("create msg err\n");exit(1);}pthread_t id[3];for( int i = 0; i < 3; i++ ){pthread_create(&id[i],NULL,work_thread,(void*)i);}struct epoll_event evs[MAXFD];while( 1 ){int n = epoll_wait(epfd,evs,MAXFD,-1);if ( n == -1 ){printf("epoll wait err\n");}else{for( int i = 0; i < n; i++ ){int fd = evs[i].data.fd;if ( evs[i].events & EPOLLIN ){if ( fd == sockfd){struct sockaddr_in caddr;int len = sizeof(caddr);int c = accept(fd,(struct sockaddr*)&caddr,&len);if ( c >= 0 ){epoll_add(epfd,c);}}else{//添加到消息队列struct mess m;m.type = 1;m.c = fd;msgsnd(msgid,&m,sizeof(int),0);printf("msgsnd\n");}}}}}}

我让他每次打印都休眠两秒,刚开始打印hello,会发现0号线程一个一个打印,在他还没打印完成的时候,我继续输入ok,其他线程阻塞着也拿不到这个ok,0号线程继续打印他没打完的数据,打完之后重置事件,保证下一次能接收数据,接着0号又接收ok,1245同理也是0号线程没处理完我输进去的,而当0号线程处理完之后,重置,1号又接收到了数据556,注意这里重置之后别的线程谁都有可能去接收这个数据

如这段话如此:


本来还想把三组io对比整出来,不过网上到处都是,写的都很全,我写的各节也把每组io的特点说的很清楚了,这里就不过多赘述了

 

相关内容

热门资讯

银河麒麟V10SP1高级服务器... 银河麒麟高级服务器操作系统简介: 银河麒麟高级服务器操作系统V10是针对企业级关键业务...
【NI Multisim 14...   目录 序言 一、工具栏 🍊1.“标准”工具栏 🍊 2.视图工具...
AWSECS:访问外部网络时出... 如果您在AWS ECS中部署了应用程序,并且该应用程序需要访问外部网络,但是无法正常访问,可能是因为...
不能访问光猫的的管理页面 光猫是现代家庭宽带网络的重要组成部分,它可以提供高速稳定的网络连接。但是,有时候我们会遇到不能访问光...
AWSElasticBeans... 在Dockerfile中手动配置nginx反向代理。例如,在Dockerfile中添加以下代码:FR...
Android|无法访问或保存... 这个问题可能是由于权限设置不正确导致的。您需要在应用程序清单文件中添加以下代码来请求适当的权限:此外...
月入8000+的steam搬砖... 大家好,我是阿阳 今天要给大家介绍的是 steam 游戏搬砖项目,目前...
​ToDesk 远程工具安装及... 目录 前言 ToDesk 优势 ToDesk 下载安装 ToDesk 功能展示 文件传输 设备链接 ...
北信源内网安全管理卸载 北信源内网安全管理是一款网络安全管理软件,主要用于保护内网安全。在日常使用过程中,卸载该软件是一种常...
AWS管理控制台菜单和权限 要在AWS管理控制台中创建菜单和权限,您可以使用AWS Identity and Access Ma...