epoll的LT模式
LT模式:电平触发,这是epoll默认模式,这时的epoll可以说是高效的poll模式,epoll_wait在收到触发事件后立刻返回给应用程序,应用程序可以不立即处理事件,下一次epoll_wait还会返回该事件的通知给应用程序,即,一次没处理完或者没处理,下次可以继续处理(也就是fd没有被内核事件表删除,依然会触发响应)
一定程度上保证了数据的安全性?并不,只是把事件再次放回队列中,不进行销毁处理,只有当事务逻辑处理完时,才会调用EPOLL_CTL_DEL宏删除fd上注册的事件,响应一次处理一次,不一定处理完,这就会导致同一个文件描述符可能会被重复触发多次,效率和ET模式比起来要慢一些。
具体代码参见https://blog.youkuaiyun.com/Kevinonlie/article/details/89497654 Linux的三种I/O复用方式——epoll中的代码,默认模式不必注册额外事件。
epoll的ET高效模式
ET模式:边沿触发,注册EPOLLET事件(位运算的与操作)后,epoll将会以ET模式运行,这时,epoll_wait如果将事件返回给应用程序,应用程序必须处理事件(不处理也行,但这样就没意义了),下一次epoll_wait不会再一次通知该事件给应用程序,也就是传递之后自动将事件从内核事件表中剔除,如果一次没处理完则会丢弃事件,这时需要在处理事件加入while(1)循环,防止一次没读完导致数据丢失,可见,ET模式降低了同一个epoll事件被触发的次数,效率要比LT高很多。
就算是ET事件,同一个socket上的某个事件还是可能被触发多次,这时需要注册EPOLLONESHOT事件(还是位运算的与操作)可以保证同一个socket在一段时间内只能有一个工作线程进行操作
ET模式类似于没有缓存区的发送窗口,每当发送数据到接受窗口不获取发送方的确认报文,立刻丢弃数据,更加高效,但是在实现上对服务器处理数据程序的要求就更加苛刻。
接下来实现一段ET模式的代码
#define _GNU_SOURCE
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <assert.h>
#include <string.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <arpa/inet.h>
#include <netinet/in.h>
#include <sys/epoll.h>
#include <fcntl.h>
#define FDMAXNUM 100
void reset_oneshot(int epfd,int fd)//reset a fd
{
struct epoll_event event;
event.data.fd = fd;
event.events = EPOLLIN | EPOLLONESHOT | EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&event);
}
void DealClientData(int fd,int epfd,short events)
{
if(events & EPOLLRDHUP)
{
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
close(fd);
}
else if(events & EPOLLIN)
{
while(1)
{
char buff[128] = {0};
int n = recv(fd,buff,127,0);
if(n <= 0|strncmp(buff,"end",3) == 0)
{
printf("Love is over\n");
reset_oneshot(epfd,fd);
close(fd);
return;
}
printf("%s\n", buff);
send(fd,"OK",2,0);
}
}
}
int Setnonblocking(int fd)
{
int old_option = fcntl(fd,F_GETFL);
int new_option = old_option | O_NONBLOCK;
fcntl(fd,F_GETFL,new_option);
return old_option;
}
void GetClientLink(int fd,int epfd,struct epoll_event *event,struct sockaddr_in cli)
{
int len = sizeof(cli);
int c = accept(fd,(struct sockaddr*)&cli,&len);
assert(c != -1);
event->events = EPOLLIN | EPOLLRDHUP | EPOLLET;
event->data.fd = c;
epoll_ctl(epfd,EPOLL_CTL_ADD,c,event);
Setnonblocking(c);
}
void DealFinshEvent(int sockfd,int epfd,struct epoll_event *events,
int n,struct sockaddr_in cli,struct epoll_event *event)
{
int i = 0;
for(;i < n;++i)
{
int fd = events[i].data.fd;
if(fd == sockfd)
{
GetClientLink(fd,epfd,event,cli);
}
else
{
DealClientData(fd,epfd,events[i].events);
}
}
}
int main(int argc, char const *argv[])
{
int sockfd = socket(AF_INET,SOCK_STREAM,0);
assert(sockfd != -1);
struct sockaddr_in ser,cli;
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));
assert(res != -1);
listen(sockfd,5);
int epfd = epoll_create(5);
assert(epfd != -1);
struct epoll_event event;
event.data.fd = sockfd;
event.events = EPOLLIN | EPOLLET;
epoll_ctl(epfd,EPOLL_CTL_ADD,sockfd,&event);
while(1)
{
struct epoll_event events[FDMAXNUM];
int n = epoll_wait(epfd,events,FDMAXNUM,-1);//Wait a event
if(n <= 0)
{
printf("This Client is unlink");
continue;
}
DealFinshEvent(sockfd,epfd,events,n,cli,&event);
}
close(sockfd);
return 0;
}
ET模式关键在于注册EPOLLET和EPOLLONESHOT事件,EPOLLET是epoll激活ET模式的关键事件,所有监听的事件都必须激活ET模式,包括服务器的监听sockfd事件(一般写为listenfd)
EPOLLONESHOT事件是确保ET模式的正常工作,EPOLLONESHOT的内容是保证注册了本事件的文件描述符只被响应一次,此时,如果不对事务处理进行修改,那么只会接受一次数据,超过本次接收的长度的内容将会被丢弃,这是很不明智的选择,所以我们要对epoll的事务处理作出修改。
while(1)
{
char buff[128] = {0};
int n = recv(fd,buff,127,0);
if(n <= 0|strncmp(buff,"end",3) == 0)
{
printf("Love is over\n");
reset_oneshot(epfd,fd);
close(fd);
return;
}
printf("%s\n", buff);
send(fd,"OK",2,0);
}
做一个while(1)循环,保证选择连接的客户端数据被正常处理完成,同时在处理事务逻辑的时候添加了新函数reset_oneshot,这个函数实现了异步I/O的功能,让服务器进程能够在接收到一个连接请求交给工作线程后可以立即再次等待新的连接到来,这样可以增加服务器性能,reset_oneshot函数内容在上文有提到,这里使用了fcntl函数和相应的事件实现异步I/O功能。
int fcntl(int fd, int cmd);
int fcntl(int fd, int cmd, long arg);
int fcntl(int fd, int cmd, struct flock *lock);
1.复制一个现有的描述符(cmd=F_DUPFD).
2.获得/设置文件描述符标记(cmd=F_GETFD或F_SETFD).
3.获得/设置文件状态标记(cmd=F_GETFL或F_SETFL).
4.获得/设置异步I/O所有权(cmd=F_GETOWN或F_SETOWN).
5.获得/设置记录锁(cmd=F_GETLK,F_SETLK或F_SETLKW).
简单的来说,fcntl作用是更改fd文件描述符的内容/状态,或复制文件描述符,或设置异步I/O权限。