一 原理概述
1 概念和原理
LT 模式(Level Triggered) LT 模式是 epoll 的默认模式。在 LT 模式下,当某个文件描述符就绪时,epoll_wait 函数会立即返回,通知应用程序有事件发生。即使应用程序没有立即处理完这些事件,下次调用 epoll_wait 时仍会再次返回这些就绪的文件描述符。
ET 模式(Edge Triggered) ET 模式要求应用程序在处理文件描述符的就绪事件时,必须确保将其处理完毕,否则 epoll_wait 将不会重复通知该文件描述符的就绪状态。ET 模式通过设置 epoll_event 结构体中的 EPOLLET 标志来启用。
2 区别
通知机制:
LT 模式:每当文件描述符就绪时,epoll_wait 将通知应用程序,即使应用程序没有处理完该事件。 ET 模式:只有在文件描述符状态发生变化时,epoll_wait 才会通知应用程序。如果应用程序没有处理完事件,将不会重复通知该文件描述符的就绪状态。
效率:
LT 模式:由于每次文件描述符就绪时都会通知应用程序,因此可能会引起频繁的上下文切换,影响效率。 ET 模式:只在状态变化时通知应用程序,可以减少不必要的上下文切换,提高效率,特别适合处理大量事件和高并发的场景。
3 适用场景
LT 模式适用场景:
对实时性要求不是非常高的应用,例如普通的网络服务器或者需要周期性处理数据的情况。 适合处理一般的数据读取、写入等操作。
ET 模式适用场景:
对事件响应速度要求较高的应用,例如高性能网络服务器,需要快速处理大量连接或数据的情况。 适合处理大数据流、高并发请求等场景,可以减少因为频繁通知而引起的性能开销。
原文链接:epoll水平触发(Level Triggered)和边缘触发(Edge Triggered)详解_epoll水平和边缘触发-CSDN博客
-
select和poll采用水平触发。
-
epoll有水平触发和边缘触发两种机制。
下面两句话是精髓。
水平触发
-
读事件:如果epol_wait触发了读事件,表示有数据可读,如果程序没有把数据读完,再次调用epoll_wait的时候,将立即再次触发读事件。
-
写事件:如果发送缓冲区没有满,表示可以写入数据,只要缓冲区没有被写满,再次调用epoll_wait的时候,将立即再次触发写事件。
边缘触发
-
读事件:epoll_wait触发读事件后,不管程序有没有处理读事件epoll_wait都不会再触发读事件,只有当新的数据到达时,才再次触发读事件。
-
写事件: epoll_wait触发写事件之后,如果发送缓冲区仍可以写(发送缓冲区没有满),epoll_wait不会再次触发写事件,只有当发送缓冲区由 满 变成 不满 时,才再次触发写事件。
二 边缘触发的监听模式
-------------------------------------------- 监听客户端的边缘触发模式 -----------------------------------------------
示例:
采用边缘触发后,如果连接队列中有多个练上来的客户端,epoll只通知一次,程序只处理了一个客户端,如果一共300个客户端,epoll只通知了295个,因为还有5个客户端在连接队列中没有被处理,会有几个连接缺失了。为了解决这个问题,处理连接上来的客户端的时候需要加while循环。
如果使用了循环,那么🔴accept一定要用非阻塞的,不然会一直阻塞在accept这里。
🔴如果监听的socket是边缘触发的模式,那么一定要采用非阻塞的socket,处理事件的时候一定要用循环!
修改后代码为:
tcpepoll2.cpp
setnonblocking(listensock); //设置为非阻塞的,因为边缘触发使用了while。
// 如果发生事件的是listensock,表示有新的客户端连上来。
if (evs[ii].data.fd==listensock)
{
while(true)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientsock = accept(listensock,(struct sockaddr*)&client,&len);
if((clientsock<0)&&(errno==EAGAIN)) break; //这一句跟上面注释的代码一样,出循环条件。
printf ("accept client(socket=%d) ok.\n",clientsock);
// static int ii=0;
// printf("这是第%d个连接。\n",++ii);
// 为新客户端准备读事件,并添加到epoll中。
ev.data.fd=clientsock;
ev.events=EPOLLIN;
epoll_ctl(epollfd,EPOLL_CTL_ADD,clientsock,&ev);
}
}
三 边缘触发的读事件
---------------------------------------- 下面是边缘触发模式的读事件处理方式 ------------------------------------
思路:监听到客户端后:①设置非阻塞②设置边缘触发。后面对客户端发来的报文处理的时候:使用循环读取接收缓冲区中的内容。
tcpepoll2.cpp
// 如果发生事件的是listensock,表示有新的客户端连上来。
if (evs[ii].data.fd==listensock)
{
while(true)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientsock = accept(listensock,(struct sockaddr*)&client,&len);
if((clientsock<0)&&(errno==EAGAIN)) break;
printf ("accept client(socket=%d) ok.\n",clientsock);
// static int ii=0;
// printf("这是第%d个连接。\n",++ii);
// 为新客户端准备读事件,并添加到epoll中。
setnonblocking(clientsock); //客户端的socket设置为非阻塞。
ev.data.fd=clientsock;
// ev.events=EPOLLIN; // LT-水平触发。
ev.events=EPOLLIN|EPOLLET; // ET-边缘触发。
epoll_ctl(epollfd,EPOLL_CTL_ADD,clientsock,&ev);
}
}
else
{
// 如果是客户端连接的socke有事件,表示有报文发过来或者连接已断开。
char buffer[1024]; // 存放从客户端读取的数据。
memset(buffer,0,sizeof(buffer));
int readn;
char *ptr = buffer;
while(true)
{
if((readn=recv(evs[ii].data.fd,ptr,5,0))<=0)
{
// 重点:读取不到数据的时候返回-1;客户端断开的时候读取到0!别搞反了。
if((readn<0)&&(errno==EAGAIN))
{ //如果数据被读取完了,把接收到的报文内容原封不动的发回去。
send(evs[ii].data.fd,buffer,strlen(buffer),0);
printf("recv(eventfd=%d):%s\n",evs[ii].data.fd,buffer);
}
else // 客户端断开的时候读取到0!别搞反了。
{
// 如果客户端的连接已断开。
printf("client(eventfd=%d) disconnected.\n",evs[ii].data.fd);
close(evs[ii].data.fd); // 关闭客户端的socket
}
break; //跳出循环
}
else
ptr=ptr+readn; //buffer的位置指针后移。
}
}
对于读事件:
水平触发的代码写起来简单一些,可以采用阻塞的socket;边缘触发必须使用非阻塞的socket,代码写起来麻烦一些。 注意 水平触发的代码不能用于边缘触发,但是边缘触发的代码可以用于水平触发???是的。意思就是边缘触发的程序逻辑,可以直接将socket写为水平触发,程序逻辑是正确的。反之就不可以。
四 边缘触发的写事件
---------------------------------------- 下面是边缘触发模式的写事件处理方式 ------------------------------------
边缘触发的写事件的验证思路:修改增加epoll德socket事件为EPOLLOUT|EPOLLET,然后在触发写事件后循环发送1kw个报文,客户端接收一个报文后usleep(1000),1ms,那么服务端发送缓冲区会填满。发现填满后阻塞了0.1s后又开始写,说明:只有当发送缓冲区由 满 变成 不满 时,才再次触发写事件。
// 如果发生事件的是listensock,表示有新的客户端连上来。
if (evs[ii].data.fd==listensock)
{
while(true)
{
struct sockaddr_in client;
socklen_t len = sizeof(client);
int clientsock = accept(listensock,(struct sockaddr*)&client,&len);
if((clientsock<0)&&(errno==EAGAIN)) break;
printf ("accept client(socket=%d) ok.\n",clientsock);
// static int ii=0;
// printf("这是第%d个连接。\n",++ii);
// 为新客户端准备读事件,并添加到epoll中。
setnonblocking(clientsock); //客户端的socket设置为非阻塞。
ev.data.fd=clientsock;
// ev.events=EPOLLOUT; // LT-水平触发。
ev.events=EPOLLIOUT|EPOLLET; // ET-边缘触发。
epoll_ctl(epollfd,EPOLL_CTL_ADD,clientsock,&ev);
}
}
else
{
printf("触发了写事件!\n");
}