一。什么是epoll
按照man⼿册的说法:是为处理⼤批量句柄⽽作了改进的poll。当然,这不是2.6内核才有的,它是在2.5.44内核中被引进的(epoll(4) is a new API introduced in Linux kernel
2.5.44),它⼏乎具备了之前所说的⼀切优点,被公认为Linux2.6下性能最好的多路I/O就绪通知⽅法。
在了解epoll之前,我们要了解两个函数:
select 和 poll 函数的功能:
- select一次可以监测
FD_SETSIZE
数量大小的描述符,FD_SETSIZE 通常是一个在 libc 编译时指定的小数字。- poll一次可以监测的描述符数量并没有限制,但撇开其它因素,我们每次都不得不检查就绪通知,线性扫描所有通过描述符,这样时间复杂度为 O(n)而且很慢。
epoll 没有这些固定限制,也不执行任何线性扫描。因此它可以更高效地执行和处理大量事件。
二。epoll的函数及其功能:
1. int epoll_create(int size);
创建⼀个epoll的句柄。⾃从linux2.6.8之后,size参数是被忽略的。需要注意的是,当创建好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_create()的返回值。
第⼆个参数表⽰动作,⽤三个宏来表⽰:
EPOLL_CTL_ADD:注册新的fd到epfd中;
EPOLL_CTL_MOD:修改已经注册的fd的监听事件;
EPOLL_CTL_DEL:从epfd中删除⼀个fd;
第三个参数是需要监听的fd。
第四个参数是告诉内核需要监听什么事。struct epoll_event结构体如下
typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t; struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };
<span style="font-family:Microsoft YaHei;">typedef union epoll_data { void *ptr; int fd; __uint32_t u32; __uint64_t u64; } epoll_data_t; struct epoll_event { __uint32_t events; /* Epoll events */ epoll_data_t data; /* User data variable */ };</span>
events可以是以下⼏个宏的集合:
EPOLLIN :表⽰对应的⽂件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表⽰对应的⽂件描述符可以写;
EPOLLPRI:表⽰对应的⽂件描述符有紧急的数据可读(这⾥应该表⽰有带外数据到来);
EPOLLERR:表⽰对应的⽂件描述符发⽣错误;
EPOLLHUP:表⽰对应的⽂件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于⽔平触发(LevelTriggered)来说的。
EPOLLONESHOT:只监听⼀次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加⼊到EPOLL队列⾥。
3.int epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout);
等待在epoll监控的事件中已经发生的事件。参数events是分配好的epoll_event结构体数组,epoll将会把发⽣的事件赋值到events数组中(events不可以是空指针,内核只负责把数据复制到这个events数组中,不会去帮助我们在⽤户态中分配内存)。maxevents告之内核这个events有多⼤,这个 maxevents的值不能⼤于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会⽴即返回,-1将不确定,也有说法说是永久阻塞)。如果函数调⽤成功,返回对应I/O上已准备好的⽂件描述符数⽬,如返回0表⽰已超时。
epoll的工作方式:
LT:水平触发,效率会低于ET触发,尤其在大并发,大流量的情况下。但是LT对代码编写要求比较低,不容易出现问题。LT模式服务编写上的表现是:只要有数据没有被获取,内核就不断通知你,因此不用担心事件丢失的情况。
ET:边缘触发,效率非常高,在并发,大流量的情况下,会比LT少很多epoll的系统调用,因此效率高。但是对编程要求高,需要细致的处理每个请求,否则容易发生丢失事件的情况。
下面举一个列子来说明LT和ET的区别(都是非阻塞模式,阻塞就不说了,效率太低):
采用LT模式下, 如果accept调用有返回就可以马上建立当前这个连接了,再epoll_wait等待下次通知,和select一样。
但是对于ET而言,如果accpet调用有返回,除了建立当前这个连接外,不能马上就epoll_wait还需要继续循环。
三。基于epoll的网络服务器实例代码:
#include<stdio.h> #include<sys/socket.h> #include<sys/types.h> #include<netinet/in.h> #include<arpa/inet.h> #include<fcntl.h> #include<stdlib.h> #include<unistd.h> #include<sys/epoll.h> #include<string.h> #define EPOLL_REVS_SIZE 64 static void usage(const char *proc) { printf("Usage: %s [local_ip] [local_port]\n",proc); } //typedef struct ep_buff{ //ET模式下需要的缓冲区 // //一个文件描述符对应一个缓冲区 // int fd; // char buff[1024]; //}ep_buff_t,*ep_buff_p; // //void* alloc_ep_buff(int fd) //{ // ep_buff_p n = (ep_buff_p)malloc(sizeof(ep_buff_t)); // if(!n){ // perror("malloc"); // exit(6); // } // // n->fd = fd; // return n; //} //int set_fd_nonblaock() //ET模式下的函数 非阻塞 //{ // //} //int myread() //{ // while(1){} //} // //int mywrite() //{ // while(1){ // } //} // //int myaccept(int epfd,int listen_sock)//获得一个新连接,就传进去 //{ // //} int startup(const char *ip,int port) { int sock = socket(AF_INET,SOCK_STREAM,0); if(sock < 0){ perror("socket"); exit(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"); exit(3); } if(listen(sock,10) < 0){ perror("listen"); exit(4); } return sock; } 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(256); if(epfd < 0){ perror("epoll_create"); return 5; } struct epoll_event ev;//创建需要监听的事件 ev.events = EPOLLIN ;//| EPOLLET; //ET模式 ev.data.fd = listen_sock; //ev.data.ptr = alloc_ep_buff(listen_sock); epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);//把listen_sock加入到epfd中 int nums = -1; struct epoll_event revs[EPOLL_REVS_SIZE];//用来接收底层的事件数组 int timeout = -1; while(1){ switch((nums = epoll_wait(epfd,revs,\ EPOLL_REVS_SIZE,timeout))){ case 0://代表超时 perror("timeout...\n"); break; case -1://代表函数出错 perror("epoll_wait"); break; default://此时至少有一个已经就绪 { int i = 0; for(;i < nums;i++){ //int sock =((ep_buff_p)(revs[i].data.ptr))->fd; int sock = revs[i].data.fd; if(sock == listen_sock && \ (revs[i].events & EPOLLIN)){ //listen_sock ready!!! struct sockaddr_in client; socklen_t len = sizeof(client); ///创建新连接 int new_sock = accept(listen_sock,(struct sockaddr*)&client,&len); if(new_sock < 0){ perror("accept"); continue; } printf("get client: [%s:%d]\n",\ inet_ntoa(client.sin_addr), ntohs(client.sin_port)); ev.data.fd = new_sock; //关注new_sock ev.events = EPOLLIN; //此事件可以读 //ev.data.ptr = alloc_ep_buff(new_sock); //将new_sock加入到epfd中监听ev,也就是读事件 epoll_ctl(epfd,EPOLL_CTL_ADD,new_sock,&ev); }else if(sock != listen_sock){ //如果不是listen_sock if(revs[i].events & EPOLLIN){ //正常的fd 读事件就绪 char buf[10240]; ssize_t s = read(sock, buf,sizeof(buf)-1);
//bug 如果一次不能读完的话,下次的数据会直接覆盖 if(s > 0){ printf("client: %s\n",buf); ev.events = EPOLLOUT; //ptr一直没有变,可以不用改 //((ep_buff_p)(ev.data.ptr))->fd = sock; ev.data.fd = sock; epoll_ctl(epfd,EPOLL_CTL_MOD,sock,&ev); }else if(s <= 0){ printf("client quit!!!!\n"); epoll_ctl(epfd,EPOLL_CTL_DEL,sock,NULL); close(sock); //free(revs[i].data.ptr);//ET }else{ perror("read"); epoll_ctl(epfd,EPOLL_CTL_DEL,sock,NULL); close(sock); //free(revs[i].data.ptr);//ET } }else if(revs[i].events & EPOLLOUT){//写事件就绪 const char *msg = "HTTP/1.0 OK 200\r\n\r\n<html><h1>hello epoll!</h1></html>"; write(sock,msg,strlen(msg));//bug!!! epoll_ctl(epfd,EPOLL_CTL_DEL,sock,NULL); close(sock); //free(revs[i].data.ptr);//ET }else{} } } }
break;
} } return 0; }
优缺点:http://blog.csdn.net/a1414345/article/details/73385556