事件库
什么是事件库,原始 Redis 事件库是如何实现的?
注意:本文档由 Redis 创建者 Salvatore Sanfilippo 在 Redis 开发早期(约 2010 年)撰写,不一定反映最新的 Redis 实现。
为什么需要事件库?
让我们通过一系列问答来弄清楚。
问:您希望网络服务器始终在做什么?
答:监听其监听端口上的入站连接并接受它们。
问:调用 accept 会产生一个描述符。我该怎么处理它?
答:保存描述符并在其上执行非阻塞读写操作。
问:为什么读写必须是非阻塞的?
A: 如果文件操作(即使 Unix 中的套接字也是文件)是阻塞的,那么服务器如何在文件 I/O 操作阻塞时接受其他连接请求呢?
Q: 我猜我必须对套接字执行许多这样的非阻塞操作,才能查看它何时准备好。我理解正确吗?
A: 是的。事件库就是为你做这个的。现在你明白了。
Q: 事件库是如何做到的?
A: 它们使用操作系统的轮询功能以及计时器。
Q: 那么有没有开源事件库可以做你刚才描述的事情呢?
A: 是的。libevent
和 libev
就是我脑海中想到的两个这样的事件库。
Q: Redis 是否使用这样的开源事件库来处理套接字 I/O?
A: 否。由于各种原因,Redis 使用自己的事件库。
Redis 事件库
Redis 实现了自己的事件库。事件库在 ae.c
中实现。
了解 Redis 事件库工作原理的最佳方法是了解 Redis 如何使用它。
事件循环初始化
在 redis.c
中定义的 initServer
函数初始化 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
为 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
描述符相关联。
以下是 aeCreateFileEvent
从 redis.c:initServer
调用时所做工作的详细说明。
initServer
将以下参数传递给 aeCreateFileEvent
server.el
: 由aeCreateEventLoop
创建的事件循环。epoll
描述符是从server.el
中获取的。server.fd
: 监听描述符,也用作索引,用于从eventLoop->events
表中访问相关文件事件结构并存储额外信息,例如回调函数。AE_READABLE
: 表示必须监控server.fd
上的EPOLLIN
事件。acceptHandler
: 当所监控的事件准备好时必须执行的函数。此函数指针存储在eventLoop->events[server.fd]->rfileProc
中。
这完成了 Redis 事件循环的初始化。
事件循环处理
从 redis.c:main
调用的 ae.c:aeMain
完成了处理在上一阶段初始化的事件循环的工作。
ae.c:aeMain
在一个循环中调用 ae.c:aeProcessEvents
,该循环处理待处理的计时和文件事件。
aeProcessEvents
ae.c:aeProcessEvents
通过在事件循环上调用 ae.c:aeSearchNearestTimer
来查找将在最短时间内待处理的计时事件。在我们的案例中,事件循环中只有一个由 ae.c:aeCreateTimeEvent
创建的计时事件。
请记住,由 aeCreateTimeEvent
创建的计时事件可能现在已经到期,因为它具有 1 毫秒的到期时间。由于计时器已经到期,因此 tvp
timeval
结构变量的秒和微秒字段将初始化为零。
tvp
结构变量以及事件循环变量将传递给 ae_epoll.c:aeApiPoll
。
aeApiPoll
函数在 epoll
描述符上执行 epoll_wait
,并使用详细信息填充 eventLoop->fired
表
fd
: 现在已准备好执行读/写操作的描述符,具体取决于掩码值。mask
: 现在可以在相应描述符上执行的读/写事件。
aeApiPoll
返回此类文件事件的个数,这些事件已准备好进行操作。现在将事情放在上下文中,如果任何客户端请求连接,那么 aeApiPoll
会注意到并使用描述符(即监听描述符)和掩码(即 AE_READABLE
)在 eventLoop->fired
表中填充一个条目。
现在,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
循环的下次迭代。
就是这样。