• Redis系列(三)事件处理细节分析及epoll介绍


      上两篇介绍了redis的启动流程接受客户端请求到调用请求处理函数,在这篇里,我将介绍redis事件触发细节,即epoll介绍。从redis源码可以看出,redis的io模型主要是基于epoll实现的,不过它也提供了 select和kqueue的实现,默认采用epoll。

    ae.c

    #ifdef HAVE_EPOLL
    #include "ae_epoll.c"
    #else
        #ifdef HAVE_KQUEUE
        #include "ae_kqueue.c"
        #else
        #include "ae_select.c"
        #endif
    #endif

    通过这么一个条件包含,就可以决定redis使用哪种i/o多路复用函数。同时redis通过ae.h的一系列声明为上层提供了一个统一的接口,以此隐藏底层io多路函数的具体实现。
    有关unix io模型的类型,可参考(Unix 五种基本I/O模型的区别) .

    那么epoll到底是个什么东西呢? 其实只是众多i/o多路复用技术当中的一种而已,但是相比其他io多路复用技术(select, poll等等),epoll有诸多优点:

      1. epoll 没有最大并发连接的限制,上限是最大可以打开文件的数目,这个数字一般远大于 2048, 一般来说这个数目和系统内存关系很大  ,具体数目可以 cat /proc/sys/fs/file-max 察看。

      2. 效率提升, Epoll 最大的优点就在于它只管你“活跃”的连接 ,而跟连接总数无关,因此在实际的网络环境中, Epoll 的效率就会远远高于 select 和 poll 。

      3. 内存拷贝, Epoll 在这点上使用了“共享内存 ”,这个内存拷贝也省略了。

    那么在我们的系统中,到底应该如何使用epoll呢? 这里,epoll给我们提供了3个api: epoll_create, epoll_ctl, epoll_wait。

    1: int epoll_create(int size);

    生成一个 epoll 专用的文件描述符,其实是申请一个内核空间,用来存放你想关注的 socket fd 上是否发生以及发生了什么事件。 size 就是你在这个 epoll fd 上能关注的最大 socket fd 数,大小自定,只要内存足够。

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

    控制某个 epoll 文件描述符上的事件:注册、修改、删除。参数说明:

       epfd 是 epoll_create() 创建 epoll 专用的文件描述符。相对于 select 模型中的 FD_SET 和 FD_CLR 宏;

      op就是你要把当前这个套接口fd如何设置到epfd上边去,一般由epoll提供的三个宏指定:EPOLL_CTL_ADD,EPOLL_CTL_DEL,EPOLL_CTL_MOD。

      fd: 当事件发生时操作的目标套接口。

      event指针就是你要给这个套接口fd绑定什么事件。

    3: int epoll_wait(int epfd,struct epoll_event * events,int maxevents,int timeout);

    等待 I/O 事件的发生;参数说明:

    epfd: 由 epoll_create() 生成的 Epoll 专用的文件描述符;

    epoll_event: 用于回传代处理事件的数组;

    maxevents: 返回的最大事件数;

    timeout: 等待 I/O 事件发生的超时值(毫秒);

    epoll_wait返回触发的事件数。

     

    下面看一个例子:

     

      

    kdpfd = epoll_create(1024);
    epoll_event lev;
    lev.events = EPOLLIN;
    epoll_ctl(kdpfd, EPOLL_CTL_ADD, listener,  &lev);
    struct epoll_event ev, *events;
    for(;;) {
        nfds = epoll_wait(kdpfd, events, maxevents, -1);
        for(n = 0; n < nfds; ++n) {
            if(events[n].data.fd == listener) {
                client = accept(listener, (struct sockaddr *) &local,
                                &addrlen);
                if(client < 0){
                    perror("accept");
                    continue;
                }
                setnonblocking(client);
                ev.events = EPOLLIN | EPOLLET;
                ev.data.fd = client;
                if (epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev) < 0) {
                    fprintf(stderr, "epoll set insertion error: fd=%d0,
                            client);
                    return -1;
                }
            }
            else
                do_use_fd(events[n].data.fd);
        }
    }

     

      首先,通过epoll_create创建一个epoll实例, 然后声明一个epoll_event lev(这是一个struct,epoll用它来代表事件), 并将该lev的events赋值为EPOLLIN(这样当listener上有数据可读时,那么epoll_wait便会返回该fd), 最后再调用epoll_wait 等待 kdpfd这个epoll实例上事件的发生。当有事件发生(io读写事件)或者到达设定的超时值,那么epoll_wait就会返回,然后我们就可以通过 events拿到相应的socketfd并进行相应的处理。

      例子中是当给listener绑定的可读事件发生时(客户端连接到达),那么就调用accept函数,获取客户端与服务器段的套接字client , 然后给这个套接字绑定 ev.events = EPOLLIN | EPOLLET; 并调用 epoll_ctl函数将该套接字client 加入到epoll实例kdpfd,再次循环进行epoll_wait, 这样,当client有数据可读时(客户端请求数据到达),那么就可以进行下一步处理了,如调用recv/read接受客户端数据,等等。

      epoll_ctl(kdpfd, EPOLL_CTL_ADD, client, &ev)

      从上边的介绍中,我们知道了如何调用epoll提供的api, 生成epoll实例,如何给套接口设置相应事件,如何将套接口添加到epoll实例以及进行事件轮询(epoll_wait)等待相应事件的发生并处理, 再来看redis代码, 就可以对redis接受客户端请求并处理的过程一目了然了。

      

    如图所示,如果监听的端口有连接到来,那么epoll_wait返回,那么redis会把触发的套接口放到eventLoop.fired这个数组里:

     1  retval = epoll_wait(state->epfd,state->events,AE_SETSIZE,
     2             tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
     3     if (retval > 0) {
     4         int j;
     5 
     6         numevents = retval;
     7         for (j = 0; j < numevents; j++) {
     8             int mask = 0;
     9             struct epoll_event *e = state->events+j;
    10 
    11             if (e->events & EPOLLIN) mask |= AE_READABLE;
    12             if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
    13             if (e->events & EPOLLERR) mask |= AE_WRITABLE;
    14             if (e->events & EPOLLHUP) mask |= AE_WRITABLE;
    15             eventLoop->fired[j].fd = e->data.fd;
    16             eventLoop->fired[j].mask = mask;
    17         }
    18     }

     

      然后在aeProcessEvents这个函数里,会取出eventLoop.fired中的fd,并取出对应的事件:aeFileEvent *fe, 然后判断事件的类型,调用相应的处理函数:

      

     if (fe->mask & mask & AE_READABLE) {
                    rfired = 1;
                    fe->rfileProc(eventLoop,fd,fe->clientData,mask);
                }
                if (fe->mask & mask & AE_WRITABLE) {
                    if (!rfired || fe->wfileProc != fe->rfileProc)
                        fe->wfileProc(eventLoop,fd,fe->clientData,mask);
                }

    至此,redis的启动流程,接受客户端请求到调用请求处理函数,以及事件如何触发,如何调用处理函数,在三篇博客里都做了详细的分析。相信结合这三篇博客可以对redis内部的实现有一个较为深刻的理解。

     

     

  • 相关阅读:
    Easyui datagrid行内【添加】、【编辑】、【上移】、【下移】
    使用dynamic linq 解决自定义查询的若干弊端
    用CS的思维可以指导BS的项目吗?
    那些让 Web 开发者们深感意外的事情
    asp.net实现access数据库分页
    那些相见恨晚的 JavaScript 技巧
    Javascript的执行过程详细研究
    asp.net 遍历文件夹下全部子文件夹并绑定到gridview上
    无废话JavaScript(下)
    asp.net DataTable导出 excel的方法记录(第三方)
  • 原文地址:https://www.cnblogs.com/yuxingfirst/p/2776012.html
Copyright © 2020-2023  润新知