原文链接:https://redis.io/topics/internals-rediseventlib
Redis执行自己的事件库,其相关实现在文件ae.c中.
理解Redis事件库如何工作的最好办法是理解Redis是如何使用事件库的.
事件循环初始化
文件redis.c中的函数initServer初始化了redisServer结构变量的很多字段,其中就包括事件循环字段el:
aeEventLoop *el
在ae.c文件中的函数initServer调用aeCreateEventLoop来初始化server.el字段.aeEventLoop结构的定义如下所示:
typedef struct aeEventLoop
{
int maxfd;
long long timeEventNextId;
aeFileEvent events[AE_SETSIZE]; /* Registered events */
aeFiredEvent fired[AE_SETSIZE]; /* Fired events */
aeTimeEvent *timeEventHead;
int stop;
void *apidata; /* This is used for polling API specific data */
aeBeforeSleepProc *beforesleep;
} aeEventLoop;
aeCreateEventLoop
函数aeCreateEventLoop首先通过malloc动态分配aeEventLoop结构的内存空间,然后调用文件ae_epoll.c中的函数aeApiCreate.
函数aeApiCreate使用malloc动态分配aeApiState结构内存空间,该结构有两个字段.epfd字段是系统调用epoll_create的返回值,代表epoll文件描述符.events字段是epoll_event结构类型的变量,该结构定义在linux的epoll库中.该字段的使用稍后将继续说明.
接下来是文件ae.c中的函数aeCreateTimeEvent. 但是在调用该函数之前,函数initServer调用文件anet.c中的函数anetTcpServer,创建并返回监听文件描述符.默认情况下, 该描述符将监听端口6379,返回的监听描述符保存在server.fd字段中.
aeCreateTimeEvent
函数aeCreateTimeEvent接收如下参数:
- eventLoop: 对应的实参是文件redis.c中的server.el
- milliseconds: 想对于当前时间, 定时器超时的毫秒数值
- proc: 函数指针.定时器超时以后,该函数将被调用
- clientData: 多数情况下为NULL
- finalizerProc: 函数指针.定时事件从链表中删除前,先执行该函数.
函数initServer调用aeCreateTimeEvent向链表timeEventHead中添加定时事件,该链表结构是server.el中的一个字段.
timeEventHead是指向定时事件链表的指针.文件redis.c中的函数initServer调用aeCreateTimeEvent的代码片段如下:
aeCreateTimeEvent(server.el /*eventLoop*/, 1 /*milliseconds*/, serverCron /*proc*/, NULL /*clientData*/, NULL /*finalizerProc*/);
文件redis.c中函数serverCron执行了许多操作,来帮助Redis正常运行.
aeCreateFileEvent
函数aeCreateFileEvent的本质是执行epoll_ctl系统调用,在监听文件描述符上监听EPOLLIN属性事件,然后关联监听文件描述符和epoll描述符.其中,监听文件描述符是由函数anetTcpServer创建的,epoll描述符是由函数aeCreateEventLoop创建的.
函数aeCreateFileEvent是在由文件redis.c中的函数initServer调用的, 下面是对该函数具体的说明.
函数initServer传递下面的参数到函数aeCreateFileEvent:
- server.el: 由aeCreateEventLoop创建的事件循环. epoll描述符定义在该结构中.
- server.fd: 监听文件描述符. 该字段同时作为索引用来存取
eventLoop->events中的文件事件结构,文件事件结构中保存着一些额外信息,比如回调函数. - AE_READABLE: 监听文件描述符server.fd的EPOLLIN事件的触发条件.
- acceptHandler: 监听文件描述符的EPOLLIN事件触发以后的执行函数,该函数指针存储在eventLoop->events[server.fd]->rfileProc中.
以上是Redis事件循环的初始化部分.
事件循环处理
事件循环初始化完成以后, 文件redis.c中的main函数调用文件ae.c中的函数aeMain来进行事件循环处理.
文件ae.c中的函数aeMain调用文件ae.c中的函数aeProcessEvents在一个while循环语句中处理激发的定时事件和文件事件.
aeProcessEvents
文件ae.c中的函数aeProcessEvents通过调用函数aeSearchNearestTimer,在事件循环中查找最小超时的定时事件.在当前的实现中,事件循环中只有一个定时事件, 该定时事件由文件ae.c中的函数aeCreateTimeEvent创建.
需要注意的是,函数aeCreateTimeEvent创建的定时事件现在可能已经超时了, 因为其在创建的时候,超时时间设置的是一毫秒.因为定时器已经超时,timeval结构变量tvp的两个字段seconds和microseconds都被初始化为零.
tvp结构变量和事件循环变量作为输入参数都被传递给文件ae_epoll.c中的aeApiPoll函数.该函数在epoll描述符上进行系统调用epoll_wait,并在事件激发后,填充eventLoop->fired表:
- fd: 可以进行读写操作的文件描述符,具体可以执行的操作取决于mask的值
- mask: 掩码,控制激发的文件描述符可以进行何种操作.
函数aeApiPoll返回激发的文件描述符的个数.在Redis工作的时候,如果有客户端连接到Redis服务器,那么函数aeApiPoll将从阻塞状态返回,并填充eventLoop->fired表,记录激发的监听文件描述符和AE_READABLE属性的mask.
由于之前事件循环初始化时, 已经为监听文件描述符的accept系统调用的回调函数注册了函数acceptHandler, accept系统调用返回客户端的连接描述符,因此,函数aeProcessEvents将调用文件redis.c中的函数acceptHandler为连接文件描述符创建一个文件事件,这是通过进一步调用文件ae.c中的函数aeCreateFileEvent来实现的, 就像下面这样:
if (aeCreateFileEvent(server.el, c->fd, AE_READABLE,
readQueryFromClient, c) == AE_ERR) {
freeClient(c);
return NULL;
}
其中, 变量c是redisClient结构变量,c->fd是连接描述符.
接下来,函数aeProcessEvent调用函数processTimeEvents来处理定时事件.
processTimeEvents
该函数遍历定时事件链表eventLoop->timeEventHead.
对于每个超时的定时事件,该函数调用注册的回调函数. 在当前的实现中,Redis为定时事件注册的唯一的回调函数为文件redis.c中的函数serverCron.该回调函数执行完毕以后,其返回值是下一次被再次调用的时间间隔.文件ae.c中的函数aeAddMilliSeconds更新该定时事件的超时时间间隔,在下一次遍历函数aeMain的while循环时,将再次检查并处理定时事件.
==================================== 分割线 ======================================
注:
在Linux操作系统上, Redis将使用高效率的epoll多路复用IO接口.因为epoll为linux操作独有的实现机制,在其他符合POSIX规范的操作系统上,将使用select多路复用IO接口,select系统调用在所有符合POSIX规范的操作系统上都是可用的. 但是一些非linux操作系统也可能有自己独有的多路复用IO实现机制.
502

被折叠的 条评论
为什么被折叠?



