Linux--高级I/O多路复用之epoll

来源:互联网 发布:最新韩国网络剧2017 编辑:程序博客网 时间:2024/06/04 18:21

epoll

epoll是Linux内核为处理大批量文件描述符而作了改进的poll,是Linux下多路复用IO接口select/poll的增强版本,它能显著提高程序在大量并发连接中只有少量活跃的情况下的系统CPU利用率。
但epoll需要使用一个额外的文件描述符,来唯一标识内核中的这个事件表。

epoll由三个系统调用组成,分别是epoll_create,epoll_ctl和epoll_wait。
epoll_create用于创建和初始化一些内部使用的数据结构;
epoll_ctl用于添加,删除或者修改指定的fd及其期待的事件;
epoll_wait就是用于等待任何先前指定的fd事件。

1.epoll_create函数:创建文件描述符

 #include <sys/epoll.h> int epoll_create(int size);

size参数现在并不起作用,只是给内核一个提示,告诉它事件表需要多大。该函数返回的文件描述符将作用其他所有epoll系统调用的第一个参数,以指定要访问的内核事件表。

2.epoll_ctl函数:用来操作内核事件表–增删改

 #include <sys/epoll.h> int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

epoll的事件注册函数,它不同于select函数是在监听事件时告诉内核要监听什么类型的事件,而是在这里先注册要监听的事件类型。
参数:
epfd:要操作的事件表的文件描述符,即epoll_create()的返回值。
op:表示动作,指定要操作的类型,用三个宏来表示:

EPOLL_CTL_ADD:往事件表eptd中注册fd上的事件
EPOLL_CTL_MOD:修改已经注册的fd的监听事件
EPOLL_CTL_DEL:从epfd中删除一个fd

fd:需要监听的fd
event:告诉内核需要监听什么事。指定事件,它是epoll_event结构类型的指针。
这里写图片描述
events成员描述事件类型,可以是以下几个宏的集合:
EPOLLIN :表示对应的文件描述符可以读(包括对端SOCKET正常关闭);
EPOLLOUT:表示对应的文件描述符可以写;
EPOLLPRI:表示对应的文件描述符有紧急的数据可读(这⾥里应该表⽰示有带外数据到来);
EPOLLERR:表示对应的文件描述符发生错误;
EPOLLHUP:表示对应的文件描述符被挂断;
EPOLLET: 将EPOLL设为边缘触发(Edge Triggered)模式,这是相对于水平触发(LevelTriggered)来说的。
EPOLLONESHOT:只监听一次事件,当监听完这次事件之后,如果还需要继续监听这个socket的话,需要再次把这个socket加入到EPOLL队列里。

返回值:成功返回0,失败返回-1并设置error。

3.epoll_wait函数:在一段超时时间内等待一组文件描述符上的事件

  #include <sys/epoll.h>    int epoll_wait(int epfd, struct epoll_event *events,int maxevents, int timeout);

返回值:该函数成功时返回就绪的文件描述符的个数,失败时返回-1并设置errno。

epoll的实现模式:

1、LT模式:从无到有一直“打电话”通知(level triggered)–水平触发。LT(level triggered)是epoll缺省的工作方式,并且同时支持block和no-block socket。 在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的。故相比ET 模式,数据不会丢失,epoll又称为大号的poll。
2、ET模式:只有从无到有时才打电话通知 (edge-triggered)–边缘触发。 ET模式是高速工作方式,只支持no-block socket,它效率要比LT更高。

ET与LT的区别在于,当一个新的事件到来时,ET模式下当然可以从epoll_wait调用中获取到这个事件,可是如果这次没有把这个事件对应的套接字缓冲区处理完,在这个套接字中没有新的事件再次到来时,在ET模式下是无法再次从epoll_wait调用中获取这个事件的。

epoll的优点

优点一:支持一个进程打开大数目的socket描述符
select 最不能忍受的是一个进程所打开的fd是有一定限制的,由FD_SETSIZE设置,默认值是2048。对于那些需要支持的上万连接数目的IM服务器来说显然太少了。这时候你一是可以选择修改这个宏然后重新编译内核,不过资料也同时指出这样会带来网络效率的下降,二是可以选择多进程的解决方案(传统的 Apache方案),不过虽然linux上面创建进程的代价比较小,但仍旧是不可忽视的,加上进程间数据同步远比不上线程间同步的高效,所以也不是一种完美的案。不过 epoll则没有这个限制,它所支持的FD上限是最大可以打开文件的数目,这个数字一般远远于2048,举个例子,在1GB内存的机器上大约是10万左右,具体数目可以cat /proc/sys/fs/file-max察看,一般来说这个数目和系统内存关系很大。

优点二:IO效率不随FD数目增加而线性下降
传统的select/poll另一个致命弱点就是当你拥有一个很大的socket集合,不过由于网络延时,任一时间只有部分的socket是”活跃”的,但是select/poll每次调用都会线性扫描全部的集合,导致效率呈现线性下降。但是epoll不存在这个问题,它只会对”活跃”的socket进行操作—这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的。那么,只有”活跃”的socket才会主动的去调用 callback函数,其他idle状态socket则不会,在这点上,epoll实现了一个”伪”AIO,因为这时候推动是在os内核。在一些 benchmark中,如果所有的socket基本上都是活跃的—比如一个高速LAN环境,epoll并不比select/poll有什么效率,相反,如果过多使用epoll_ctl,效率相比还有稍微的下降。但是一旦使用idle connections模拟WAN环境,epoll的效率就远在select/poll之上了

优点三:使用mmap加速内核与用户空间的消息传递
这点实际上涉及到epoll的具体实现了。无论是select,poll还是epoll都需要内核把FD消息通知给用户空间,如何避免不必要的内存拷贝就很重要,在这点上,epoll是通过内核与用户空间mmap同一块内存实现的。而如果你想像我一样从2.5内核就关注epoll的话,一定不会忘记手工mmap这一步的。(mmap底层是使用红黑树加队列实现的,每次需要在操作的fd,先在红黑树中拿到,放到队列中,那么用户收到epoll_wait消息以后只需要看一下消息队列中有没有数据,有我就取走)

优点四:内核微调
这一点其实不算epoll的优点了,而是整个linux平台的优点。也许你可以怀疑linux平台,但是你无法回避linux平台赋予你微调内核的能力。比如,内核TCP/IP协议栈使用内存池管理sk_buff结构,那么可以在运行时期动态调整这个内存pool(skb_head_pool)的大小– 通过echoXXXX>/proc/sys/net/core/hot_list_length完成。再比如listen函数的第2个参数(TCP完成3次握手的数据包队列长度),也可以根据你平台内存大小动态调整。更甚至在一个数据包里面数据巨大但同时每个数据包本本身大小却很小的特殊系统上尝试最新的NAPI网卡驱动架构。

下面使用epoll,完成简单http消息回显,并使用浏览器测试。

#include<stdio.h>#include<stdlib.h>#include<sys/types.h>#include<sys/socket.h>#include<arpa/inet.h>#include<netinet/in.h>#include<string.h>#include<sys/stat.h>#include<fcntl.h>#include<sys/epoll.h>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,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,10) < 0){        perror("listen");        return 4;    }    return sock;}int main(int argc,char * argv[]){    if(argc != 3)    {        printf("usage: %s ip port\n",argv[0]);        return 1;    }    int listen_sock = startup(argv[1],atoi(argv[2]));    int epollfd = epoll_create(256);    if(epollfd < 0){        perror("epoll_create");        close(listen_sock);        return 5;    }    struct epoll_event ev;    ev.events = EPOLLIN;    ev.data.ptr = alloc_fd_buf(listen_sock);    epoll_ctl(epollfd,EPOLL_CTL_ADD,listen_sock,&ev);    int num = 0;    struct epoll_event evs[64];    int timeout = 1000;    while(1){        switch(num = epoll_wait(epollfd,evs,64,timeout)){            case -1:                perror("epoll_wait");                break;            case 0:                printf("timeout\n");                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:%s %d\n",inet_ntoa(client.sin_addr),ntohs(client.sin_port));            ev.events = EPOLLIN;            ev.data.ptr = alloc_fd_buf(new_sock);            epoll_ctl(epollfd,EPOLL_CTL_ADD,new_sock,&ev);        }//if        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:%s",fp->buf);                    ev.events = EPOLLOUT;                    ev.data.ptr = fp;                    epoll_ctl(epollfd,EPOLL_CTL_MOD,fp->fd,&ev);                }else if(s ==0){                    printf("client close\n");                    close(fp->fd);                    epoll_ctl(epollfd,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(epollfd,EPOLL_CTL_DEL,fp->fd,NULL);                free(fp);            }else{            }        }//else if(fp->fd)        else{}        }            break;        }//switch    }//while}}

这里写图片描述