I/O多路转接之epoll
epoll是在2.6内核中提出的,是之前的select和poll的增强版本。相对于select和poll来说,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。
epoll接口
#include <sys/epoll.h>
int epoll_create(int size);
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event );
int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
(1) int epoll_create(int size);
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
2. int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
函数是对指定描述符fd执行op操作。
- epfd:是epoll_create()的返回值。
- op:表示op操作,用三个宏来表示:添加EPOLL_CTL_ADD,删除EPOLL_CTL_DEL,修改EPOLL_CTL_MOD。分别添加、删除和修改对fd的监听事件。
- fd:是需要监听的fd(文件描述符)
- epoll_event:是告诉内核需要监听什么事,struct epoll_event结构如下:
typedef union epoll_data {
void ptr;
int fd;
__uint32_t u32;
__uint64_t u64;
} epoll_data_t;
epoll_data是一个联合体,其中fd使用的最多,表示指定事件的文件描述符。ptr可以用来指定与fd相关的用户数据。如果要将文件描述符和用户数据关联起来,可以让ptr指向的用户数据这种包含fd。
struct epoll_event {
__uint32_t events;
epoll_data_t data;
};
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3. int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
功能:该函数成功时返回就绪的文件描述符的个数,失败返回-1并设置errno
等待epfd上的io事件,最多返回maxevents个事件。
参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
工作模式:
LT(level triggered)是缺省的工作方式,并且同时支持block和no-block socket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.LT同时支持阻塞和非阻塞方式。
ET(edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),ET模式在很大程度上降低了同一个epoll事件被重复触发的次数,所以ET要更高效。使用ET模式的文件描述符都应该是非阻塞的,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务都饿死。ET只支持非阻塞方式。
工作原理:
1、当调用epoll_wait获得就绪文件描述符时,返回的不是实际的描述符,而是一个代表就绪描述符数量的值,你只需要去epoll指定一个数组依次取得相应数量的文件描述符即可,这里使用了内存映射(mmap)技术,会节省一些文件描述符在系统调用时的复制开销。
2、一但文件描述符就绪,内核会采用类似callback的回调机制,迅速激活这个文件描述符,当进程调用epoll_wait时便得到通知。
3、epoll与select和poll不同,epoll将事件表完全交给内核去管理,用户只需要将要监测的文件描述符添加进入内核表中即可,等到事件就绪后内核会自动将就绪事件的文件描述符激活
epoll优点:
1、底层采用红黑树给内核管理,增删改查效率高
2、采用回调机制.
3、一旦就绪,放入就绪队列,上层直接从就绪队列读取,算法复杂度为O(1)
4、就绪队列连续存储(遍历快)
5、采用内核映射技术
6、可以采用ET模式.
#include<stdio.h>
#include<stdlib.h>
#include<unistd.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<string.h>
#include<sys/epoll.h>
#define _SIZE_ 128
typedef struct epoll_msg
{
int fd;
char buf[_SIZE_];
}epoll_t,*epoll_p,**epoll_pp;
static void * allocator(int fd)
{
epoll_p buf=(epoll_p)malloc (sizeof (epoll_t));
if (NULL==buf)
{
perror("malloc" );
exit (6 );
}
buf->fd=fd;
return buf;
}
void delalloc(void * ptr)
{
if (NULL!=ptr)
{
free (ptr);
}
}
int startup(const char * ip,int port)
{
int sock=socket(AF_INET,SOCK_STREAM,0 );
if (sock<0 )
{
perror("socket" );
exit (1 );
}
int opt=1 ;
if (setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof (opt))<0 )
{
perror("setsockopt" );
exit (2 );
}
struct sockaddr_in local;
local.sin_family=AF_INET;
local.sin_port=htons(port);
local.sin_addr.s_addr=inet_addr(ip);
if (bind(sock,(struct sockaddr*)&local,sizeof (local))<0 )
{
perror("bind" );
exit (3 );
}
if (listen(sock,5 )<0 )
{
perror("listen" );
exit (4 );
}
return sock;
}
int main(int argc,char * argv[])
{
if (argc!=3 )
{
printf ("usage:%s ip_local port_local\n" ,argv[0 ]);
return 1 ;
}
int listen_sock=startup(argv[1 ],atoi(argv[2 ]));
int epfd=epoll_create(256 );
if (epfd<0 )
{
perror("epoll_create" );
exit (5 );
}
struct epoll_event envs;
envs.events=EPOLLIN|EPOLLET;
envs.data.ptr=allocator(listen_sock);
epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&envs);
while (1 )
{
int num=0 ;
int timeout=-1 ;
struct epoll_event evs[32 ];
int max=32 ;
switch ((num=epoll_wait(epfd,evs,max,timeout)))
{
case 0 :
printf ("timeout..." );
break ;
case -1 :
perror("epoll_wait" );
break ;
default :
{
int i=0 ;
for (i=0 ;i<num;i++)
{
int fd=((epoll_p)(evs[i].data.ptr))->fd;
if (fd==listen_sock&&evs[i].events&EPOLLIN)
{
struct sockaddr_in peer;
socklen_t len=sizeof (peer);
int connfd=accept(listen_sock,(struct sockaddr*)&peer,&len);
if (connfd<0 )
{
perror("accept" );
continue ;
}
envs.events=EPOLLIN;
envs.data.ptr=allocator(connfd);
epoll_ctl(epfd,EPOLL_CTL_ADD,connfd,&envs);
}
else if (fd!=listen_sock&&evs[i].events&EPOLLIN)
{
int s=read(fd,((epoll_p)(evs[i].data.ptr))->buf,_SIZE_-1 );
if (s>0 )
{
char * buf=((epoll_p)(evs[i].data.ptr))->buf;
buf[s]=0 ;
printf ("client# %s\n" ,buf);
evs[i].events=EPOLLOUT;
epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&evs[i]);
}
else if (s==0 )
{
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL);
delalloc(evs[i].data.ptr);
evs[i].data.ptr=NULL;
close(fd);
}
else
{
perror("read" );
}
}
else if (fd!=listen_sock&&evs[i].events&EPOLLOUT)
{
char *msg="http/1.0 200 ok\r\n\r\n<html><h1>hello wrold</h1></html>" ;
write(fd,msg,strlen (msg));
delalloc(evs[i].data.ptr);
epoll_ctl(epfd,EPOLL_CTL_DEL,fd,&evs[i]);
}
}
}
}
}
return 0 ;