对于poll我们也有很大的缺陷,比如我们依旧需要每次监听时从用户态将要监听的事件拷贝到内核态进行操作,并且poll与select都不能直接返回我们已就绪的文件描述符,而是需要用户进行循环判断事件是否就绪。。。Linux给我们提供了一个独有的I/O复用函数epoll,解决了这些问题。
epoll是Linux独有的,是select和poll的改进
epoll API详解
epoll不像poll和select只有单个函数,它是由一组函数来完成任务。epoll每次将用户直接将关心的事件放到内核里的一个事件表中,就不需要像select和poll那样每次调用都要重复传入用户定义的文件描述符集或事件集以拷贝给内核(省去了一次拷贝),epoll只需要使用一个额外的文件描述符来标识这个内核事件表。
创建内核事件表
#include<sys/epoll.h>
int epoll_create(int size);
//返回的文件描述符标志内核事件表
size:这个参数不起什么作用,只是给内核一个提示,告诉它事件表需要多大,实际上epoll底层这个内核事件表就是一棵红黑树,是可以扩展的且查找效率高,所以该参数没有什么太大意义,你甚至可以传递0。
操作内核事件表
#incldue<sys/epoll.h>
int epoll_ctl(int epfd,int op,int fd,struct epoll_event* event);
//成功返回0,失败返回-1并设置errno
epfd:指定要操作的内核事件表
op:指定操作的类型
EPOLL_CTL_ADD 往内核事件表中注册fd上关心的事件
EPOLL_CTL_MOD 在内核事件表中修改fd上的注册的事件
EPOLL_CTL_DEL 在内核时间表中删除fd上的注册的事件
fd:即要进行监听的文件描述符
event:指定关注事件类型
struct epoll_event
{
__uint32_t events; /*epoll事件*/
epoll_data_t data; /*用户数据*/
};
typedef union epoll_data
{
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
}epoll_data_t;
epoll_event中的events是用户所关注的fd上的事件类型,与poll的事件类型差不多,表示epoll的事件就在poll对应的宏前面加上"E"。但是epoll有两个额外的事件类型---EPOLLET和EPOLLONESHOT;对于epoll_event中的data常用于存储用户数据,它是一个epoll_data的联合体,使用最多的是fd,指定事件所从属的目标文件描述符,与epoll_ctl的第三个参数值往往是一样的。ptr成员往往用来指定fd相关的用户数据(联合体fd与ptr不能同时使用,可以在ptr指向的用户数据中包含fd)。epoll_data中的fd往往是用来epoll_wait中由内核填充使得用户可以获取到这个就绪的文件符的。
启动监听内核事件表
#include<sys/epoll.h>
int epoll_wait(int epfd,struct epoll_event* events,int maxevents,int timeout);
//成功返回就绪文件描述符的个数,失败返回-1
timeout:与poll的参数意义一样,-1为阻塞直到有就绪事件发生,0是直接返回
maxevents:指定最多监听多少个事件(第二个参数数组大小),当达到maxevents时epoll_wait会返回
events:epoll_wait如果检测到事件,就将所有的就绪的事件从内核事件表中复制到用户定义的这第二个参数events指向的数组中,这个数组只输出epoll_wait检测到的就绪事件,不用用户循环查找,提高了应用程序索引就绪文件描述符的效率O(1).
代码实现
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<string.h>
#include<assert.h>
#include<sys/epoll.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<arpa/inet.h>
#include<netinet/in.h>
#define SIZE 100
int main()
{
int sockfd=socket(AF_INET,SOCK_STREAM,0);
assert(sockfd!=-1);
struct sockaddr_in ser,cli;
ser.sin_family=AF_INET;
ser.sin_port=htons(7700);
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 epollfd=epoll_create(5); //创建内核事件表,5并不起作用,内核红黑树,可扩展
assert(epollfd!=-1);
struct epoll_event event; //将sockfd加入内核事件表
event.events=EPOLLIN; //sockfd的触发事件设为可读
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_wait error\n");
continue;
}
int i=0;
for(;i<n;i++) //n即就绪描述符的个数
{
int fd=events[i].data.fd;
if(fd==sockfd)
{
int len=sizeof(cli);
int c=accept(sockfd,(struct sockaddr*)&cli,&len);
if(c<0)
continue;
event.events=EPOLLIN|EPOLLRDHUP; //需要将连接套接子加入内核事件表,设置其触发事件为断开连接或可读
event.data.fd=c;
epoll_ctl(epollfd,EPOLL_CTL_ADD,c,&event);
}
else if(events[i].events&EPOLLRDHUP)//同poll一样,先分析断开链接
{
epoll_ctl(epollfd,EPOLL_CTL_DEL,fd,NULL);
close(fd);
printf("%d:over\n",fd);
}
else if(events[i].events&EPOLLIN)
{
char buff[128]={0};
int n=recv(fd,buff,127,0);
if(n==-1)
{
printf("recv failed\n");
continue;
}
printf("%d:%s\n",fd,buff);
send(fd,"ok",2,0);
}
else
continue;
}
}
}
工作模式
epoll对文件描述符的操作有两种模式:LT(level trigger)和ET(edge trigger)
LT模式是默认模式,LT模式与ET模式的区别如下:
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序可以不立即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须立即处理该事件。如果不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
1. LT模式LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。
2. ET模式ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once)
ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。