一、基本概念
epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。
另一点原因就是获取事件的时候,它无须遍历整个被侦听的描述符集,只要遍历那些被内核IO事件异步唤醒而加入Ready队列的描述符集合就行了。epoll除了提供select/poll那种IO事件的
水平触发(Level Triggered)外,还提供了边缘触发(Edge Triggered),这就使得用户空间程序有可能缓存IO状态,减少epoll_wait/epoll_pwait的调用,提高应用程序效率。
二、epoll函数解析
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、epoll_create(int size):
创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。这个参数不同于select()中的第一个参数,给出最大监听的fd+1的值。需要注意的是,当创建好epoll句柄后,它就是会占用一个fd值,
在linux下如果查看/proc/进程id/fd/,是能够看到这个fd的,所以在使用完epoll后,必须调用close()关闭,否则可能导致fd被耗尽。
2、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结构如下:
typedef union epoll_data{void *ptr;int fd;__uint32_t u32;__uint64_t u64;}epoll_data_t;
events可以是以下几个宏的集合:struct epoll_event {__uint32_t events; /* Epoll events */epoll_data_t data; /* User data variable */};
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这里应该表示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(Level Triggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里
3、epoll_wait(int epfd, struct epoll_event * events, int maxevents, int timeout)
等待事件的产生,类似于select()调用。参数events用来从内核得到事件的集合,maxevents告之内核这个events有多大,这个maxevents的值不
能大于创建epoll_create()时的size,参数timeout是超时时间(毫秒,0会立即返回,-1将不确定,也有说法说是永久阻塞)。该函数返回需要处理的事件数目,如返回0表示已超时。
三、epoll 的工作原理
epoll同样只告知那些就绪的⽂文件描述符,⽽而且当我们调⽤用epoll_wait()获得就绪⽂文件描述符时,返回的不是实际的描述符,⽽而是⼀一个代表就绪描述符数量的值,
你只需要去epoll指定的⼀个数组中依次取得相应数量的⽂文件描述符即可,这⾥里也使⽤用了内存映射(mmap)技术,这样便彻底省掉了这些⽂文件描述符在系统调⽤用时复制的开销。
另⼀一个本质的改进在于epoll采⽤用基于事件的就绪通知⽅方式。在select/poll中,进程只有在调⽤用⼀一定的⽅方法后,内核才对所有监视的⽂文件描述符进⾏行扫描,
⽽而epoll事先通过epoll_ctl()来注册⼀一个⽂文件描述符,⼀一旦基于某个⽂文件描述符就绪时,内核会采⽤用类似callback的回调机制,迅速激活这个⽂文件描述符,
当进程调⽤用epoll_wait()时便得到通知。
Epoll的2种⼯工作⽅方式-⽔水平触发(LT)和边缘触发(ET):
假如有这样一个例子:
1. 我们已经把一个用来从管道中读取数据的文件句柄(RFD)添加到epoll描述符
2. 这个时候从管道的另一端被写入了2KB的数据
3. 调用epoll_wait(2),并且它会返回RFD,说明它已经准备好读取操作
4. 然后我们读取了1KB的数据
5. 调用epoll_wait(2)......
Edge Triggered 工作模式:
如果我们在第1步将RFD添加到epoll描述符的时候使用了EPOLLET标志,那么在第5步调用epoll_wait(2)之后将有可能会挂
起,因为剩余的数据还存在于文件的输入缓冲区内,而且数据发出端还在等待一个针对已经发出数据的反馈信息。只有在监视
的文件句柄上发生了某个事件的时候 ET 工作模式才会汇报事件。因此在第5步的时候,调用者可能会放弃等待仍在存在于文
件输入缓冲区内的剩余数据。在上面的例子中,会有一个事件产生在RFD句柄上,因为在第2步执行了一个写操作,然后,
事件将会在第3步被销毁。因为第4步的读取操作没有读空文件输入缓冲区内的数据,因此我们在第5步调用 epoll_wait(2)完成
后,是否挂起是不确定的。epoll工作在ET模式的时候,必须使用非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操
作把处理多个文件描述符的任务饿死。最好以下面的方式调用ET模式的epoll接口,在后面会介绍避免可能的缺陷。
i 基于非阻塞文件句柄
ii 只有当read(2)或者write(2)返回EAGAIN时才需要挂起,等待。但这并不是说每次read()时都需要循环读,直到读到产生一
个EAGAIN才认为此次事件处理完成,当read()返回的读到的数据长度小于请求的数据长度时,就可以确定此时缓冲中已没有
数据了,也就可以认为此事读事件已处理完成。
Level Triggered 工作模式
相反的,以LT方式调用epoll接口的时候,它就相当于一个速度比较快的poll(2),并且无论后面的数据是否被使用,因此他们具
有同样的职能。因为即使使用ET模式的epoll,在收到多个chunk的数据的时候仍然会产生多个事件。调用者可以设定
EPOLLONESHOT标志,在 epoll_wait(2)收到事件后epoll会与事件关联的文件句柄从epoll描述符中禁止掉。因此当
EPOLLONESHOT设定后,使用带有 EPOLL_CTL_MOD标志的epoll_ctl(2)处理文件句柄就成为调用者必须作的事情。
注:ET模式在很大程度上减少了epoll事件被重复触发的次数,因此效率要比LT模式高。epoll工作在ET模式的时候,必须使用
非阻塞套接口,以避免由于一个文件句柄的阻塞读/阻塞写操作把处理多个文件描述符的任务饿死。
LT(level triggered)是epoll缺省的⼯工作⽅方式,并且同时⽀支持block和no-block socket.在这种做法中,内核告诉你⼀一个⽂
文件描述符是否就绪了,然后你可以对这个就绪的fd进⾏行IO操作。如果你不作任何操作,内核还是会继续通知你 的,所以,
这种模式编程出错误可能性要⼩小一点。传统的select/poll都是这种模型的代表。
ET (edge-triggered)是⾼高速⼯工作⽅方式,只⽀支持no-block socket,它效率要⽐比LT更⾼高。ET与LT的区别在于,当一个
新的事件到来时,ET模式下当然可以从epoll_wait调⽤用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲
区处理完,在这个套接字中没有新的事件再次到来时,在ET模式下是⽆无法再次从epoll_wait调⽤用中获取这个事件的。⽽而
LT模式正好相反,只要⼀一个事件对应的套接字缓冲区还有数据,就总能从epoll_wait中获取这个事件。因此,LT模式下开发
基于epoll的应⽤用要简单些,不太容易出错。⽽而在ET模式下事件发⽣生时,如果没有彻底地将缓冲区数据处理完,则会导
致缓冲区中的⽤用户请求得不到响应。Nginx默认采⽤用ET模式来使⽤用epoll。
四、epoll实现服务器
#include<stdio.h>
#include<sys/epoll.h>
#include<sys/types.h>
#include<sys/socket.h>
#include<netinet/in.h>
#include<arpa/inet.h>
#include<stdlib.h>
#include<string.h>
static void usage(const char *proc)
{printf("usage:%s [local_ip] [local_port]",proc);
}typedef struct fd_buf
{int fd;char buf[10240];
}fd_buf_t,*fd_buf_p;static void *alloc_fd_buf(int fd)
{fd_buf_p tmp=(fd_buf_p)malloc(sizeof(fd_buf_t));if(!tmp){perror("malloc");return NULL;}tmp->fd=fd;return tmp;
}int startup(const char *_ip,const int _port)
{int sock=socket(AF_INET,SOCK_STREAM,0);if(sock<0){perror("socket");return 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");return 3;}if(listen(sock,10)<0){perror("listen");return 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){printf("epoll_create");close(listen_sock);return 5;}struct epoll_event ev;ev.events=EPOLLIN;ev.data.ptr=alloc_fd_buf(listen_sock);epoll_ctl(epfd,EPOLL_CTL_ADD,listen_sock,&ev);int num=0;struct epoll_event evs[64];int timeout=-1;while(1){switch((num=epoll_wait(epfd,evs,64,timeout))){//等待失败 case -1:perror("epoll_wait");break;//超时case 0:perror("timeout...");break;//等待成功default:{int i=0;for(;i<num;i++){fd_buf_p fp=(fd_buf_p)evs[i].data.ptr;if(fp->fd==listen_sock && \(evs[i].events & EPOLLIN)){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 a new client\n");ev.events=EPOLLIN;ev.data.ptr=alloc_fd_buf(new_sock);epoll_ctl(epfd,EPOLL_CTL_ADD,\new_sock,&ev);}else if(fp->fd!=listen_sock){//读事件if(evs[i].events & EPOLLIN){ssize_t s=read(fp->fd,fp->buf,sizeof(fp->buf));if(s>0){fp->buf[s]=0;printf("client say:%s\n",fp->buf);ev.events=EPOLLOUT;ev.data.ptr=fp;epoll_ctl(epfd,EPOLL_CTL_MOD,fp->fd,&ev);}else if(s<=0){close(fp->fd);epoll_ctl(epfd,EPOLL_CTL_DEL,fp->fd,NULL);free(fp);}else{}}//写事件else if(evs[i].events & EPOLLOUT){const char *msg="HTTP/1.0 200 OK\r\n\r\n<html><h1>hello epoll</h1></html>";write(fp->fd,msg,strlen(msg));close(fp->fd);epoll_ctl(epfd,EPOLL_CTL_DEL,fp->fd,NULL);free(fp);}else{}}else{}}}break;}}return 0;
}
五、epoll与select、poll比较
1 支持一个进程所能打开的最大连接数
select 单个进程所能打开的最大连接数有FD_SETSIZE宏定义,其大小是32个整数的大小(在32位的机器上,大小就是32*32,同理64位机器上 FD_SETSIZE为32*64),当然我们可以对它进行修改,然后重新编译内核,但是性能可能会受到影响,这需要进一步的测试。 poll poll本质上和select没有区别,但是它没有最大连接数的限制,原因是它是基于链表来存储的 epoll
虽然连接数有上限,但是很大,1G内存的机器上可以打开10万左右的连接,2G内存的机器可以打开20万左右的连接。
2 FD剧增后带来的IO效率问题
select
因为每次调用时都会对连接进行线性遍历,所以随着FD的增加会造成遍历速度慢的“线性下降性能问题”。 poll 同上 epoll 因为epoll内核中实现是根据每个fd上的callback函数来实现的,只有活跃的socket才会主动调用callback,所以在活跃socket较少的情况下,使用epoll没有前面两者的线性下降的性能问题,但是所有socket都很活跃的情况下,可能会有性能问题。
3 消息传递方式
select 内核需要将消息传递到用户空间,都需要内核拷贝动作 poll 同上 epoll epoll通过内核和用户空间共享一块内存来实现的
综上,在选择select,poll,epoll时要根据具体的使用场合以及这三种方式的自身特点。表面上看epoll的性能最好,但是在连接数少并且连接都十
分活跃的情况下,select和poll的性能可能比epoll好,毕竟epoll的通知机制需要很多函数回调。