事件库
什么是事件库,以及原始的Redis事件库是如何实现的?
注意:本文档由Redis的创建者Salvatore Sanfilippo在Redis开发初期(约2010年)编写,并不一定反映最新的Redis实现。
为什么需要事件库?
让我们通过一系列问答来弄清楚。
问:你期望网络服务器一直在做什么?
答:监听其正在监听的端口上的入站连接并接受它们。
问:调用accept会生成一个描述符。我应该如何处理它?
答:保存描述符并在其上执行非阻塞的读/写操作。
问:为什么读写必须是非阻塞的?
答:如果文件操作(即使在Unix中套接字也是一个文件)是阻塞的,那么服务器在文件I/O操作中被阻塞时,如何能够接受其他连接请求。
问:我猜我必须在套接字上执行许多这样的非阻塞操作,以查看它何时准备就绪。我说得对吗?
答:是的。这就是事件库为你做的事情。现在你明白了。
问:事件库是如何工作的?
答:它们使用操作系统的轮询设施和计时器。
问:那么有没有开源的事件库可以实现你刚才描述的功能?
答:有的。libevent
和 libev
是我能立刻想到的两个这样的事件库。
问:Redis是否使用这样的开源事件库来处理套接字I/O?
答:不。出于各种原因,Redis使用自己的事件库。
Redis 事件库
Redis 实现了自己的事件库。事件库在 ae.c
中实现。
理解Redis事件库如何工作的最佳方式是理解Redis如何使用它。
事件循环初始化
initServer
函数定义在 redis.c
中,用于初始化 redisServer
结构体变量的众多字段。其中一个字段是 Redis 事件循环 el
:
aeEventLoop *el
initServer
通过调用在 ae.c
中定义的 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
s aeApiState
包含两个字段 - epfd
保存由 epoll_create
调用返回的 epoll
文件描述符,以及 events
是 Linux epoll
库定义的 struct epoll_event
类型。events
字段的使用将在后面描述。
接下来是ae.c:aeCreateTimeEvent
。但在那之前,initServer
调用了anet.c:anetTcpServer
,它创建并返回一个监听描述符。默认情况下,描述符在端口6379上监听。返回的监听描述符存储在server.fd
字段中。
aeCreateTimeEvent
aeCreateTimeEvent
接受以下参数:
eventLoop
: 这是redis.c
中的server.el
- milliseconds: 从当前时间开始的毫秒数,计时器将在该时间后过期。
proc
: 函数指针。存储定时器到期后需要调用的函数的地址。clientData
: 大多数情况下为NULL
。finalizerProc
: 指向在定时事件从定时事件列表中移除之前必须调用的函数的指针。
initServer
调用 aeCreateTimeEvent
来向 server.el
的 timeEventHead
字段添加一个定时事件。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
系统调用,该系统调用在由 anetTcpServer
创建的 监听描述符 上添加对 EPOLLIN
事件的监视,并将其与通过调用 aeCreateEventLoop
创建的 epoll
描述符关联起来。
以下是关于从redis.c:initServer
调用时,aeCreateFileEvent
具体功能的解释。
initServer
将以下参数传递给 aeCreateFileEvent
:
server.el
: 由aeCreateEventLoop
创建的事件循环。epoll
描述符是从server.el
中获取的。server.fd
: 作为监听描述符,同时也用作从eventLoop->events
表中访问相关文件事件结构并存储额外信息(如回调函数)的索引。AE_READABLE
: 表示需要监视server.fd
的EPOLLIN
事件。acceptHandler
: 当被监视的事件准备就绪时必须执行的函数。此函数指针存储在eventLoop->events[server.fd]->rfileProc
中。
这完成了Redis事件循环的初始化。
事件循环处理
ae.c:aeMain
从 redis.c:main
调用,负责处理在前一阶段初始化的事件循环。
ae.c:aeMain
在一个while循环中调用 ae.c:aeProcessEvents
,该循环处理待处理的时间和文件事件。
aeProcessEvents
ae.c:aeProcessEvents
通过调用 ae.c:aeSearchNearestTimer
在事件循环中查找将在最短时间内挂起的时间事件。在我们的例子中,事件循环中只有一个由 ae.c:aeCreateTimeEvent
创建的计时器事件。
记住,由aeCreateTimeEvent
创建的计时器事件可能现在已经过期,因为它的到期时间为一毫秒。由于计时器已经过期,tvp
timeval
结构变量的秒和微秒字段被初始化为零。
tvp
结构变量与事件循环变量一起传递给 ae_epoll.c:aeApiPoll
。
aeApiPoll
函数在 epoll
描述符上执行 epoll_wait
,并将详细信息填充到 eventLoop->fired
表中:
fd
: 描述符,现在可以根据掩码值进行读/写操作。mask
: 现在可以在相应的描述符上执行的读/写事件。
aeApiPoll
返回准备进行操作的文件事件的数量。现在为了理解上下文,如果有任何客户端请求连接,那么 aeApiPoll
会注意到它,并在 eventLoop->fired
表中填充一个条目,描述符为 监听描述符,掩码为 AE_READABLE
。
现在,aeProcessEvents
调用注册为回调的 redis.c:acceptHandler
。acceptHandler
在监听描述符上执行 accept,返回一个与客户端连接的连接描述符。redis.c:createClient
通过调用 ae.c:aeCreateFileEvent
在连接描述符上添加一个文件事件,如下所示:
if (aeCreateFileEvent(server.el, c->fd, AE_READABLE,
readQueryFromClient, c) == AE_ERR) {
freeClient(c);
return NULL;
}
c
是 redisClient
结构变量,c->fd
是连接的描述符。
接下来,ae.c:aeProcessEvent
调用 ae.c:processTimeEvents
processTimeEvents
ae.processTimeEvents
遍历从 eventLoop->timeEventHead
开始的时间事件列表。
对于每一个已经过时的定时事件,processTimeEvents
会调用已注册的回调函数。在这种情况下,它会调用唯一注册的定时事件回调函数,即 redis.c:serverCron
。回调函数返回一个以毫秒为单位的时间,表示回调函数必须再次被调用的时间。这个变化通过调用 ae.c:aeAddMilliSeconds
记录下来,并将在下一次 ae.c:aeMain
循环迭代时处理。
就这样。