介绍
Memcached的底层网络驱动部分直接使用了libevent,其实Redis使用libevent也是可以,仅仅使用其event_base
完全可以行得通。但是作者为什么需要自己造个轮子,可能作者觉得libevent
封装得过于复杂实现的功能过多吧。这里区别一下Redis作者的ae.c
和libevent的区别吧:
- libevent统一了三种事件的处理,IO事件、时间事件和信号事件,并且每个事件可以注册多个回调函数,事件之间具有优先级关系(通过将就绪链表依据优先级设定为多条实现)。而Redis仅仅统一了IO事件和事件事件,且一个事件fd只能注册一个回调函数,并且IO事件之间不具备优先级,按照epoll返回顺序依次执行。因此Redis的封装更加简单明了。
- 二者都是通过Reactor模式封装epoll了,所以实现起来,基本就是对一个大结构体操作。所以很容易实现。
- 再次发现,Redis的作者很喜欢造轮子。
实现
1、事件分发器及IO事件、定时事件结构体
通过三个结构体,直接看清楚这种Reactor模式实现起来多么容易。
- 时间事件通过单链表存储,内部存储距离1970年1月1日00:00:00的秒及微妙。每次时间事件是否需要处理就是通过获取当前时间,与存储的时间简单比较而已。因为先处理IO事件,所以加上处理IO事件的时间,就会导致时间事件的处理稍微推迟。于是通常IO时间回调函数尽量短。这样就可以通过单线程实现高并发操作,牛逼了。
- IO事件是通过数组存储的,存储在数组中的index=fd。于是当事件fd很大,作者就直接使用realloc扩容,简单粗暴高效。而libevent是使用双向链表存储,相对复杂一点。
- 通过以上,再去分析就很容易了。
/*
Ractor模式结构体,管理全部的事件以及epoll函数的状态信息数据
*/
typedef struct aeEventLoop {
int maxfd; /*已经注册的最大文件描述符 highest file descriptor currently registered */
int setsize; /* 文件描述符监听集合大小max number of file descriptors tracked */
long long timeEventNextId;/* 下一个时间事件的ID */
time_t lastTime; /* 最后一次执行事件的时间 ,缓存上次事件Used to detect system clock skew */
/*
牛逼了,直接realloc,但是对于libevent是通过双向链表存储,数组查找快,链表删除快。
*/
aeFileEvent *events; /* 动态分配的数组,保存注册的文件事件,套接字读和写事件,Registered events */
aeFiredEvent *fired; /* 动态分配的数组,就绪的事件,事件就绪,则将其插入就绪列表Fired events */
aeTimeEvent *timeEventHead;/*事件事件,通过单链表连接在一起*/
int stop;/*事件处理的开关*/
void *apidata; /* 多路复用库的事件状态,通常用来保存epoll需要的特定数据This is used for polling API specific data */
aeBeforeSleepProc *beforesleep;/*执行处理事件之前的函数 */
aeBeforeSleepProc *aftersleep;/*执行处理事件之后的函数 */
} aeEventLoop;
/* File event structure
一个IO事件的封装
*/
typedef struct aeFileEvent {
int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) */
aeFileProc *rfileProc;//读文件函数指针
aeFileProc *wfileProc;//写文件函数指针
void *clientData;//指向客户端传动的数据
} aeFileEvent;
/* Time event structure
一个时间事件的封装
*/
typedef struct aeTimeEvent {
long long id; /*事件事件ID time event identifier. */
long when_sec; /* seconds */
long when_ms; /* milliseconds */
aeTimeProc *timeProc;/*事件事件就绪回调函数*/
aeEventFinalizerProc *finalizerProc;/* 事件事件终结回调函数*/
void *clientData;/*用户数据*/
struct aeTimeEvent *next;/*通过链表连接*/
} aeTimeEvent;
/*
对于epoll,aeEventLoop内的apidata指向aeApiState,里面存储了epoll的文件描述符以及epoll_wait返回存储就绪事件结果的数组。
*/
typedef struct aeApiState {//epoll函数本身的文件描述符
int epfd;//epoll对应的文件描述符
struct epoll_event *events;//epoll_wait返回需要使用
} aeApiState;
依据上面的结构体,可以画出下面事件处理框架:
看清楚了上图,再继续讲解API将变得异常简单,所以在设计代码的时候,先选定好结构体,并画出对应的内存模型之后,代码的设计就会变得异常简单。
封装epoll
/*
初始化epoll_create
*/
static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState));//分配需要存储epoll本身信息状态的内存。
if (!state) return -1;
state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);//分配数组,最大事件数目,用于存储epoll的返回值。
if (!state->events) {//每一步都必须判断内存是否分配成功。
zfree(state);
return -1;
}
state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */
if (state->epfd == -1) {
zfree(state->events);
zfree(state);
return -1;
}
eventLoop->apidata = state;//存储到aeEventLoop中
return 0;
}
/*
往epoll里面添加事件,添加fd和事件类型
*/
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee = {0}; /* avoid valgrind warning */
/* If the fd was already monitored for some event, we need a MOD
* operation. Otherwise we need an ADD operation. */
int op = eventLoop->events[fd].mask == AE_NONE ?
EPOLL_CTL_ADD : EPOLL_CTL_MOD;
ee.events = 0;//事件全部采用默认的水平触发。
mask |= eventLoop->events[fd].mask; /* Merge old events */
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;//读或者写
ee.data.fd = fd;//传入fd,就绪的时候也通用返回fd,最后通过fd判断就绪事件。
if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;//添加或者修改
return 0;
}
/*
往epoll中删除事件
*/
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask) {
aeApiState *state = eventLoop->apidata;//获取epoll信息
struct epoll_event ee = {0}; /* avoid valgrind warning,看来作者每个代码都用valgrind检测过的 */
int mask = eventLoop->events[fd].mask & (~delmask);
ee.events = 0;
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.fd = fd;
if (mask != AE_NONE) {//修改事件
epoll_ctl(state->epfd,EPOLL_CTL_MOD,fd,&ee);
} else {
/* Note, Kernel < 2.6.9 requires a non null event pointer even for
* EPOLL_CTL_DEL. */
epoll_ctl(state->epfd,EPOLL_CTL_DEL,fd,&ee);//删除事件
}
}
/*
epoll_wait循环等待就绪事件
*/
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, numevents = 0;
retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,
tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);//结果全部存储在state->events中,空间大小由setsize设定。
if (retval > 0) {//大于0,则有就绪了,retval是就绪个数。
int j;
numevents = retval;
for (j = 0; j < numevents; j++) {//直接遍历就绪事件,将对应的事件加入到就绪事件中。
int mask = 0;
struct epoll_event *e = state->events+j;//访问数组,考察就绪事件
if (e->events & EPOLLIN) mask |= AE_READABLE;//可读
if (e->events & EPOLLOUT) mask |= AE_WRITABLE;//可写
if (e->events & EPOLLERR) mask |= AE_WRITABLE;//
if (e->events & EPOLLHUP) mask |= AE_WRITABLE;
eventLoop->fired[j].fd = e->data.fd;//加入就绪事件的fd
eventLoop->fired[j].mask = mask;//因为什么事件就绪
}
}
return numevents;//返回就绪事件的数量。
}
//存储epoll的状态信息
typedef struct aeApiState {//epoll函数本身的文件描述符
int epfd;//epoll对应的文件描述符
struct epoll_event *events;//epoll_wait返回需要使用
} aeApiState;
/*
扩展存储epoll_wait返回存储结果的内存。
*/
static int aeApiResize(aeEventLoop *eventLoop, int setsize) {
aeApiState *state = eventLoop->apidata;
state->events = zrealloc(state->events, sizeof(struct epoll_event)*setsize);//直接realloc,很平常的哦。
return 0;
}
事件模型
全部代码包含在ae.c
中,也就是将前面的代码再次封装成aeEventLoop
,使得更加好用。下面看看就很清楚。
/*
一些重要的标志信息,通过此信息标记当前事件的几种状态。
*/
#define AE_OK 0
#define AE_ERR -1
#define AE_NONE 0 /* No events registered. */
#define AE_READABLE 1 /* Fire when descriptor is readable. */
#define AE_WRITABLE 2 /* Fire when descriptor is writable. */
#define AE_BARRIER 4 /* With WRITABLE, never fire the event if the
READABLE event already fired in the same event
loop iteration. Useful when you want to persist
things to disk before sending replies, and want
to do that in a group fashion. */
#define AE_FILE_EVENTS 1
#define AE_TIME_EVENTS 2
#define AE_ALL_EVENTS (AE_FILE_EVENTS|AE_TIME_EVENTS)
#define AE_DONT_WAIT 4
#define AE_CALL_AFTER_SLEEP 8
#define AE_NOMORE -1
/*
选择最好的IO复用函数。在Linux平台,必定选择高效率的epoll。
所以直接#include "ae_epoll.c",将c文件里面的static函数全部包含进来。
*/
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL//选定epoll
#include "ae_epoll.c"//直接包含.c文件,再次证明,include仅仅是将文件对应的内容全部包含进来
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
#endif
/*
1、初始化eventLoop结构体,setsize代码可以管理事件的个数,
因为操作系统fd是从小到大依次增加的,所以依据服务器合理设定初始值,
后续可以使用realloc(原始不变,但新增加内存)动态扩大。
*/
aeEventLoop *aeCreateEventLoop(int setsize) {
aeEventLoop *eventLoop;
int i;
if ((eventLoop = zmalloc(sizeof(*eventLoop))) == NULL) goto err;//分配内存
eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);//分配存储事件内存
eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);//分配临时存储就绪事件内存。
if (eventLoop->events == NULL || eventLoop->fired == NULL) goto err;
eventLoop->setsize = setsize;//初始化setsize
eventLoop->lastTime = time(NULL);
eventLoop->timeEventHead = NULL;
eventLoop->timeEventNextId = 0;
eventLoop->stop = 0;
eventLoop->maxfd = -1;
eventLoop->beforesleep = NULL;
eventLoop->aftersleep = NULL;
if (aeApiCreate(eventLoop) == -1) goto err;//初始化epoll。
/* Events with mask == AE_NONE are not set. So let's initialize the
* vector with it. */
for (i = 0; i < setsize; i++)//将文件事件全部标记为没有注册。
eventLoop->events[i].mask = AE_NONE;
return eventLoop;
err://错误直接全部释放内存。
if (eventLoop) {
zfree(eventLoop->events);
zfree(eventLoop->fired);
zfree(eventLoop);
}
return NULL;
}
/*
2、向aeEventLoop中添加一个IO事件,传入事件fd,监听事件mask、
回调函数,客户数据即可,将此事件注册到epoll中,并通过eventLoop事件数组管理。
*/
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData)
{
if (fd >= eventLoop->setsize) {//fd比setsize还大,则肯定失败。
errno = ERANGE;
return AE_ERR;
}
aeFileEvent *fe = &eventLoop->events[fd];//直接事件fd就当作数组索引,很容易。直接取出内存,填充即可。
if (aeApiAddEvent(eventLoop, fd, mask) == -1)//将fd加入
return AE_ERR;
fe->mask |= mask;//事件
if (mask & AE_READABLE) fe->rfileProc = proc;//读回调函数
if (mask & AE_WRITABLE) fe->wfileProc = proc;//写回调函数
fe->clientData = clientData;//就绪传递给回调函数的数据。
if (fd > eventLoop->maxfd)//通常文件fd是慢慢增加的
eventLoop->maxfd = fd;
return AE_OK;
}
/*
3、删除IO事件,数组不必缩小,直接将其标记为未注册即可。
*/
void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask)
{
if (fd >= eventLoop->setsize) return;
aeFileEvent *fe = &eventLoop->events[fd];
if (fe->mask == AE_NONE) return;//如果还没有注册,那么直接返回,每个数组槽,对应一个事件。
/* We want to always remove AE_BARRIER if set when AE_WRITABLE
* is removed. */
if (mask & AE_WRITABLE) mask |= AE_BARRIER;
aeApiDelEvent(eventLoop, fd, mask);
fe->mask = fe->mask & (~mask);//直接清空
//需要更新最大的fd
if (fd == eventLoop->maxfd && fe->mask == AE_NONE) {
/* Update the max fd */
int j;
for (j = eventLoop->maxfd-1; j >= 0; j--)
if (eventLoop->events[j].mask != AE_NONE) break;
eventLoop->maxfd = j;
}
}
/*
4、向aeEventLoop中添加一个时间事件,距离当前多少ms后发生,回调函数,事件终止回调函数等。每次在事件循环中检测时间是否到达,到达则执行就绪时间事件。
*/
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
aeTimeProc *proc, void *clientData,
aeEventFinalizerProc *finalizerProc)
{
long long id = eventLoop->timeEventNextId++;//下一个事件ID,每个事件对应一个ID,0 1 2 3 4 5
aeTimeEvent *te;
te = zmalloc(sizeof(*te));//分配一个事件结构体
if (te == NULL) return AE_ERR;
te->id = id;//ID从0 1 2 3 4 计数
aeAddMillisecondsToNow(milliseconds,&te->when_sec,&te->when_ms);//获取当前距离1970.1.1:0.0.0的秒及微妙数
te->timeProc = proc;
te->finalizerProc = finalizerProc;
te->clientData = clientData;
te->next = eventLoop->timeEventHead;//将这个事件插入链表中而已。
eventLoop->timeEventHead = te;
return id;//返回当前时间事件ID
}
/*
5、删除时间事件,通过其id,遍历链表,找到id,然后将其标记为AE_DELETED_EVENT_ID,采用惰性删除。
*/
int aeDeleteTimeEvent(aeEventLoop *eventLoop, long long id)
{
aeTimeEvent *te = eventLoop->timeEventHead;
while(te) {
if (te->id == id) {
te->id = AE_DELETED_EVENT_ID;
return AE_OK;
}
te = te->next;
}
return AE_ERR; /* NO event with the specified ID found */
}
/*
6、开始事件循环处理及等待
*/
/* Process every pending time event, then every pending file event
* (that may be registered by time event callbacks just processed).
* Without special flags the function sleeps until some file event
* fires, or when the next time event occurs (if any).
*
* If flags is 0, the function does nothing and returns.
* if flags has AE_ALL_EVENTS set, all the kind of events are processed.
* if flags has AE_FILE_EVENTS set, file events are processed.
* if flags has AE_TIME_EVENTS set, time events are processed.
* if flags has AE_DONT_WAIT set the function returns ASAP until all
* if flags has AE_CALL_AFTER_SLEEP set, the aftersleep callback is called.
* the events that's possible to process without to wait are processed.
*
* The function returns the number of events processed.
*/
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
int processed = 0, numevents;
/* Nothing to do? return ASAP */
/* 同时为空,则直接返回*/
if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
/* Note that we want call select() even if there are no
* file events to process as long as we want to process time
* events, in order to sleep until the next time event is ready
* to fire. */
/*
请注意,既然我们要处理时间事件,即使没有要处理的文件事件,我们仍要调用select(),
以便在下一次事件准备启动之前进行休眠。
当前还没有要处理的文件事件,或者设置了时间时间但是没有设置不阻塞标识。
*/
if (eventLoop->maxfd != -1 ||
((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
int j;
aeTimeEvent *shortest = NULL;
struct timeval tv, *tvp;
if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
shortest = aeSearchNearestTimer(eventLoop);//找到最短注册的时间事件。
if (shortest) {
/*
修正需要等待的时间,因为执行aeSearchNearestTimer,需要花费一定得时间,所以可以修正epoll的时间
*/
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;
if (ms > 0) {//小于ms级别,则可以忽略
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 */
if (flags & AE_DONT_WAIT) {//epoll_wait逐个检测事件一次,并立即返回
tv.tv_sec = tv.tv_usec = 0;
tvp = &tv;
} else {//一直阻塞等待,直到事件就绪
/* Otherwise we can block */
tvp = NULL; /* wait forever */
}
}
/* Call the multiplexing API, will return only on timeout or when
* some event fires. */
/*
直接wait全部,并返回就绪的事件个数。
*/
numevents = aeApiPoll(eventLoop, tvp);//没有IO,就相等于延迟而已
/* After sleep callback. */
if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)
eventLoop->aftersleep(eventLoop);
for (j = 0; j < numevents; j++) {//开始依据fired中的fd循环events中注册的回调函数哦。
aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];//获取事件
int mask = eventLoop->fired[j].mask;//事件状态
int fd = eventLoop->fired[j].fd;//事件fd
int fired = 0; /* Number of events fired for current fd. */
/* Normally we execute the readable event first, and the writable
* event laster. This is useful as sometimes we may be able
* to serve the reply of a query immediately after processing the
* query.
*
* However if AE_BARRIER is set in the mask, our application is
* asking us to do the reverse: never fire the writable event
* after the readable. In such a case, we invert the calls.
* This is useful when, for instance, we want to do things
* in the beforeSleep() hook, like fsynching a file to disk,
* before replying to a client. */
int invert = fe->mask & AE_BARRIER;
/* 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.
*
* Fire the readable event if the call sequence is not
* inverted. */
if (!invert && fe->mask & mask & AE_READABLE) {
fe->rfileProc(eventLoop,fd,fe->clientData,mask);//执行回调函数,传给回调函数有fd,fd的clientdata ,和事件就绪标记
fired++;
}
/* Fire the writable event. */
if (fe->mask & mask & AE_WRITABLE) {
if (!fired || fe->wfileProc != fe->rfileProc) {
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
}
}
/* If we have to invert the call, fire the readable event now
* after the writable one. */
if (invert && fe->mask & mask & AE_READABLE) {
if (!fired || fe->wfileProc != fe->rfileProc) {
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
fired++;
}
}
processed++;
}
}
/* Check time events */
if (flags & AE_TIME_EVENTS)//再检查时间事件,获取当前事件,然后和链表里面的事件比较,大则就绪执行,小则等待即可。
processed += processTimeEvents(eventLoop);
return processed; /* return the number of processed file/time events */
}
/*
7、优先处理IO事件,再通过对比事件,处理到时的时间事件,遍历单链表,
找到时间到达的事件,并调用其回调函数。如果不是定时时间,则将其标记为删除
下次执行到此函数再删除,这就是惰性删除特性,如果是定时事件,则重新设定此事件多久后执行。
*/
/* Process time events */
/*
事件循环,重点考察epoll是如何处理事件事件的。
实际上,就是通过当前获取的时间值,和先前注册的时间值比较,小则就绪,然后可以执行对应的回调函数了。
所以时间事件实际上不精确,类似于通过epoll延迟,然后比较时间值。
*/
static int processTimeEvents(aeEventLoop *eventLoop) {
int processed = 0;
aeTimeEvent *te, *prev;
long long maxId;
time_t now = time(NULL);//返回当前日期和时间,距离1970.1.1:0:0:0的秒数累计值
/* If the system clock is moved to the future, and then set back to the
* right value, time events may be delayed in a random way. Often this
* means that scheduled operations will not be performed soon enough.
*
* Here we try to detect system clock skews, and force all the time
* events to be processed ASAP when this happens: the idea is that
* processing events earlier is less dangerous than delaying them
* indefinitely, and practice suggests it is. */
/*
处理系统Bug,如果上次时间比当前还大,那么时间出了问题,让全部时间就绪,并执行。
这里尝试发现时间混乱的情况,上一次处理事件的时间比当前时间还要大
重置最近一次处理事件的时间
*/
if (now < eventLoop->lastTime) {
te = eventLoop->timeEventHead;
while(te) {
te->when_sec = 0;
te = te->next;
}
}
eventLoop->lastTime = now;//记录上次进入此函数的时间
prev = NULL;
te = eventLoop->timeEventHead;
maxId = eventLoop->timeEventNextId-1;//最大时间事件ID。
while(te) {//遍历时间事件,比较时间
long now_sec, now_ms;
long long id;
/* Remove events scheduled for deletion.
如果时间被删除了
*/
if (te->id == AE_DELETED_EVENT_ID) {
//删除当前事件,prev保存上个节点地址,单链表的删除,easy
aeTimeEvent *next = te->next;
if (prev == NULL)//刚好是头结点,则直接删除
eventLoop->timeEventHead = te->next;
else
prev->next = te->next;//否则通过prev删除
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;
}
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++;//时间次数+1
if (retval != AE_NOMORE) {//如果不是定时时间,则继续设置此时间时间。
aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);
} else {
te->id = AE_DELETED_EVENT_ID;//否则设置功能为删除,这是惰性删除,下次执行到这里再删除。
}
}
prev = te;//更新前驱节点值
te = te->next;//为了下一次迭代
}
return processed;
}
从上面的一些代码可以看出,Redis的作者通过简单的封装epoll使其可以高效的管理IO事件和时间事件,所以作者并没有使用显得臃肿的libevent。
来源:CSDN
作者:有时需要偏执狂
链接:https://blog.csdn.net/u010710458/article/details/80604980