一.epoll的LT和ET模式介绍
epol 对文件描述符有两种操作模式:LT(Level Trigger,电平触发)模式和 ET(EdgeTrigger,边沿触发)模式。LT模式是默认的工作模式。当往epol 内核事件表中注册一个文件描述符上的 EPOLLET 事件时,epoll将以高效的 ET 模式来操作该文件描述符。
epoll的LT和ET模式区别示意图
二.epoll的ET模式如何处理
1.epoll的ET模式编程读取数据的处理方式
将描述符设置为非阻塞模式 +循环读取数据
也就是ET模式下的描述符必须是非阻塞的
2.将描述符设置为非阻塞模式的方法
//ser_epo11_ET.C
#include <fcntl.h>
void setnonblock(int fd)
{
int oldfl=fcntl(fd,F_GETFL);
int newfl=oldfl=O_NONBLOCK;
if(fcntl(fd,F SETFL,newfl)==-1)
perror("fcntl error\n");
}
三.ET模式的完整代码
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <string.h>
#include <assert.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include<fcntl.h>
#include<errno.h>
#define MAXFD 10
void setnoblock(int fd)
{
int oldfl=fcntl(fd,F_GETFL);
int newfl=oldfl|O_NONBLOCK;
if(fcntl(fd,F_SETFL,newfl)==-1)
{
perror("fcntl error!\n");
}
}
int create_socket()
{
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("127.0.0.1");
int res=bind(sockfd,(struct sockaddr *)&saddr,sizeof(saddr));
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;
ev.data.fd=fd;
setnoblock(fd);
if(epoll_ctl(epfd,EPOLL_CTL_ADD,fd,&ev)==-1)
{
perror("epoll ctl add err");
}
}
void epoll_del(int epfd,int fd)
{
if(epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL)==-1)
{
perror("epoll ctl add err\n");
}
}
int main()
{
int sockfd=create_socket();
assert(sockfd!=-1);
int epfd=epoll_create(MAXFD);
assert(epfd!=-1);
epoll_add(epfd,sockfd);
struct epoll_event evs[MAXFD];
while(1)
{
int n=epoll_wait(epfd,evs,MAXFD,5000);
if(n==-1)
{
perror("epoll_wait error\n");
continue;
}
else if(n==0)
{
printf("time out\n");
continue;
}
else
{
for(int i=0;i<n;i++)
{
int fd=evs[i].data.fd;
if(evs[i].events & EPOLLIN)
{
if(fd==sockfd)
{
//accept
struct sockaddr_in caddr;
int len=sizeof(caddr);
int c=accept(fd,(struct sockaddr *)&caddr,&len);
if(c<0)
{
continue;
}
printf("accept c=%d\n",c);
epoll_add(epfd,c);
}
else
{
// recv
while(1)
{
char buff[128]={0};
int num=recv(fd,buff,1,0);
if(num==-1)
{
if(errno!=EAGAIN && errno!=EWOULDBLOCK)
{
perror("recv err!");
}
else
{
send(fd,"ok",2,0);
}
break;
}
else if(num==0)
{
epoll_del(epfd,fd);
close(fd);
printf("client close\n");
break;
}
else
{
printf("buff(%d)=%s\n",fd,buff);
}
}
}
}
}
}
}
exit(0);
}
四.总结
ET模式下我们都需要做哪些事情?一共三点:
(1)添加事件类型的时候一定要添加上EPOLLET,这叫开启ET模式
(2)描述符要设置成非阻塞模式:
(3)在IO函数返回以后,就是epoll wait返回以后,它提醒我们描述符上有读事件产生了,我们要循环去处理;直到把这个描述符上的数据处理完;然后再去处理下一个描述符;
所以就是说ET模式下的编程就要求我们的描述符必须是非阻塞模式,
如下图:
对于 LT 模式操作的文件描述符,当 epoll wait 检测到其上有事件发生并将此事件通知应用程序
后,应用程序可以不立即处理该事件。这样,当应用程序下一次调用epoll wait 时,还会再次向
应用程序通告此事件,直到该事件被处理。
对于 ET 模式操作的文件描述符,当 epoll wait 检测到其上有事件发生并将此事件通知 应用程
序后,应用程序必须立即处理该事件,因为后续的 epoll wait 调用将不再向应用程序通知这一事
件。所以 ET模式在很大程度上降低了同一个 epol 事件被重复触发的次数,因此效率比 LT 模式
高。
五.EPOLLONESHOT事件
即使我们使用 ET模式,一个socket上的某个事件还是可能被触发多次。这在并发程序
中就会引起一个问题。比如一个线程(或进程,下同)在读取完某个socket上的数据后开
始处理这些数据,而在数据的处理过程中该socket上又有新数据可读(EPOLLIN再次被触
发),此时另外一个线程被唤醒来读取这些新的数据。于是就出现了两个线程同时操作一个
socket的局面。这当然不是我们期望的。我们期望的是一个socket连接在任一时刻都只被-
个线程处理。这一点可以使用epoll的EPOLLONESHOT事件实现。
对于注册了EPOLLONESHOT事件的文件描述符,操作系统最多触发其上注册的一个可
读、可写或者异常事件,且只触发一次,除非我们使用cpollct函数重置该文件描述符上注
册的 EPOLLONESHOT 事件。这样,当一个线程在处理某个socket时,其他线程是不可能有
机会操作该 socket的。但反过来思考,注册了EPOLLONESHOT事件的socket一旦被某个
线程处理完毕,该线程就应该立即重置这个socket上的EPOLLONESHOT事件,以确保这个
socket 下一次可读时,其EPOLLIN事件能被触发,进而让其他工作线程有机会继续处理这
个 socket。
原因:不希望出现两个线程同时操作一个socket的局面;
使用EPOLLONESHOT事件之后,当一个线程在处理某个socket时,其他线程时不可能有机会操作该
socket的.
六.同步非同步,阻塞非阻塞
Linux-计算机网络-探索epoll是同步阻塞的还是异步非阻塞的-CSDN博客