I/O多路复用之epoll
在上一章,我们对select进行了大致的描述,知道了它相对传统的阻塞式服务提高了并发度,但是它也由于轮询而导致效率底下。本文对epoll进行讲解,相比select它的并发度更高,现代高负载服务器很多都采用这种模型。
在讲解epoll的具体用法之前,我们先看看采用 epoll模型主要用到的三个函数以及一个数据结构。
epoll中三个主要的函数:
(1)int epoll_create(int size);
功能 :生成一个epoll专用的文件描述符。
参数 :size:在该epoll fd上关注的最大socket fd数。
返回值:生成的文件描述符。
(2)int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
功能 :控制某个epoll文件描述符上的事件,可以注册事件,修改事件,删除事件。
参数 :epfd :由 epoll_create 生成的epoll专用的文件描述符;
op :EPOLL_CTL_ADD 注册、EPOLL_CTL_MOD 修 改、EPOLL_CTL_DEL 删除;
fd :关联的文件描述符;
event:指向epoll_event的指针;
返回值:0:成功;
-1:失败;
(3)int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout);
功能 :轮询I/O事件的发生。
参数 :epfd :由 epoll_create 生成的epoll专用的文件描述符;
events :用于回传待处理事件的数组;
maxevents:每次能处理的事件数;
timeout :等待I/O事件发生的超时值;-1相当于阻塞,0相当于非阻塞;
返回值:>=0 :返回发生事件数;
-1 :错误;
epoll中的主要数据结构:

1 typedef union epoll_data { 2 void *ptr; 3 int fd; 4 __uint32_t u32; 5 __uint64_t u64; 6 } epoll_data_t; 7 8 struct epoll_event { 9 __uint32_t events; /* Epoll events */ 10 epoll_data_t data; /* User data variable */ 11 };
其中,events的类型有:
EPOLLIN :文件描述符可以读;
EPOLLOUT:文件描述符可以写;
EPOLLPRI:文件描述符有紧急的数据可读;
EPOLLERR:文件描述符发生错误;
EPOLLHUP:文件描述符被挂断;
EPOLLET :文件描述符有事件发生;
epoll的使用还是很简单的,请看下面一个简单的采用epoll提供并发服务的服务端程序(注:为了简洁,都没有进行错误处理,实际使用时,一定要记住进行错误处理。):

1 #include <errno.h> 2 #include <string.h> 3 #include <sys/types.h> 4 #include <netinet/in.h> 5 #include <sys/socket.h> 6 #include <sys/wait.h> 7 #include <unistd.h> 8 #include <arpa/inet.h> 9 #include <sys/epoll.h> 10 #include <sys/time.h> 11 12 #define MAXBUF 1024 13 #define MAX_EPOLL_SIZE 10000 14 #define SERVICE_PORT 8888 15 16 17 int main(int argc, char **argv) 18 { 19 int server_fd, new_fd; 20 struct sockaddr_in server_addr, client_addr; 21 22 struct epoll_event ev; 23 struct epoll_event events[MAX_EPOLL_SIZE]; 24 25 socklen_t len = sizeof(struct sockaddr_in); 26 server_fd = socket(AF_INET, SOCK_STREAM, 0); 27 28 bzero(&server_addr, sizeof(server_addr)); 29 server_addr.sin_family = AF_INET; 30 server_addr.sin_port = htons(SERVICE_PORT); 31 server_addr.sin_addr.s_addr = INADDR_ANY; 32 33 bind(server_fd, (struct sockaddr *) &server_addr, sizeof(struct sockaddr)); 34 listen(server_fd, 1000); 35 36 //create epoll fd, and register the server listening fd 37 int epoll_fd = epoll_create(MAX_EPOLL_SIZE); 38 ev.events = EPOLLIN | EPOLLET; 39 ev.data.fd = server_fd; 40 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, server_fd, &ev); 41 42 int active_fd_count = 1; 43 while (1) 44 { 45 //wait for some events to happen 46 int event_active_fd_count = epoll_wait(epoll_fd, events, active_fd_count, -1); 47 48 // process all events 49 for (int i = 0; i < event_active_fd_count; ++i) 50 { 51 if (events[i].data.fd == server_fd) 52 { 53 new_fd = accept(server_fd, (struct sockaddr *) &client_addr,&len); 54 55 //register new fd to epoll 56 ev.events = EPOLLIN | EPOLLET; 57 ev.data.fd = new_fd; 58 epoll_ctl(epoll_fd, EPOLL_CTL_ADD, new_fd, &ev); 59 active_fd_count++; 60 } 61 else 62 { 63 handle message on events[i].data.fd 64 if (client close the connection) 65 { 66 epoll_ctl(epoll_fd, EPOLL_CTL_DEL, events[i].data.fd,&ev); 67 active_fd_count--; 68 } 69 } 70 } 71 } 72 close(server_fd); 73 return 0; 74 }
讲完epoll的常规使用方法,这里需要注意的是epoll有两种工作方式:
(1)ET:Edge Triggered,边缘触发。仅当状态发生变化时才会通知,需要细致的处理每个请求,否则容易发生丢失事件的情况。只支持非阻塞的socket。
(2)LT:Level Triggered,水平触发(默认工作方式)。只要还有没有处理的事件就会一直通知,因此不用担心事件丢失的情况。效率会低于ET触发,尤其在大并发,大流量的情况下。支持阻塞和非阻塞的socket。
最后讲讲 为什么epoll会比select高效,主要从三方面来进行论述。
(1)elect对描述符状态的改变是通过轮询来进行查找的;而epoll是当描述符状态发生改变时主动进行通知内核,这就是所谓的Reactor事件处理机制。可以用“好莱坞原则”进行描述:不要打电话给我们,我们会打电话通知你。相比之下,select的机制就好比面试结束后不停给面试官打电话询问面试结果。效率孰高孰低,可见一 斑。
(2)select的文件描述符是使用链表进行组织的;而epoll是使用红黑树这一高效数据结构组织的。
(3)select从内核到用户空间传递文件描述符上发送的信息是使用内存复制的方式进行的;而epoll是采用共享内存的方式。