事件库

什么是事件库,最初的 Redis 事件库是如何实现的?

Redis 开源

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

为什么需要事件库?

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

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

问:调用 accept 产生一个描述符。 我该怎么办?
答:保存描述符并对其执行非阻塞读/写操作。

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

问:我想我必须在套接字上执行许多这样的非阻塞操作才能看到它何时准备就绪。 我对吗?
答:是的。 这就是事件库为你做的事情。 现在你明白了。

问:事件库如何完成他们所做的事情?
答:他们使用操作系统轮询工具以及计时器。

问:那么,有没有任何开源事件库可以完成您刚才描述的事情?
答:是的。 libeventlibev 是我可以立即想到的两个这样的事件库。

问:Redis 是否使用此类开源事件库来处理套接字 I/O?
答:否。由于各种原因,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 首先 mallocs aeEventLoop 结构,然后调用 ae_epoll.c:aeApiCreate

aeApiCreate mallocs 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.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 系统调用,该调用添加一个对 EPOLLIN 事件的监视,在由 anetTcpServer 创建的 *监听描述符* 上,并将其与通过调用 aeCreateEventLoop 创建的 epoll 描述符关联。

以下是来自 redis.c:initServer 调用时 aeCreateFileEvent 具体执行的操作的说明。

initServer 将以下参数传递给 aeCreateFileEvent

  • server.el:由 aeCreateEventLoop 创建的事件循环。 epoll 描述符从 server.el 获得。
  • server.fd:*监听描述符*,也用作从 eventLoop->events 表访问相关文件事件结构并存储额外信息(如回调函数)的索引。
  • AE_READABLE:表示必须监视 server.fdEPOLLIN 事件。
  • 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 将会注意到它,并将一个条目填充到 eventLoop->fired 表中,其中描述符是监听描述符,掩码是 AE_READABLE

现在,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 循环的下一次迭代中处理。

就这样。

评价此页
返回顶部 ↑