用Bollger记录技术之路的点滴...

关注高性能linux网络编程,NoSQL, c/c++/java ~~~ weibo @语_行 http://weibo.com/201281062~~~ twitter @JerryVector https://twitter.com/JerryVector
  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

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

Posted on 2012-11-18 16:25  语行  阅读(9564)  评论(0编辑  收藏  举报

  上两篇介绍了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内部的实现有一个较为深刻的理解。