*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]; /* 已注册的事件 */
    aeFiredEvent fired[AE_SETSIZE]; /* 触发的事件 */
    aeTimeEvent *timeEventHead;
    int stop;
    void *apidata; /* 用于轮询 API 特定的数据 */
    aeBeforeSleepProc *beforesleep;
} aeEventLoop;

*aeCreateEventLoop

aeCreateEventLoop 首先通过 malloc 分配 aeEventLoop 结构,然后调用 ae_epoll.c:aeApiCreate

aeApiCreate 通过 malloc 分配 aeApiState,它有两个字段——epfd,保存由 epoll_create 调用返回的 epoll 文件描述符,以及 events,其类型为 Linux epoll 库定义的 struct epoll_eventevents 字段的用途将在后面描述。

接下来是 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 描述符关联起来。

以下是当从 redis.c:initServer 调用时,aeCreateFileEvent 具体所做操作的解释。

initServeraeCreateFileEvent 传递以下参数:

  • 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 在 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 会注意到它,并用描述符为 监听描述符、掩码为 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 while 循环的下一次迭代中处理。

就这些。