I/O多路复用详解(三)

来源:互联网 发布:网络女主播不雅视频 编辑:程序博客网 时间:2024/06/05 00:50

4、epoll 

    在linux的网络编程中,很长的一段时间都在使用select来做事件触发。然而select逐渐暴露出了一些缺陷,使得linux不得不在新的内核中寻找出替代方案,那就是epoll。其实,epoll与select原理类似,只不过,epoll作出了一些重大改进,即:

      a、当它们所监听的集合中有状态发生改变时,select需要循环检查整个集合,才能确定那个文件描述符状态发生改变,进而进行操作;而epoll在添加文件描述符到集合时,已经绑定了该文件描述符的对应函数,因此,当该文件描述符状态改变时,不需要循环查询整个集合,因而将复杂度由0(n)将为o(1),性能得到几何量级的提高,尤其是在大量连接的情况下。

       b、再有,select所监听的描述字最大数目是有一定限制的,由FD_SETSIZE设置,通常是1024。对于那些需要支持的上万连接数目的web服务器来说显然是太少了,尽管可以通过修改头文件再重编译内核来扩大这个数目,不过资料也同时指出这样会带来网络效率的下降。(详细请参考:epoll 相对于poll的优点)

epoll的接口非常简单,一共就三个函数:

(1)、int epoll_create(int size);

     创建一个epoll的句柄,size用来告诉内核这个监听的数目一共有多大。需要注意的是,当创建好epoll句柄后,epoll本身就占用一个fd值,所以用完后必须调用close()关闭,以防止fd被耗尽。


(2)、int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);

     epoll的事件注册函数,第一个参数是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队列里。

(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表示已超时。

      令人高兴的是,2.6内核的epoll比其2.5开发版本的/dev/epoll简洁了许多,所以,大部分情况下,强大的东西往往是简单的。唯一有点麻烦是epoll有2种工作方式:LT和ET。

       LT(level triggered)是缺省的工作方式,并且同时支持block和no-blocksocket.在这种做法中,内核告诉你一个文件描述符是否就绪了,然后你可以对这个就绪的fd进行IO操作。如果你不作任何操作,内核还是会继续通知你的,所以,这种模式编程出错误可能性要小一点。传统的select/poll都是这种模型的代表.

       ET (edge-triggered)是高速工作方式,只支持no-block socket。在这种模式下,当描述符从未就绪变为就绪时,内核通过epoll告诉你。然后它会假设你知道文件描述符已经就绪,并且不会再为那个文件描述符发送更多的就绪通知,直到你做了某些操作导致那个文件描述符不再为就绪状态了(比如,你在发送,接收或者接收请求,或者发送接收的数据少于一定量时导致了一个EWOULDBLOCK 错误)。但是请注意,如果一直不对这个fd作IO操作(从而导致它再次变成未就绪),内核不会发送更多的通知(only once),不过在TCP协议中,ET模式的加速效用仍需要更多的benchmark确认。

注:以上参考epoll精髓一文。

一个epoll的例子:

#include <stdio.h>#include <stdlib.h>#include <errno.h>#include <string.h>#include <sys/types.h>#include <netinet/in.h>#include <sys/socket.h>#include <sys/wait.h>#include <unistd.h>#include <arpa/inet.h>#include <openssl/ssl.h>#include <openssl/err.h>#include <fcntl.h>#include <sys/epoll.h>#include <sys/time.h>#include <sys/resource.h>  #define MAXBUF 1024#define MAXEPOLLSIZE 10000 /*setnonblocking - 设置句柄为非阻塞方式*/int setnonblocking(int sockfd){    if (fcntl(sockfd, F_SETFL, fcntl(sockfd, F_GETFD, 0)|O_NONBLOCK) == -1) {        return -1;    }    return 0;} /*handle_message - 处理每个 socket 上的消息收发*/int handle_message(int new_fd){    char buf[MAXBUF + 1];    int len;    /* 开始处理每个新连接上的数据收发 */    bzero(buf, MAXBUF + 1);    /* 接收客户端的消息 */    len = recv(new_fd, buf, MAXBUF, 0);    if (len > 0)        printf            ("%d接收消息成功:'%s',共%d个字节的数据\n",             new_fd, buf, len);    else {        if (len < 0)            printf                ("消息接收失败!错误代码是%d,错误信息是'%s'\n",                 errno, strerror(errno));        close(new_fd);        return -1;    }    /* 处理每个新连接上的数据收发结束 */    return len;}/************关于本文档*********************************************filename: epoll-server.c*purpose: 演示epoll处理海量socket连接的方法*wrote by: zhoulifa(zhoulifa@163.com) 周立发(http://zhoulifa.bokee.com)Linux爱好者 Linux知识传播者 SOHO族 开发者 最擅长C语言*date time:2007-01-31 21:00*Note: 任何人可以任意复制代码并运用这些文档,当然包括你的商业用途* 但请遵循GPL*Thanks to:Google*Hope:希望越来越多的人贡献自己的力量,为科学技术发展出力* 科技站在巨人的肩膀上进步更快!感谢有开源前辈的贡献!*********************************************************************/int main(int argc, char **argv){    int listener, new_fd, kdpfd, nfds, n, ret, curfds;    socklen_t len;    struct sockaddr_in my_addr, their_addr;    unsigned int myport, lisnum;    struct epoll_event ev;    struct epoll_event events[MAXEPOLLSIZE];    struct rlimit rt;     if (argv[1])        myport = atoi(argv[1]);    else        myport = 7838;     if (argv[2])        lisnum = atoi(argv[2]);    else        lisnum = 2;     /* 设置每个进程允许打开的最大文件数 */    rt.rlim_max = rt.rlim_cur = MAXEPOLLSIZE;    if (setrlimit(RLIMIT_NOFILE, &rt) == -1) {        perror("setrlimit");        exit(1);    }    else printf("设置系统资源参数成功!\n");     /* 开启 socket 监听 */    if ((listener = socket(PF_INET, SOCK_STREAM, 0)) == -1) {        perror("socket");        exit(1);    } else        printf("socket 创建成功!\n");     setnonblocking(listener);     bzero(&my_addr, sizeof(my_addr));    my_addr.sin_family = PF_INET;    my_addr.sin_port = htons(myport);    if (argv[3])        my_addr.sin_addr.s_addr = inet_addr(argv[3]);    else        my_addr.sin_addr.s_addr = INADDR_ANY;     if (bind        (listener, (struct sockaddr *) &my_addr, sizeof(struct sockaddr))        == -1) {        perror("bind");        exit(1);    } else        printf("IP 地址和端口绑定成功\n");     if (listen(listener, lisnum) == -1) {        perror("listen");        exit(1);    } else        printf("开启服务成功!\n");     /* 创建 epoll 句柄,把监听 socket 加入到 epoll 集合里 */    kdpfd = epoll_create(MAXEPOLLSIZE);    len = sizeof(struct sockaddr_in);    ev.events = EPOLLIN | EPOLLET;    ev.data.fd = listener;    if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, listener, &ev) < 0) {        fprintf(stderr, "epoll set insertion error: fd=%d\n", listener);        return -1;    } else        printf("监听 socket 加入 epoll 成功!\n");    curfds = 1;    while (1) {        /* 等待有事件发生 */        nfds = epoll_wait(kdpfd, events, curfds, -1);        if (nfds == -1) {            perror("epoll_wait");            break;        }        /* 处理所有事件 */        for (n = 0; n < nfds; ++n) {            if (events[n].data.fd == listener) {                new_fd = accept(listener, (struct sockaddr *) &their_addr,                                &len);                if (new_fd < 0) {                    perror("accept");                    continue;                } else                    printf("有连接来自于: %s:%d, 分配的 socket 为:%d\n", inet_ntoa(their_addr.sin_addr), ntohs(their_addr.sin_port), new_fd);                 setnonblocking(new_fd);                ev.events = EPOLLIN | EPOLLET;                ev.data.fd = new_fd;                if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, new_fd, &ev) < 0) {                    fprintf(stderr, "把 socket '%d' 加入 epoll 失败!%s\n",                            new_fd, strerror(errno));                    return -1;                }                curfds++;            } else {                ret = handle_message(events[n].data.fd);                if (ret < 1 && errno != 11) {                    epoll_ctl(kdpfd, EPOLL_CTL_DEL, events[n].data.fd,                              &ev);                    curfds--;                }            }        }    }    close(listener);    return 0;}


?

编译此程序用命令:

gcc -Wall epoll-server.c -o server

运行此程序需要具有管理员权限!

sudo ./server 7838 1

通过测试这一个服务器可能同时处理10000 -3 = 9997 个连接!

如果这是一个在线服务系统,那么它可以支持9997人同时在线,比如游戏、聊天等。

 

参考网址:http://zhoulifa.bokee.com/6081520.html

0 0
原创粉丝点击