Redis 事件循环(Event Loop)
Redis 事件循环(Event Loop)整体流程
Redis 采用 I/O 多路复用(如 epoll/kqueue) 的事件驱动模型,其事件循环核心逻辑在 ae.c
中实现。
本文基于 Linux 下的 epoll 事件驱动模型
流程图解
启动 Redis|
创建事件循环 aeEventLoop|监听TCP端口|
创建定时器回调(serverCron)|
注册文件事件(监听端口)|
进入 aeMain 主循环|-----------------------------| |回调 beforesleep || |调用 aeApiPoll 等待事件 || |回调 aftersleep || |处理文件事件(读/写客户端数据) || |处理时间事件(定时任务) ||-----------------------------|
直到 stop 标志置位,退出循环
1. Redis 中的事件驱动模型
编译 Redis 时,会检测当前系统中支持的事件驱动模型,从而选择最优的一个事件驱动模型。性能从高到低排序。在 Linux 中,显然会选择 epoll。
select
是一个通用的事件驱动模型,几乎所有系统都支持,因此,它作为兜底的方案。
/* Include the best multiplexing layer supported by this system.* The following should be ordered by performances, descending. */
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else#ifdef HAVE_EPOLL#include "ae_epoll.c"#else#ifdef HAVE_KQUEUE#include "ae_kqueue.c"#else#include "ae_select.c"#endif#endif
#endif
2. Redis 事件类型
Redis 中有两种事件类型:
- 文件事件(File Events):Redis 对 I/O 操作的抽象,用于处理客户端连接、网络请求读写等与 Socket 相关的操作。
- 时间事件(Time Events):Redis 对定时任务的抽象,用于执行周期性或延迟任务(过期键清理、统计信息更新,主从复制心跳等)。
文件事件除了网络事件,还包括其它一些可被监听的事件(比如,管道事件)。
Redis 使用宏定义了这两种事件类型,宏定义位于 ae.h
。
#define AE_FILE_EVENTS (1<<0) // 文件事件
#define AE_TIME_EVENTS (1<<1) // 时间事件#define AE_ALL_EVENTS (AE_FILE_EVENTS|AE_TIME_EVENTS) // 包括文件事件和时间事件
Redis 使用 aeFileEvent
和 aeTimeEvent
结构体分别表示文件事件和时间事件。它们也都定义在 ae.h
中。
此外,aeFiredEvent
结构体用于临时存储单个已触发的文件事件信息。当 Redis 调用 aeApiPoll
(如 epoll_wait
)等待文件事件时,epoll 返回所有就绪的 fd 及其事件类型。aeFiredEvent[]
结构体数组临时存储所有这些就绪事件的信息。
3. 事件数据结构
事件循环结构体 aeEventLoop
调用 aeCreateEventLoop
函数创建。
typedef struct aeEventLoop {int maxfd; // 当前最大文件描述符int setsize; // 最大监听文件描述符数long long timeEventNextId; // 时间事件 ID 计数器aeFileEvent *events; // 注册的文件事件数组aeFiredEvent *fired; // 已触发的文件事件数组aeTimeEvent *timeEventHead; // 时间事件链表头int stop; // 事件循环停止标志void *apidata; /* 指向事件模型的aeApiState指针 */aeBeforeSleepProc *beforesleep; // aeApiPoll()前回调aeBeforeSleepProc *aftersleep; // aeApiPoll()后回调int flags; // 标志
} aeEventLoop;
文件事件 aeFileEvent
调用 aeCreateFileEvent
创建。
typedef struct aeFileEvent {int mask; // 事件类型(AE_READABLE/AE_WRITABLE/AE_BARRIER)aeFileProc *rfileProc; // 读事件处理器(如 readQueryFromClient)aeFileProc *wfileProc; // 写事件处理器(如 sendReplyToClient)void *clientData; // 客户端数据指针
} aeFileEvent;
文件事件处理器
acceptTcpHandler
:接受客户端连接;readQueryFromClient
:读取客户端请求;sendReplyToClient
:向客户端发送响应。
工作流程
- 客户端发起连接,Redis 触发监听 Socket 的 可读事件,调用
acceptTcpHandler
接收连接。 - 客户端发送命令,Redis 触发客户端 Socket 的 可读事件,调用
readQueryFromClient
读取请求并解析。 - 服务端生成响应后,Redis 注册客户端 Socket 的 可写事件,通过
sendReplyToClient
发送响应数据。
时间事件 aeTimeEvent
调用 aeCreateTimeEvent
创建。
typedef struct aeTimeEvent {long long id; // 时间事件唯一ID,单调递增monotime when; // 单调时间,事件过期时间aeTimeProc *timeProc; // 时间事件回调函数aeEventFinalizerProc *finalizerProc; // 事件销毁回调void *clientData; // 指向Client结构指针struct aeTimeEvent *prev; // 指向前一个时间事件struct aeTimeEvent *next; // 指向下一个时间事件int refcount; // 引用计数
} aeTimeEvent;
就绪文件事件 aeFiredEvent
/* A fired event */
typedef struct aeFiredEvent {int fd; // 就绪fdint mask; // fd上的就绪事件
} aeFiredEvent;
4. 事件创建函数
文件事件 aeCreateFileEvent
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,aeFileProc *proc, void *clientData)
{// 超过了最大的fdif (fd >= eventLoop->setsize) {errno = ERANGE;return AE_ERR;}// 实际上我们无需分配aeFileEvent结构内存,// 在创建aeEventLoop结构时已经为我们创建好了aeFileEvent[]数组aeFileEvent *fe = &eventLoop->events[fd];if (aeApiAddEvent(eventLoop, fd, mask) == -1)return AE_ERR;fe->mask |= mask; // 关注fd上发生的事件(读写事件)if (mask & AE_READABLE) fe->rfileProc = proc; // 设置读事件处理器if (mask & AE_WRITABLE) fe->wfileProc = proc; // 设置写事件处理器fe->clientData = clientData;if (fd > eventLoop->maxfd)eventLoop->maxfd = fd; // 记录目前使用的最大fdreturn AE_OK;
}
不同的事件模型,aeApiAddEvent
实现不相同。我们只关注 epoll 的实现,位于 ae_epoll.c
。
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {aeApiState *state = eventLoop->apidata;struct epoll_event ee = {0}; /* avoid valgrind warning *//* 如果epoll已经监视了fd的某些事件,则需要进行MOD操作。否则,我们需要一个 ADD 操作。* 当fd被关闭,会重新将mask设置为 AE_NONE,否则当有其他新客户端连接复用了该fd,会出问题 */int op = eventLoop->events[fd].mask == AE_NONE ?EPOLL_CTL_ADD : EPOLL_CTL_MOD;ee.events = 0;// 合并之前关注的事件,否则就丢失了。mask |= eventLoop->events[fd].mask; /* Merge old events */if (mask & AE_READABLE) ee.events |= EPOLLIN;if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;// 保存fd,后面处理就绪的fd时需要用到ee.data.fd = fd;// 向epoll中添加fd及其关注的事件if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;return 0;
}
貌似 Redis 只使用了 epoll 的水平触发模式(LT)(epoll 的默认模式),没有使用更高效的边缘触发模式(ET)。
时间事件 aeCreateTimeEvent
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,aeTimeProc *proc, void *clientData,aeEventFinalizerProc *finalizerProc)
{long long id = eventLoop->timeEventNextId++; // 单调递增aeTimeEvent *te;// 与文件事件结构不同,我们需要申请aeTimeEvent结构的内存te = zmalloc(sizeof(*te));if (te == NULL) return AE_ERR;te->id = id; // 时间事件唯一IDte->when = getMonotonicUs() + milliseconds * 1000; // 设置过期时间te->timeProc = proc; // 到期后回调函数te->finalizerProc = finalizerProc; // 销毁回调函数te->clientData = clientData;// 将aeTimeEvent添加到eventLoop->timeEventHead链表头te->prev = NULL;te->next = eventLoop->timeEventHead;te->refcount = 0;if (te->next)te->next->prev = te;eventLoop->timeEventHead = te;return id;
}
就绪文件事件
就绪文件事件结构 aeFiredEvent
是在 epoll_wait
调用返回后填充的,其内存已经在创建 aeEventLoop
结构时分配了。
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {aeApiState *state = eventLoop->apidata;int retval, numevents = 0;// 返回就绪的fd数量retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,tvp ? (tvp->tv_sec*1000 + (tvp->tv_usec + 999)/1000) : -1);if (retval > 0) {int j;numevents = retval;// 遍历所有的就绪fdfor (j = 0; j < numevents; j++) {int mask = 0;struct epoll_event *e = state->events+j;.....// 填充就绪文件事件结构eventLoop->fired[j].fd = e->data.fd;eventLoop->fired[j].mask = mask;}}return numevents;
}
5. 事件循环初始化
在 Redis 启动时,初始化事件循环结构体 aeEventLoop
,并注册默认事件处理器。
// server.c
int main(int argc, char **argv) {// 其它初始化.....initServerConfig() {// 初始化.....initConfigValues() // 使用全局数组configs初始化默认值}.....// 解析命令行参数// 加载解析配置文件 .....initServer() {.....server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR); // 创建事件循环.....listenToPort(server.port,&server.ipfd) // 监听TCP端口.....aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) // 创建定时器回调serverCron.....// 创建监听事件结构,设置acceptTcpHandler回调,添加到epoll中createSocketAcceptHandler(&server.ipfd, acceptTcpHandler) .....// 注册回调函数beforeSleep,在每次进入aeApiPoll(等待I/O事件)前被调用aeSetBeforeSleepProc(server.el,beforeSleep);// 注册回调函数afterSleep,在从aeApiPoll返回后(有事件就绪或超时)被调用aeSetAfterSleepProc(server.el,afterSleep);}.....InitServerLast() {bioInit();initThreadedIO();set_jemalloc_bg_thread(server.jemalloc_bg_thread);server.initial_memory_usage = zmalloc_used_memory();}// 进入主循环aeMain(server.el);// 关闭事件循环aeDeleteEventLoop(server.el);return 0; // Redis退出
}
5.1 创建事件循环
执行下面这行代码创建 aeEventLoop
结构,并保存该结构指针到 server.el
。
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
server.maxclients
的值由 redis.conf
配置文件中的 maxclients
配置指令指定,默认是 10000。
看下配置文件对 maxclients
配置指令的描述。
设置同时连接的最大客户端数量。默认情况下,此限制设置为 10000 个客户端连接。然而,如果 Redis 服务器无法配置进程文件限制以达到指定的限制数量,那么允许的最大客户端数量将被设置为当前的文件限制数减去 32(因为 Redis 会为内部用途保留一些文件描述符)。
通过
ulimit -n
查看当前进程文件限制,需修改/etc/security/limits.conf
提升软硬限制(如设置* soft nofile 65535
),并重启生效。一旦达到该限制,Redis 将关闭所有新连接,并发送错误信息 “max number of clients reached”。
重要提示:当使用 Redis 集群时,最大连接数也会被集群总线占用:集群中的每个节点将使用两个连接,一个是入站连接(incoming),另一个是出站连接(outcoming)。在集群规模非常大的情况下,相应地调整这个限制数量是很重要的。
/* 在配置服务器的事件循环时,我们会进行这样的设置:* 让我们能够处理的文件描述符总数达到server.maxclients(服务器允许的最大客户端数量)加上RESERVED_FDS(预留的文件描述符数量),* 并且为了保证安全还会再多留一些。由于RESERVED_FDS默认值是 32,我们额外增加 96 个,* 以此确保额外分配的文件描述符数量不超过 128 个 */
#define CONFIG_FDSET_INCR (CONFIG_MIN_RESERVED_FDS+96)
因此 server.maxclients+CONFIG_FDSET_INCR
默认为 10128。该值会在下面赋值给 eventLoop->setsize
。
aeCreateEventLoop
函数位于 ae.c
。
aeEventLoop *aeCreateEventLoop(int setsize) {aeEventLoop *eventLoop;int i;monotonicInit(); /* just in case the calling app didn't initialize */// 分配内存if ((eventLoop = zmalloc(sizeof(*eventLoop))) == NULL) goto err;// 初始化文件事件数组eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);// 初始化就绪文件事件数组eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);if (eventLoop->events == NULL || eventLoop->fired == NULL) goto err;// 初始化eventLoop->setsize = setsize;eventLoop->timeEventHead = NULL;eventLoop->timeEventNextId = 0;eventLoop->stop = 0;eventLoop->maxfd = -1;eventLoop->beforesleep = NULL;eventLoop->aftersleep = NULL;eventLoop->flags = 0;// 不同事件模型(如epoll)都有各自的实现// 创建具体事件模型的基础设施if (aeApiCreate(eventLoop) == -1) goto err;/* Events with mask == AE_NONE are not set. So let's initialize the* vector with it. */for (i = 0; i < setsize; i++)eventLoop->events[i].mask = AE_NONE; // 初始化return eventLoop;err:if (eventLoop) {zfree(eventLoop->events);zfree(eventLoop->fired);zfree(eventLoop);}return NULL;
}
5.1.1 事件模型初始化
不同的事件模型(如 epoll)中,各自实现自己的 aeApiCreate()
函数。以 Linux 下的 epoll 为例,实现在 ae_epoll.c
中。
static int aeApiCreate(aeEventLoop *eventLoop) {// 创建 aeApiState 结构aeApiState *state = zmalloc(sizeof(aeApiState));if (!state) return -1;// 分配 epoll_event数组state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);if (!state->events) {zfree(state);return -1;}// 创建epoll实例state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */if (state->epfd == -1) {zfree(state->events);zfree(state);return -1;}anetCloexec(state->epfd); // 设置FD_CLOEXEC标志eventLoop->apidata = state; // 关联到事件循环return 0;
}
不同事件模型对 aeApiState
结构体的实现也不相同。保存了具体事件模型的信息。以 epoll 的事件模型为例,定义在 ae_epoll.c
。
typedef struct aeApiState {int epfd; // epoll实例struct epoll_event *events; // 事件数组
} aeApiState;
5.2 打开监听 Socket
我们先看下 redis.conf
对 bind
配置指令的描述。
默认情况下,如果未指定
bind
配置指令,Redis 会监听主机上所有可用网络接口的连接。
可以使用bind
配置指令,后面跟上一个或多个 IP 地址,让 Redis 仅监听一个或多个选定的接口。
每个地址前面都可以加上 “-” 前缀,这意味着如果该地址不可用,Redis 启动时不会失败。这里的 “不可用” 仅指该地址不对应任何网络接口。已经被使用的地址总会导致启动失败,不支持的协议则会被静默跳过。
示例:
bind 192.168.1.100 10.0.0.1
# 监听两个特定的 IPv4 地址
bind 127.0.0.1 ::1
# 监听回环 IPv4 和 IPv6 地址
bind * -::*
# 与默认情况相同,监听所有可用接口
警告:
如果运行 Redis 的计算机直接暴露在互联网中,监听所有接口是非常危险的,这会让 Redis 实例暴露给互联网上的所有人。因此,默认情况下,我们使用以下 bind 指令,这将强制 Redis 仅监听 IPv4 和 IPv6(如果可用)的回环接口地址(这意味着 Redis 只能接受来自运行它的同一台主机上的客户端连接)。
bind 127.0.0.1 -::1
# 监听回环 IPv4 和 IPv6 地址
如果不配置bind
指令,那么默认你的实例将监听所有网络接口。
server.bindaddr
是一个长度为 CONFIG_BINDADDR_MAX
(16) 的字符指针数组,保存了配置指令 bind 指定的绑定地址字符串。server.bindaddr_count
指定了实际指定的地址数量。
server.port
保存了 Redis 的端口,默认是 6379。
将获取的监听 Socket 的 fd 保存到 server.ipfd
。server.ipfd
是一个 socketFds
结构。最多绑定 CONFIG_BINDADDR_MAX
(16)个地址。
#define CONFIG_BINDADDR_MAX 16
typedef struct socketFds {int fd[CONFIG_BINDADDR_MAX];int count;
} socketFds;
调用 listenToPort()
打开地址端口。位于 server.c
。
没啥好说的,老一套,调用 Socket 层函数 socket()
、bind()
和 listen()
获取到监听 socket。
/* 初始化一组文件描述符,使其监听指定的 'port',并绑定 Redis 服务器配置中指定的地址。* 监听用的文件描述符会存储在整数数组 'fds' 中,其数量会被设置到 '*count' 里。* 要绑定的地址在全局的 server.bindaddr 数组中指定,该数组的元素数量为 server.bindaddr_count。* 若服务器配置里未指定具体的绑定地址,此函数会尝试同时为 IPv4 和 IPv6 协议绑定 *(所有地址)。* 若操作成功,函数返回 C_OK。* 如果函数执行出错,它将返回 C_ERR。当出现以下情况时,函数被视为执行出错:* 服务器配置中指定的 server.bindaddr 地址中至少有一个无法绑定;* 或者服务器配置中未指定任何绑定地址,且该函数无法为IPv4或IPv6协议中至少一种协议绑定通配符地址(*,即所有地址)。*/
int listenToPort(int port, socketFds *sfd) {int j;char **bindaddr = server.bindaddr; // 字符数组,保存所有需要绑定的地址(字符串形式)int bindaddr_count = server.bindaddr_count; // 需要绑定的地址数量char *default_bindaddr[2] = {"*", "-::*"}; // 默认绑定的网络接口,即所有网络接口(包括ipv4和ipv6)/* 如果未指定绑定地址,则强制绑定到 0.0.0.0(监听所有网络接口!!危险) */if (server.bindaddr_count == 0) {bindaddr_count = 2;bindaddr = default_bindaddr;}// 遍历所有需要绑定的地址,端口port,默认是6379for (j = 0; j < bindaddr_count; j++) {char* addr = bindaddr[j];// 提取地址字符串的首个字符 '-'(可选)int optional = *addr == '-';// 如果存在 '-',实际的地址从下一个字符开始if (optional) addr++;// 如果地址包含 ':',说明是ipv6地址// sfd->count从0开始(刚开始没有绑定任何地址)// 每成功绑定一个地址,count加1if (strchr(addr,':')) {/* Bind IPv6 address. */sfd->fd[sfd->count] = anetTcp6Server(server.neterr,port,addr,server.tcp_backlog);} else {/* Bind IPv4 address. */sfd->fd[sfd->count] = anetTcpServer(server.neterr,port,addr,server.tcp_backlog);}// 只有成功绑定的地址,count才会加1,否则count不变,继续绑定下一个地址if (sfd->fd[sfd->count] == ANET_ERR) {int net_errno = errno;serverLog(LL_WARNING,"Warning: Could not create server TCP listening socket %s:%d: %s",addr, port, server.neterr);// EADDRNOTAVAIL表明该地址不对应任何网络接口,如果该地址前面加了 '-',则不会报错,跳过即可if (net_errno == EADDRNOTAVAIL && optional)continue;// 不支持的协议则会被静默跳过if (net_errno == ENOPROTOOPT || net_errno == EPROTONOSUPPORT ||net_errno == ESOCKTNOSUPPORT || net_errno == EPFNOSUPPORT ||net_errno == EAFNOSUPPORT)continue;/* Rollback successful listens before exiting */closeSocketListeners(sfd);return C_ERR;}// 地址绑定成功anetNonBlock(NULL,sfd->fd[sfd->count]); // 设置为非阻塞anetCloexec(sfd->fd[sfd->count]); // 设置执行 fork+exec 后关闭sfd->count++; // 成功绑定count加1}return C_OK;
}
简单看下 ipv4 地址的绑定函数 anetTcpServer
,位于 inet.c
。
int anetTcpServer(char *err, int port, char *bindaddr, int backlog)
{return _anetTcpServer(err, port, bindaddr, AF_INET, backlog);
}static int _anetTcpServer(char *err, int port, char *bindaddr, int af, int backlog)
{int s = -1, rv;char _port[6]; /* strlen("65535") */struct addrinfo hints, *servinfo, *p;snprintf(_port,6,"%d",port);memset(&hints,0,sizeof(hints));hints.ai_family = af;hints.ai_socktype = SOCK_STREAM;hints.ai_flags = AI_PASSIVE; /* No effect if bindaddr != NULL */// 如果bindaddr是 '*'(绑定所有接口),bindaddr 置为 0 即可。// bindaddr = 0 对 scoekt 层函数来说就是绑定所有端口的意思。if (bindaddr && !strcmp("*", bindaddr))bindaddr = NULL;// 同上 ipv4if (af == AF_INET6 && bindaddr && !strcmp("::*", bindaddr))bindaddr = NULL;// getaddrinfo 系统调用将地址端口等信息转换成网络格式,保存到 servinfo。// servinfo 内存由 getaddrinfo 分配的,使用完后需要调用 freeaddrinfo 释放。if ((rv = getaddrinfo(bindaddr,_port,&hints,&servinfo)) != 0) {anetSetError(err, "%s", gai_strerror(rv));return ANET_ERR;}// p->ai_next为 NULL,只执行单次循环for (p = servinfo; p != NULL; p = p->ai_next) {// 执行socket系统调用if ((s = socket(p->ai_family,p->ai_socktype,p->ai_protocol)) == -1)continue;if (af == AF_INET6 && anetV6Only(err,s) == ANET_ERR) goto error;// 设置 SO_REUSEADDR 选项if (anetSetReuseAddr(err,s) == ANET_ERR) goto error;// 执行 bind 和 listen 系统调用if (anetListen(err,s,p->ai_addr,p->ai_addrlen,backlog,0) == ANET_ERR) s = ANET_ERR;goto end;}if (p == NULL) {anetSetError(err, "unable to bind socket, errno: %d", errno);goto error;}error:if (s != -1) close(s); // 出错,关闭打开的sockets = ANET_ERR;
end:freeaddrinfo(servinfo); // freeaddrinfo 系统调用释放 getaddrinfo 分配的 servinforeturn s;
}
5.3 为监听 Socket 设置回调函数
打开监听 Socket 后,就该为其设置回调函数了
createSocketAcceptHandler(&server.ipfd, acceptTcpHandler);
客户端连接进来后,调用 acceptTcpHandler
接收客户端连接。
/* Create an event handler for accepting new connections in TCP or TLS domain sockets.* This works atomically for all socket fds */
int createSocketAcceptHandler(socketFds *sfd, aeFileProc *accept_handler) {int j;// 遍历所有的监听socketfor (j = 0; j < sfd->count; j++) {// 创建文件事件结构,添加到epoll中监听读事件if (aeCreateFileEvent(server.el, sfd->fd[j], AE_READABLE, accept_handler,NULL) == AE_ERR) {/* Rollback */for (j = j-1; j >= 0; j--) aeDeleteFileEvent(server.el, sfd->fd[j], AE_READABLE);return C_ERR;}}return C_OK;
}
6. 事件循环主流程
事件循环核心函数 aeMain
持续轮询事件并处理。
// ae.c
void aeMain(aeEventLoop *eventLoop) {eventLoop->stop = 0;while (!eventLoop->stop) {// 处理所有待处理事件aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_BEFORE_SLEEP|AE_CALL_AFTER_SLEEP);}
}
AE_ALL_EVENTS
表示处理所有事件,即文件事件和时间事件。
AE_CALL_BEFORE_SLEEP
表明在执行 aeApiPoll
前回调 beforeSleep
。
AE_CALL_AFTER_SLEEP
表明在 aeApiPoll
返回后回调 afterSleep
。
7. 事件处理核心逻辑
aeProcessEvents
是事件处理的核心函数,负责处理文件事件(I/O)和时间事件(定时任务)。
其核心逻辑分为四步:
- 计算阻塞时间:根据下一个时间事件的触发时间,决定
aeApiPoll
的超时时间; - 等待事件就绪:调用
aeApiPoll
(即epoll_wait
)阻塞等待文件事件; - 处理文件事件:处理就绪的读/写事件;
- 处理时间事件:执行所有到期的定时任务。
// ae.c
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {int processed = 0, numevents;/* 既不关注文件事件也不关注时间事件,那没什么可做的,直接返回 */if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;// 注意:即使没有文件事件要处理,只要我们想处理时间事件,// 我们也要调用 aeApiPoll 休眠,直到下一次时间事件到期后返回,// 以便我们及时处理时间事件。if (eventLoop->maxfd != -1 ||((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {int j;struct timeval tv, *tvp;int64_t usUntilTimer = -1;// --- 1. 计算aeApiPoll阻塞时间 ---// 获取最近的时间事件距离现在的微秒数// AE_DONT_WAIT表明aeApiPoll无需阻塞,无论是否有就绪文件事件,都立刻返回。// !AE_DONT_WAIT则表明aaeApiPoll需要阻塞等待就绪的文件事件,因此需要计算个超时时间,// 避免长时间阻塞,无法及时处理时间事件。if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))usUntilTimer = usUntilEarliestTimer(eventLoop); // 获取最近的时间事件距离现在的微秒数// 转换为 timeval 结构if (usUntilTimer >= 0) {tv.tv_sec = usUntilTimer / 1000000; // 秒tv.tv_usec = usUntilTimer % 1000000; // 微秒tvp = &tv;} else {if (flags & AE_DONT_WAIT) {tv.tv_sec = tv.tv_usec = 0; // 非阻塞模式tvp = &tv;} else {tvp = NULL; // 无限阻塞}}// 回调 beforesleepif (eventLoop->beforesleep != NULL && flags & AE_CALL_BEFORE_SLEEP)eventLoop->beforesleep(eventLoop);// --- 2. I/O 多路复用等待文件事件 ---// 根据操作系统选择epoll(Linux)、kqueue(BSD)或 select(通用)。// 只在超时或某些文件事件触发时返回,// 返回就绪的文件事件的数量(超时返回可能为 0)。numevents = aeApiPoll(eventLoop, tvp); /* 回调 aftersleep */if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)eventLoop->aftersleep(eventLoop);// --- 3. 处理文件事件(I/O 事件优先)---// 遍历所有就绪事件for (j = 0; j < numevents; j++) {// 获取就绪的文件事件结构aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];int mask = eventLoop->fired[j].mask; // 获取就绪的fd上的事件(AE_READABLE/AE_WRITABLE)int fd = eventLoop->fired[j].fd; // 获取就绪的fd// 每个就绪fd处理的事件数,每一轮for循环重新初始化为0// 如果只处理了读或写事件,则fired=1,// 如果既处理了读也处理了写事件,则fired=2int fired = 0;/* 通常情况下,我们会先执行可读事件,然后再执行可写事件。这是很有用的,* 因为有时我们在处理完一个查询之后,也许能够立即发送该查询的回复。* 然而,如果在掩码中设置了AE_BARRIER标志,* 那么我们的应用程序就要求我们做相反的操作:绝不在可读事件之后触发可写事件。* 在这种情况下,我们会颠倒调用顺序。* 例如,当我们想在回复客户端之前,在beforeSleep()钩子函数中执行一些操作(比如将一个文件同步到磁盘)时,* 这种做法就很有用。 */int invert = fe->mask & AE_BARRIER;/* 请注意 “fe->mask & mask & ...” 这段代码:可能前面一个已处理的事件删除了一个已触发但我们尚未处理的事件,* 所以我们要检查该事件是否仍然有效。** 如果调用顺序没有颠倒,优先处理读事件。 */if (!invert && fe->mask & mask & AE_READABLE) {fe->rfileProc(eventLoop,fd,fe->clientData,mask); // 调用读事件处理器fired++;fe = &eventLoop->events[fd]; /* Refresh in case of resize. */}/* Fire the writable event. */if (fe->mask & mask & AE_WRITABLE) {// 如果前面处理了读事件,而读写事件的处理器相同,// 则没必要再执行一遍写处理器了if (!fired || fe->wfileProc != fe->rfileProc) {fe->wfileProc(eventLoop,fd,fe->clientData,mask); // 调用写事件处理器fired++;}}/* 如果调用顺序颠倒了,则在写事件处理完之后,在处理读事件 */if (invert) {fe = &eventLoop->events[fd]; /* Refresh in case of resize. */if ((fe->mask & mask & AE_READABLE) &&(!fired || fe->wfileProc != fe->rfileProc)){fe->rfileProc(eventLoop,fd,fe->clientData,mask); // 调用读事件处理器fired++;}}processed++; // 已处理的fd数量加1}}// --- 4. 处理时间事件(定时任务)---// 优先处理文件事件,// 上面已经处理了文件事件,接下来就该处理时间事件了if (flags & AE_TIME_EVENTS) {processed += processTimeEvents(eventLoop);}return processed; // 返回已处理的文件和时间事件总和
}
7.1 时间事件处理逻辑
aeProcessEvents
调用 processTimeEvents
完成时间事件的处理。
所有的时间事件 aeTimeEvent
都是通过 aeCreateTimeEvent()
函数添加到 eventLoop->timeEventHead
双向链表头的。
/* Process time events */
static int processTimeEvents(aeEventLoop *eventLoop) {int processed = 0;aeTimeEvent *te;long long maxId;// 所有的时间事件通过双向链表链接在一起,// eventLoop->timeEventHead就是链表头。// 从链表头开始处理所有时间事件。te = eventLoop->timeEventHead;// 获取当前最大的时间事件ID,ID分配时单调递增maxId = eventLoop->timeEventNextId-1;monotime now = getMonotonicUs(); // 获取当前时间的微秒数// 循环处理所有时间事件while(te) {long long id;/* Remove events scheduled for deletion. */if (te->id == AE_DELETED_EVENT_ID) {aeTimeEvent *next = te->next;/* If a reference exists for this timer event,* don't free it. This is currently incremented* for recursive timerProc calls */if (te->refcount) {te = next; // 遍历下一个时间事件continue;}// te的引用计数0// 从链表中删除teif (te->prev)te->prev->next = te->next;elseeventLoop->timeEventHead = te->next;if (te->next)te->next->prev = te->prev;// 如果te有注册回调函数finalizerProcif (te->finalizerProc) {te->finalizerProc(eventLoop, te->clientData); // 执行回调now = getMonotonicUs(); // 获取当前时间的微秒数}zfree(te); // 释放te内存te = next; // 遍历下一个时间事件continue;}/* 确保我们在此迭代中不处理由时间事件创建的时间事件。* 请注意,这个检查目前是无用的:我们总是在链表头部添加新的事件事件,* 但是如果我们改变实现细节,这个检查可能会再次有用:我们将它保存在这里以备将来防御。 */if (te->id > maxId) {te = te->next;continue;}// 时间事件过期,处理它if (te->when <= now) {int retval;id = te->id;te->refcount++; // 引用计数加1,防止在执行timeProc期间被删除// 调用时间事件处理函数,如serverCronretval = te->timeProc(eventLoop, id, te->clientData);te->refcount--; // 引用计数减1processed++; // 处理的事件加1now = getMonotonicUs(); // serverCron返回1000/server.hz// 如果hz=10,则每100ms调用一次if (retval != AE_NOMORE) {te->when = now + retval * 1000; // 更新过期时间} else {// 单次时间事件,设置删除标志,下一次事件循环会删除te->id = AE_DELETED_EVENT_ID; }}te = te->next; // 继续链表中下一个时间事件,直到为null}return processed;
}
8. 后记
Redis 事件循环大致流程就这样了。Redis 的核心业务都在事件循环中完成,例如,beforeSleep 和 serverCron 这两个重量级函数。
后面会另开文章重点讲解。