> 文章列表 > Redis源码阅读 ae.c

Redis源码阅读 ae.c

Redis源码阅读 ae.c

Redis源码阅读 ae.c

Event loop 讲解一下这个是如何实现的

Redis 使用事件驱动的方式实现 IO 多路复用,其核心是基于第三方库的事件循环库。Redis 支持多种事件循环库,包括:

  • libevent

  • libev

  • ae (Redis 自带的事件循环库)

其中,Redis 自带的事件循环库 ae 较为轻量级,且支持跨平台,因此 Redis 默认使用 ae 作为事件循环库。

下面简单介绍 Redis 中事件循环的实现原理:

初始化事件循环

在 Redis 启动时,会创建一个事件循环对象,并注册一些必要的事件。事件循环对象中包含了一个事件池和一个定时器队列。

aeEventLoop 是 Redis 中的一个事件循环库,是实现 Redis 事件驱动模型的核心。aeEventLoop 的实现基于 epoll (Linux)和 kqueue (FreeBSD、macOS)等系统调用,能够高效地处理大量的并发连接。下面是一些 aeEventLoop 的讲解:

在 Redis 启动时,会创建一个 aeEventLoop 对象,并初始化 epollkqueue 句柄。

注册事件

当 Redis 需要监听某个文件描述符的事件时,会调用事件循环库提供的 API,将该事件添加到事件池中。事件包含了文件描述符、事件类型(读、写、异常等)、处理函数等信息。

当 Redis 需要监听某个文件描述符的事件时,会调用 aeCreateFileEvent 函数,将该事件添加到事件池中。该函数会将文件描述符和事件类型作为参数,以及一个回调函数,该函数会在文件描述符上产生事件时被调用。在 aeCreateFileEvent 函数中,会将文件描述符添加到 epollkqueue 的事件集合中。

运行事件循环

在事件循环库的主循环中,会不断地从事件池中获取事件,并调用相应的处理函数进行处理。如果事件处理函数中需要阻塞等待某个条件,事件循环库会将该事件从事件池中移除,并将其添加到等待队列中,直到条件满足后重新将其添加到事件池中。

aeMain 函数中,会不断地从 epollkqueue 中获取事件,并调用相应的回调函数进行处理。在处理完所有的文件描述符事件后,aeMain 函数会处理所有已到期的时间事件,即调用 aeProcessEvents 函数。

处理定时器

事件循环库还支持定时器功能,即在一定时间后触发某个事件。Redis 中的定时器使用的是时间事件,每个时间事件包含了一个时间戳和一个处理函数。事件循环库会维护一个定时器队列,并在每次循环时检查是否有时间事件到期,如果到期则调用相应的处理函数。

aeEventLoop 还支持定时器功能,即在一定时间后触发某个事件。Redis 中的定时器使用的是时间事件,每个时间事件包含了一个时间戳和一个处理函数。在 aeCreateTimeEvent 函数中,会将时间事件添加到时间事件链表中,时间事件链表按照时间戳从小到大排序。在 aeProcessTimeEvents 函数中,会循环遍历时间事件链表,处理所有已到期的时间事件。

总之,aeEventLoop 是 Redis 实现高性能 IO 的重要组成部分,能够高效地处理大量的并发连接,并支持定时器功能。aeEventLoop 实现简单、易于维护,是 Redis 实现事件驱动模型的核心。

分析一下ae.c中的数据结构关系

在 Redis 中,ae.c 文件是实现事件驱动模型的核心代码文件,其中定义了很多数据结构和函数。下面是 ae.c 中一些重要的数据结构及其关系:

aeEventLoop 结构体

aeEventLoop 结构体是整个事件循环的核心数据结构,定义如下:

typedef struct aeEventLoop {int maxfd;                  // 监听的文件描述符的最大值int setsize;                // 监听的文件描述符的数量long long timeEventNextId;  // 时间事件的编号time_t lastTime;            // 上一次处理时间事件的时间aeFileEvent *events;        // 文件事件数组aeFiredEvent *fired;        // 已触发事件数组aeTimeEvent *timeEventHead; // 时间事件链表头指针int stop;                   // 事件循环是否停止标志void *apidata;              // 多路复用库的私有数据aeBeforeSleepProc *beforesleep; // 事件循环的前置处理函数aeBeforeSleepProc *aftersleep; // 事件循环的后置处理函数int flags;
} aeEventLoop;

其中,maxfd 表示监听的文件描述符的最大值,setsize 表示监听的文件描述符的数量,events 是一个指向 aeFileEvent 数组的指针,fired 是一个指向 aeFiredEvent 数组的指针,timeEventHead 是一个指向 aeTimeEvent 链表头的指针,stop 表示事件循环是否停止,apidata 是多路复用库的私有数据,beforesleep 是事件循环的前置处理函数。

aeFileEvent 结构体

aeFileEvent 结构体表示要监听的文件描述符的事件,定义如下:

typedef struct aeFileEvent {int mask;                 // 监听的事件掩码aeFileProc *rfileProc;    // 读事件的处理函数aeFileProc *wfileProc;    // 写事件的处理函数void *clientData;         // 客户端数据
} aeFileEvent;

其中,mask 表示要监听的事件掩码,可以是 AE_READABLEAE_WRITABLE 或它们的组合,rfileProcwfileProc 分别表示读事件和写事件的处理函数,clientData 表示客户端数据。

aeFiredEvent 结构体

aeFiredEvent 结构体表示已触发的事件,定义如下:

typedef struct aeFiredEvent {int fd;            // 触发事件的文件描述符int mask;          // 触发的事件类型
} aeFiredEvent;

其中,fd 表示触发事件的文件描述符,mask 表示触发的事件类型,可以是 AE_READABLEAE_WRITABLE 或它们的组合。

aeTimeEvent 结构体

aeTimeEvent 结构体表示时间事件,定义如下:

typedef struct aeTimeEvent {long long id;             // 时间事件的编号long when_sec;            // 时间事件的触发时间(秒)long when_ms;             // 时间事件的触发时间(毫秒)aeTimeProc *timeProc;     // 时间事件的处理函数aeEventFinalizerProc *finalizerProc; // 时间事件的处理函数的释放函数void *clientData;         // 客户端数据struct aeTimeEvent *prev; // 前一个时间事件指针struct aeTimeEvent *next; // 后一个时间事件指针
} aeTimeEvent;

其中,id 表示时间事件的编号,when_secwhen_ms 分别表示时间事件的触发时间(秒和毫秒),timeProc 表示时间事件的处理函数,finalizerProc 表示时间事件的处理函数的释放函数,clientData 表示客户端数据。时间事件按照触发时间从小到大排序,以双向链表的形式组织起来。

aeApiState 结构体

aeApiState 结构体表示多路复用库的私有数据,定义如下:

typedef struct aeApiState {int epfd;       // epoll 句柄struct epoll_event *events; // epoll 事件数组
} aeApiState;

其中,epfd 表示 epoll 句柄,events 是一个指向 epoll 事件数组的指针。多路复用库的私有数据由 aeEventLoop 结构体中的 apidata 字段指向。

为啥使用aeApiPoll 的select呢而不用epoll呢?

在 Redis 中,aeApiPoll 函数是用来进行文件事件的多路复用的函数,它可以使用 selectpollepoll 等多种多路复用机制。而在选择使用 select 还是 epoll 时,有以下几个因素需要考虑:

  • 可移植性:select 是 POSIX 标准的一部分,几乎所有的操作系统都支持,因此具有很好的可移植性;而 epoll 是 Linux 特有的机制,其他操作系统可能不支持。

  • 文件描述符数量:select 在处理大量文件描述符时,性能会随着文件描述符数量的增加而下降,而 epoll 则可以高效地处理大量文件描述符。

  • 内存占用:selectpoll 在每次调用时,需要将所有待处理的文件描述符集合复制到内核中,因此内存占用会随着文件描述符数量的增加而增加,而 epoll 则只需要在初始化时将所有待处理的文件描述符添加到内核事件表中,因此内存占用相对较小。

使用 select 还是 epoll 主要取决于具体的应用场景。如果要在多个操作系统上运行,或者处理的文件描述符数量较少,那么可以选择使用 select;如果只在 Linux 上运行,或者需要高效地处理大量文件描述符,那么可以选择使用 epoll。在 Redis 中,由于需要在多个操作系统上运行,同时处理的文件描述符数量也不是很多,因此选择使用 select 是比较合适的。