一、基本知识html
epoll是在2.6内核中提出的,是以前的select和poll的加强版本。相对于select和poll来讲,epoll更加灵活,没有描述符限制。epoll使用一个文件描述符管理多个描述符,将用户关系的文件描述符的事件存放到内核的一个事件表中,这样在用户空间和内核空间的copy只需一次。linux
二、epoll函数
android
epoll操做过程须要三个接口,分别以下:windows
#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);
epoll的事件注册函数,它不一样与select()是在监听事件时告诉内核要监听什么类型的事件epoll的事件注册函数,它不一样与select()是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。第一个参数是epoll_create()的返回值,第二个参数表示动做,用三个宏来表示:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除一个fd;
第三个参数是须要监听的fd,第四个参数是告诉内核须要监听什么事,struct epoll_event结构以下:
并发
struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */};
events能够是如下几个宏的集合:
EPOLLIN :表示对应的文件描述符能够读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符能够写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来讲的。
EPOLLONESHOT:只监听一次事件,当监听完此次事件以后,若是还须要继续监听这个socket的话,须要再次把这个socket加入到EPOLL队列里app
(3) int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待事件的产生,相似于select()调用。参数events用来从内核获得事件的集合,maxevents告以内核这个events有多大,这个maxevents的值不能大于建立epoll_create()时的size,参数timeout是超时时间(毫秒,0会当即返回,-1将不肯定,也有说法说是永久阻塞)。该函数返回须要处理的事件数目,如返回0表示已超时。socket
三、工做模式
ide
epoll对文件描述符的操做有两种模式:LT(level trigger)和ET(edge trigger)。LT模式是默认模式,LT模式与ET模式的区别以下:函数
LT模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序能够不当即处理该事件。下次调用epoll_wait时,会再次响应应用程序并通知此事件。
ET模式:当epoll_wait检测到描述符事件发生并将此事件通知应用程序,应用程序必须当即处理该事件。若是不处理,下次调用epoll_wait时,不会再次响应应用程序并通知此事件。
ET模式在很大程度上减小了epoll事件被重复触发的次数,所以效率要比LT模式高。epoll工做在ET模式的时候,必须使用非阻塞套接口,以免因为一个文件句柄的阻塞读/阻塞写操做把处理多个文件描述符的任务饿死。
四、原理
1)调用epoll_create时,作了如下事情:
内核帮咱们在epoll文件系统里建了个file结点;
在内核cache里建了个红黑树用于存储之后epoll_ctl传来的socket;
创建一个list链表,用于存储准备就绪的事件。
2)调用epoll_ctl时,作了如下事情:
把socket放到epoll文件系统里file对象对应的红黑树上;
给内核中断处理程序注册一个回调函数,告诉内核,若是这个句柄的中断到了,就把它放到准备就绪list链表里。
3)调用epoll_wait时,作了如下事情:
观察list链表里有没有数据。有数据就返回,没有数据就sleep,等到timeout时间到后即便链表没数据也返回。并且,一般状况下即便咱们要监控百万计的句柄,大多一次也只返回不多量的准备就绪句柄而已,因此,epoll_wait仅须要从内核态copy少许的句柄到用户态而已。
总结以下:
一颗红黑树,一张准备就绪句柄链表,少许的内核cache,解决了大并发下的socket处理问题。
执行epoll_create时,建立了红黑树和就绪链表;
执行epoll_ctl时,若是增长socket句柄,则检查在红黑树中是否存在,存在当即返回,不存在则添加到树干上,而后向内核注册回调函数,用于当中断事件来临时向
准备就绪链表中插入数据;
执行epoll_wait时马上返回准备就绪链表里的数据便可。
两种模式的实现:
当一个socket句柄上有事件时,内核会把该句柄插入上面所说的准备就绪list链表,这时咱们调用epoll_wait,会把准备就绪的socket拷贝到用户态内存,而后清空准备就绪list链表,最后,epoll_wait检查这些socket,若是是LT模式,而且这些socket上确实有未处理的事件时,又把该句柄放回到刚刚清空的准备就绪链表。因此,LT模式的句柄,只要它上面还有事件,epoll_wait每次都会返回。
五、epoll的优势
自己没有最大并发链接的限制,仅受系统中进程能打开的最大文件数目限制;
效率提高:只有活跃的socket才会主动的去调用callback函数;
省去没必要要的内存拷贝:epoll经过内核与用户空间mmap同一块内存实现。
固然,以上的优缺点仅仅是特定场景下的状况:高并发,且任一时间只有少数socket是活跃的。若是在并发量低,socket都比较活跃的状况下,select就不见得比epoll慢了(就像咱们经常说快排比插入排序快,可是在特定状况下这并不成立)。
六、epoll的缺点
1. 相对select来讲, epoll的跨平台性不够用 只能工做在linux下, 而select能够在windows linux apple上使用, 还有手机端android iOS之类的均可以. android虽然是linux的 内核 但早期版本一样不支持epoll
2. 相对select来讲 仍是用起来仍是复杂了一些, 不过和IOCP比起来 增长了一点点的复杂度却基本上达到了IOCP的并发量和性能, 而复杂度远远小于IOCP.
3. 相对IOCP来讲 对多核/多线程的支持不够好, 性能也所以在性能要求比较苛刻的状况下不如IOCP.
七、epoll实现的TCP server
代码:
#include <stdio.h> #include <sys/types.h> #include <sys/socket.h> #include <netinet/in.h> #include <arpa/inet.h> #include <sys/epoll.h> #include <string.h> static void my_read(int epfd,int fd,char *buf,int len) { int rs=1; while(rs) { ssize_t size=recv(fd,buf,len,0); if(size<0) { if(errno=EAGAIN) { break; } else { perror("recv"); return 9; } } else if(size==0) { //表示对端的sock已经正常关闭 printf("client close ...\n"); struct epoll_event ev; epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL); close(fd); break; } else{ buf[size-1]='\0'; printf("client # %s\n",buf); if(size==len) { rs=1; } else rs=0; } } } static void my_write(int fd,char *buf,int len) { int ws=1; while(ws) { ssize_t size=send(fd,buf,len,0); if(size<0) { //缓冲区已经满了,延时重试 if(errno==EAGAIN) { usleep(1000); continue; } if(errno==EINTR) { return -1; } } if(size==len) { continue; } len-=size; buf+=size; } } static void set_no_block(int fd) { //先获得以前的状态,在以前状态上在加入新状态 int before=fcntl(fd,F_GETFL); fcntl(fd,F_SETFL,before|O_NONBLOCK); } static int startup(const char *ip,int port) { int sock=socket(AF_INET,SOCK_STREAM,0); if(sock<0) { perror("sock"); return 2; } int opt=1; setsockopt(sock,SOL_SOCKET,SO_REUSEADDR,&opt,sizeof(opt)); 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"); return 3; } if(listen(sock,5)<0) { perror("listen"); return 4; } return sock; } static void usage(const char *proc) { printf("%s [ip] [port]",proc); } int main(int argc,char *argv[]) { if(argc!=3) { usage(argv[0]); return 1; } int listen_sock=startup(argv[1],atoi(argv[2])); int epfd=epoll_create(128); if(epfd<0) { perror("epoll_create"); return 5; } struct epoll_event ev; ev.events=EPOLLIN; ev.data.fd=listen_sock; if(epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev)<0) { perror("epoll_ctl"); return 6; } struct epoll_event evs[128]; int len=sizeof(evs)/sizeof(evs[0]); int ready=0; int timeout= -1; while(1) { switch(ready=epoll_wait(epfd,evs,len,timeout)) { case 0: printf("timeout..\n"); break; case -1: perror("epoll_wait"); return 7; break; default: { int i=0; for(i;i<ready;i++) { //LISTEN SOCKET int fd=evs[i].data.fd; if(i==0&&fd==listen_sock&&evs[i].events&EPOLLIN) { struct sockaddr_in peer; socklen_t len=sizeof(peer); int new_sock=accept(listen_sock,(struct sockaddr*)&peer,&len); if(new_sock<0) { perror("accept"); return 8; } else { //在epoll_wait以后改变其文件状态,为非阻塞,ET工做 set_no_block(new_sock); printf("get new socket:ip %s:port %d\n",inet_ntoa(peer.sin_addr),ntohs(peer.sin_port)); ev.events=EPOLLIN|EPOLLET; ev.data.fd=new_sock; if(epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev)<0) { perror("epoll_ctl"); return 9; } } } else { //read if(evs[i].events&EPOLLIN) { char buf[1024]; //my_read(fd,buf,len); ssize_t _s=recv(fd,buf,sizeof(buf)-1,0); if(_s>0) { buf[_s-1]='\0'; printf("client # %s\n",buf); //read finish change to write ev.data.fd=fd; ev.events=EPOLLOUT; epoll_ctl(epfd,EPOLL_CTL_MOD,fd,&ev); } else if(_s==0) { printf("client close ....\n"); epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL); close(fd); } else { perror("recv"); return 10; } } //write else if(evs[i].events&EPOLLOUT) { char *msg="HTTP/1.1 200 OK\r\n\r\n<html><h1>hello momo</h1></html>\r\n"; //my_send(fd,msg,sizeof(msg)-1); send(fd,msg,strlen(msg),0); epoll_ctl(epfd,EPOLL_CTL_DEL,fd,NULL); close(fd); } else { continue; } } } } break; } } return 0; }