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