Redis中的事件循环是他的ae模块(advance eventloop?),这是个简单的事件循环模块,自身实现了事件循环框架和时间事件逻辑,而具体事件处理则根据不同的系统编译不同的模块。ae主要由这几个模块组成:
- ae.c
- ae.h
- ae_epoll.c
- ae_evport.c
- ae_kqueue.c
- ae_select.c
可以看出来,ae实现了epoll,evport,kqueue和select模型几种,需要设置参数来配置具体的模块,关于这几种事件模型原理这里就不细说了,本文仅以epoll来说。
/* Include the best multiplexing layer supported by this system.
- The following should be ordered by performances, descending. */
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
#endif
所有epoll事件均是以数组元素的形式添加到数组中,fd作为数组索引,整个事件循环框架会维护两个数组:注册事件数组和触发事件数组,在poll时会把注册事件中完成的事件添加到触发事件数组中。而时间事件则是一个单向链表:
/* State of an event based program */
typedef struct aeEventLoop {
...
//注册的可以被epoll触发的事件数组
aeFileEvent *events; /* Registered events */
//已经触发的epoll事件数组
aeFiredEvent *fired; /* Fired events */
//时间事件链表,利用epoll超时时间来实现
aeTimeEvent *timeEventHead;
...
} aeEventLoop;
typedef struct aeApiState {
int epfd;
//epoll内部数组,用于接收以触发事件数组
struct epoll_event *events;
} aeApiState;
这样在添加新的事件时就需要把fd,属性,回调以及回调参数等分别添加到这些数组中。
aeFileEvent *fe = &eventLoop->events[fd];
//添加到epoll
if (aeApiAddEvent(eventLoop, fd, mask) == -1)
return AE_ERR;
fe->mask |= mask;
if (mask & AE_READABLE) fe->rfileProc = proc;
if (mask & AE_WRITABLE) fe->wfileProc = proc;
fe->clientData = clientData;
//设置最大fd
if (fd > eventLoop->maxfd)
eventLoop->maxfd = fd;
而对于时间事件,则只是简单的添加到时间队列中:
aeTimeEvent *te;
te = zmalloc(sizeof(*te));
...
te->next = eventLoop->timeEventHead;
eventLoop->timeEventHead = te;
ae框架中还有两个回调,一个是在一次事件处理前执行,一个是在事件处理后执行,这样用户就有足够的自由度来处理一些自己的事情。:
/* State of an event based program */
typedef struct aeEventLoop {
......
aeBeforeSleepProc *beforesleep;
aeBeforeSleepProc *aftersleep;
} aeEventLoop;
在Redis中,预处理回调(beforesleep)会在每次进入事件前:
- 检查是否停止事件循环。
- 快速的检查并清理超时键值对。
- 向从服务器发送ACK请求。
- 处理上一轮循环中阻塞的请求。
- 写入aof文件。
- 将发送给client的缓冲区发送掉。
完成这些后,ae将进入下一轮事件循环。
eventLoop->stop = 0;
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
}
处理循环时,首先会在时间事件中找到离现在(now)的最近的待触发时间事件,并将到这个事件所需的时间设置为epoll的超时时间,这样epoll一旦超时,意味着刚刚找到的时间事件需要处理了。
aeTimeEvent *shortest = NULL;
struct timeval tv, *tvp;
//找到最近的时间事件
if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
shortest = aeSearchNearestTimer(eventLoop);
if (shortest) {
long now_sec, now_ms;
aeGetTime(&now_sec, &now_ms);
tvp = &tv;
//计算出和现在的差值并作为超时时间
/* How many milliseconds we need to wait for the next
* time event to fire? */
long long ms =
(shortest->when_sec - now_sec)*1000 +
shortest->when_ms - now_ms;
//如果差值小于0,那应该有个时间事件已经过了,应该赶紧处理
if (ms > 0) {
tvp->tv_sec = ms/1000;
tvp->tv_usec = (ms % 1000)*1000;
} else {
tvp->tv_sec = 0;
tvp->tv_usec = 0;
}
//没找到时间事件
} else {
/* If we have to check for events but need to return
* ASAP because of AE_DONT_WAIT we need to set the timeout
* to zero */
//如果需要一直阻塞就直接设置NULL,否则超时时间0
if (flags & AE_DONT_WAIT) {
tv.tv_sec = tv.tv_usec = 0;
tvp = &tv;
} else {
/* Otherwise we can block */
tvp = NULL; /* wait forever */
}
}
//执行poll
/* Call the multiplexing API, will return only on timeout or when
* some event fires. */
numevents = aeApiPoll(eventLoop, tvp);
aeApiPoll会把触发的事件放到触发数组(fired)中:
retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
if (retval > 0) {
int j;
numevents = retval;
for (j = 0; j < numevents; j++) {
int mask = 0;
struct epoll_event *e = state->events+j;
......
eventLoop->fired[j].fd = e->data.fd;
eventLoop->fired[j].mask = mask;
}
}
然后框架依次去执行触发数组的回调函数:
for (j = 0; j < numevents; j++) {
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
int mask = eventLoop->fired[j].mask;
int fd = eventLoop->fired[j].fd;
int rfired = 0;
/* note the fe->mask & mask & ... code: maybe an already processed
* event removed an element that fired and we still didn't
* processed, so we check if the event is still valid. */
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);
}
processed++;
}
最后会处理一下时间事件链表,其实就是检查一下这个链表,如果有需要删除的就触发回收回调并删除,如果有时间到的那就触发时间回调:
while(te) {
long now_sec, now_ms;
long long id;
//这应该是上一轮标记需要删除的时间事件,现在删除并触发回收回调
/* Remove events scheduled for deletion. */
if (te->id == AE_DELETED_EVENT_ID) {
aeTimeEvent *next = te->next;
if (prev == NULL)
eventLoop->timeEventHead = te->next;
else
prev->next = te->next;
if (te->finalizerProc)
te->finalizerProc(eventLoop, te->clientData);
zfree(te);
te = next;
continue;
}
/* Make sure we don't process time events created by time events in
* this iteration. Note that this check is currently useless: we always
* add new timers on the head, however if we change the implementation
* detail, this check may be useful again: we keep it here for future
* defense. */
if (te->id > maxId) {
te = te->next;
continue;
}
//如果事件已到,就触发时间回调,如果时间回调返回AE_NOMORE,那就标记需要删除这个时间事件。
aeGetTime(&now_sec, &now_ms);
//这里还是有可能超过设定时间的,这种也需要触发
if (now_sec > te->when_sec ||
(now_sec == te->when_sec && now_ms >= te->when_ms))
{
int retval;
id = te->id;
retval = te->timeProc(eventLoop, id, te->clientData);
processed++;
if (retval != AE_NOMORE) {
aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);
} else {
te->id = AE_DELETED_EVENT_ID;
}
}
prev = te;
te = te->next;
}
这种将时间事件作为poll的超时时间的机制可以很好的在一个事件循环中同时添加文件时间和时间事件并兼容多种事件模型。
因为fd是以数组的形式保存的,所以一开始需要分配足够的空间,而相应的触发数组和内部数组也需要相应的大小。同时这种结构也无法向Windows兼容。