一、ET 和 LT 模式定义
对于采用LT工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序可以不立即处理此事件,直到该事件被处理。而对于ET工作模式的文件描述符,当epoll_wait检测到其上有事件发生并将此事件通知应用程序后,应用程序必须立即处理此事件,因为后续的epoll_wait将不会再向应用程序通知这一事件,可见,ET模式在很大程度上降低了同一个epoll事件被重复触发的次数。
LT:电平触发
发送端发送helloword,接收端recv只接受五位时,仅仅读取了hello,下一次调用epoll_wait时,未处理完的事件还会被再次触发,将剩下的world读取。
ET:边沿触发
发送端发送helloword,接收端recv时,仅仅读取了hello,下一次调用epoll_wait时,未处理完的事件不会被再次触发(通知应用程序),每次必须将就绪文件描述符上的事件处理完成。
二、epoll的事件----EPOLLET
LT 是默认的工作方式,这种模式下epoll相当于一个效率较高的poll。当往epoll内核事件表中注册有个文件描述符上的EPOLLET事件时,epoll将以ET模式来操作该文件描述符。ET模式是epoll的高效工作模式。我们今天就主要将epoll的相比较select和poll所多出来的两个事件,结合ET模式和LT模式来分析对比一下。
我们首先要明白ET为什么是高效的模式??
- 同一个事件ET只会通知一次,LT会通知多次,epoll_wait函数调用多次,epoll_wait的调用需要消耗时间。
- LT模式下,epoll_wait因为上一个事件未处理完而直接返回,造成对后续就绪事件的延迟处理。
- ET模式内核实现时,内核事件表底层是红黑树,其拥有的链表会将rdlist中就绪的文件描述符通过txlist拷贝给用户空间,并且rdlist会被清空。
- LT模式内核实现时,将rdlist中的就绪文件描述符通过txlist拷贝给用户空间,rdlist也会被清空,但是会将未处理的或处理未完成的文件描述符又返回给rdlist,以便下次继续访问。
- LT是事件处理完删除,ET是通知后就删除。
三、两者模式之间的对比以及ET代码
先给大家看一下,ET和LT在处理事件不同之处,待会我会给大家验证下ET模式下的读取数据的情况。
这就是两者在处理响应事件时候的本质区别,因此ET模式得加一个非阻塞状态,来检验数据是否已经全部被读取完毕。事件检验真理-----我们来看下ET代码的实现。
在这之前我们得明白ET模式得满足以下几点:
1.文件描述符必须设置为非阻塞状态;(因为socket在创建的时候默认为阻塞的)
2.内核事件表上的文件描述符必须关注EPOLLET事件
3.当事件发生时,必须以循环的方式处理事件,知道事件处理完成。
recv的返回值为<= 0 -----》errno 以及它的两个值 EAGAIN 和 EWOULDBLOCK
//ET模式
#include <stdio.h>
#include <stdlib.h>
#include <assert.h>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <signal.h>
#include <string.h>
#include <sys/epoll.h>
#include <fcntl.h>
#include <errno.h>
#define SIZE 100
int CreateSocket(int port,char *ip)
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);//协议簇 TCP协议
assert(sockfd != -1);
struct sockaddr_in ser,cli;
memset(&ser,0,sizeof(ser));
ser.sin_family = AF_INET;
ser.sin_addr.s_addr = inet_addr(ip); //IP地址
ser.sin_port = htons(port);
int res = bind(sockfd,(struct sockaddr *)&ser,sizeof(ser));
assert(res != -1);//绑定失败 1.IP地址不对 2.端口号被占用或者没有权限使用
listen(sockfd,5); //size = 5 内核维护的已经完成链接客户端的文件描述符个数(6)实际会加一
return sockfd;
}
void et(int fd,struct epoll_event event,int epollfd)
{
if(event.events & EPOLLRDHUP)
{
close(fd);
epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,NULL);
printf("%d client break link\n",fd);
}
else if(event.events & EPOLLIN)
{
printf("%d 's data is : \n",fd); //修改
while(1)
{
char buff[128] = {0};
int n = recv(fd,buff,5,0);
if(n <= 0)
{
if((errno == EAGAIN) || (errno == EWOULDBLOCK)) //数据读取完成
{
break;
}
close(fd);
epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,NULL);
}
printf("%s",buff); // 去掉%d fd
}
printf("\n");
send(fd,"OK",2,0);
}
else
{
printf("error\n");
}
}
void SetNonBlock(int fd) //非阻塞状态
{
int old = fcntl(fd,F_GETFL);
int new = old | O_NONBLOCK; //临时变量的值给文件描述符,改变内部属性
fcntl(fd,F_SETFL,new); //原来状态上加非阻塞
}
int main()
{
int sockfd = CreateSocket(6888,"127.0.0.1");
int epollfd = epoll_create(5);
assert(epollfd != -1);
struct epoll_event event;
event.events = EPOLLIN;
event.data.fd = sockfd;
epoll_ctl(epollfd,EPOLL_CTL_ADD,sockfd,&event);
while(1)
{
struct epoll_event events[SIZE];
int n = epoll_wait(epollfd,events,SIZE,-1);
if(n <= 0)
{
printf("Epoll error\n");
continue;
}
printf("epoll wait return\n");
//deal link
int i = 0;
for(; i < n; ++i)
{
int fd = events[i].data.fd;
if(fd == sockfd) //与客户端连接 连接不一定发生
{
struct sockaddr_in cli;
int len = sizeof(cli);
int c = accept(fd,(struct sockaddr*)&cli,&len);
if(c < 0)
{
printf("one client link error\n");
continue;
}
SetNonBlock(c);
event.events = EPOLLIN | EPOLLRDHUP | EPOLLET;
event.data.fd = c;
epoll_ctl(epollfd,EPOLL_CTL_ADD,c,&event);
}
else
{
et(fd,events[i],epollfd);
}
}
}
}
对于上述代码。我们要使用fcntl函数,它可以改变已打开的文件的性质,将阻塞设置为非阻塞。它有5种功能,我们使用的就是第三种的F_GETFL功能函数
我们需要验证ET模式的读取数据的情况,当我把buff中一次读取数据的值设为5时,我输入10个数据,会发现它是一次性读出,如下图:
满足了ET循环读完buff里数据的特征,因为它不会再次触发。而LT模式它会两次读取完buff里的数据,感兴趣的话大家可以验证下。在《Linux高性能服务器编程》的154页。
三、epoll的另一个事件---EPOLLONESHOT事件
即使我们使用ET模式,但是一个socket上的某个事件还是可能被触发多次。这在并发程序中就会引起一个问题。
比如说在读取完某个socket上的数据后开始处理这些数据,而在数据处理过程中该socket上又有新的数据可读,EPOLLIN会再次被触发,此时另外一个线程被唤醒起来读取这些数据,于是就出现了两个线程同时操作一个socket的局面,这当然不是我们所期望的,这时就可以使用epoll的EPOLLONESHOT事件实现。
对于注册了EPOLLONESHOT事件的描述符,操作系统最多触发其上注册的一个可读、可写或异常事件。这样,当一个线程在处理某个socket时,其他线程也是不可以有机会操作该socket的,但我们也要反过来思考,注册了该事件后,一旦线程处理完毕。我们应该立即重置此事件,以确保这个socket下一个可读时,其EPLOOIN事件能被触发,进而让其他工作线程有机会继续处理这个socket。
部分代码如下:
因此尽管一个socket在不同事件可能被不同的线程处理,但可以使用reset_oneshot()函数重置socket上的注册事件。并且同一时刻肯定只有一个线程为它服务,这就保证了连接的完整性,从而避免了很多可能的竞态条件。