原文链接: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实现机制.