事件库

什么是事件库,原始 Redis 事件库是如何实现的?

注意:本文档由 Redis 创建者 Salvatore Sanfilippo 在 Redis 开发早期(约 2010 年)撰写,不一定反映最新的 Redis 实现。

为什么需要事件库?

让我们通过一系列问答来弄清楚。

问:您希望网络服务器始终在做什么?
答:监听其监听端口上的入站连接并接受它们。

问:调用 accept 会产生一个描述符。我该怎么处理它?
答:保存描述符并在其上执行非阻塞读写操作。

问:为什么读写必须是非阻塞的?
A: 如果文件操作(即使 Unix 中的套接字也是文件)是阻塞的,那么服务器如何在文件 I/O 操作阻塞时接受其他连接请求呢?

Q: 我猜我必须对套接字执行许多这样的非阻塞操作,才能查看它何时准备好。我理解正确吗?
A: 是的。事件库就是为你做这个的。现在你明白了。

Q: 事件库是如何做到的?
A: 它们使用操作系统的轮询功能以及计时器。

Q: 那么有没有开源事件库可以做你刚才描述的事情呢?
A: 是的。libeventlibev 就是我脑海中想到的两个这样的事件库。

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 首先使用 mallocaeEventLoop 结构分配内存,然后调用 ae_epoll.c:aeApiCreate

aeApiCreate 使用 mallocaeApiState 分配内存,该结构有两个字段 - 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.eltimeEventHead 字段中。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 描述符相关联。

以下是 aeCreateFileEventredis.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:acceptHandleracceptHandler监听描述符上执行 accept,返回一个与客户端连接的连接描述符redis.c:createClient 通过调用 ae.c:aeCreateFileEvent(如下所示)在连接描述符上添加文件事件

if (aeCreateFileEvent(server.el, c->fd, AE_READABLE,
    readQueryFromClient, c) == AE_ERR) {
    freeClient(c);
    return NULL;
}

credisClient 结构变量,c->fd 是连接描述符。

接下来,ae.c:aeProcessEvent 调用 ae.c:processTimeEvents

processTimeEvents

ae.processTimeEvents 迭代从 eventLoop->timeEventHead 开始的计时事件列表。

对于每个已到期的计时事件,processTimeEvents 都调用已注册的回调。在本例中,它调用已注册的唯一计时事件回调,即 redis.c:serverCron。回调返回计时器再次调用回调的时间(以毫秒为单位)。此更改通过调用 ae.c:aeAddMilliSeconds 记录,并将处理 ae.c:aeMain 循环的下次迭代。

就是这样。

RATE THIS PAGE
Back to top ↑