一、前言:从启动到响应的全景图
在前面的文章中,我们已经分别探讨了 select、poll、epoll 的原理,以及 epoll 中 LT/ET 模式的区别。现在,是时候将这些知识串联起来,完整地描绘出 Redis 服务器端基于 epoll 的工作全景图。
Redis 的高性能并非来自单一的"银弹",而是其精巧的单线程 + I/O 多路复用 + 高效数据结构 三位一体架构的完美体现。其中,epoll 作为 Linux 平台上的 I/O 多路复用器,扮演着"中枢神经"的角色,负责高效地收集和分发所有网络事件。
💡 核心价值 :
理解 Redis 基于epoll的完整工作流程,不仅能让你掌握一个顶级开源项目的核心设计,更能为你构建自己的高性能网络服务提供绝佳的范本!
本文将带你:
- 从
main函数开始,一步步追踪 Redis 的启动流程 - 剖析其事件循环 (
aeEventLoop) 如何驱动整个服务器 - 详解客户端连接建立、命令处理、结果返回的全过程
二、服务器启动:万事俱备,只欠东风
2.1 初始化阶段 (main -> initServer)
当执行 redis-server 命令后,程序从 main 函数进入,最终会调用 initServer() 函数进行核心初始化。
cpp
// server.c
int main(int argc, char **argv) {
// ...
initServer(); // 核心初始化
// ...
aeMain(server.el); // 进入主事件循环
}
2.2 创建监听套接字 (Listening Socket)
initServer 的首要任务之一就是创建一个用于监听客户端连接请求的 TCP 套接字。
cpp
void initServer(void) {
// ...
// 1. 创建 TCP socket
server.fd = anetTcpServer(server.neterr, server.port, server.bindaddr, server.protected_mode);
// ...
}
这个 server.fd 就是我们的监听套接字,它被绑定到配置文件中指定的端口(默认 6379)。
2.3 创建 epoll 实例与事件循环
接下来,Redis 会初始化其核心的事件驱动框架。
cpp
void initServer(void) {
// ...
// 2. 创建事件循环 (aeEventLoop)
server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
// ...
}
在 Linux 系统上,aeCreateEventLoop 内部会调用 aeApiCreate,而后者最终会执行 epoll_create1(EPOLL_CLOEXEC),创建一个 epoll 实例,并将其文件描述符 (epfd) 保存在 aeApiState 结构中。
2.4 注册监听事件
万事俱备,只差将监听套接字注册到 epoll 实例中。
cpp
void initServer(void) {
// ...
// 3. 将监听套接字 server.fd 注册到事件循环中
if (aeCreateFileEvent(server.el, server.fd, AE_READABLE,
acceptTcpHandler, NULL) == AE_ERR) {
// 错误处理
}
// ...
}
aeCreateFileEvent: Redis 事件循环的 API,用于注册文件事件。AE_READABLE: 表示关心"可读"事件。acceptTcpHandler: 事件处理器函数。当监听套接字变为可读(即有新连接到达)时,该函数会被调用。
这一步内部会调用 epoll_ctl(EPOLL_CTL_ADD, ...),将 server.fd 添加到 epoll 的红黑树中,并关联 acceptTcpHandler 回调。
至此,Redis 服务器已成功启动,并进入了"待命"状态,静静地等待客户端的到来。
三、主事件循环:永不停止的心脏
服务器启动后,main 函数会调用 aeMain(server.el),进入主事件循环。
cpp
// ae.c
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// 核心:处理文件事件和时间事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
aeProcessEvents 是整个事件循环的核心,它主要做两件事:
- 计算超时时间 :如果有定时任务(如
serverCron),则计算下次执行前需要等待的时间。 - 调用
aeApiPoll:这是最关键的一步,它会阻塞在epoll_wait上,等待内核通知事件发生。
cpp
// ae_epoll.c
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
// ...
// 调用 epoll_wait,阻塞等待事件
numevents = epoll_wait(state->epfd, state->events, eventLoop->setsize, timeout);
// ...
// 将内核返回的就绪事件填充到 eventLoop->fired 数组
for (j = 0; j < numevents; j++) {
eventLoop->fired[j].fd = state->events[j].data.fd;
// ... 设置 mask
}
return numevents;
}
✅ 关键点 :
epoll_wait返回后,eventLoop->fired数组里只包含真正就绪的 fd。主循环接下来只需要遍历这个数组即可,效率极高。
四、处理客户端请求:从连接到响应
4.1 接受新连接 (acceptTcpHandler)
当第一个客户端尝试连接时,内核会将监听套接字 server.fd 加入 epoll 的就绪链表。epoll_wait 返回后,Redis 会调用 acceptTcpHandler。
cpp
void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
int cfd, max = MAX_ACCEPTS_PER_CALL;
while(max--) {
// 1. 调用 accept 接受新连接
cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
if (cfd == ANET_ERR) break;
// 2. 为新客户端创建 client 对象
acceptCommonHandler(createClient(cfd), 0, cip);
}
}
createClient 函数会为这个新连接分配一个 client 结构体,并将新客户端的套接字 cfd 也注册到 epoll 实例中 ,关心其 AE_READABLE 事件,并设置处理器为 readQueryFromClient。
cpp
// networking.c
client *createClient(int fd) {
// ...
if (fd != -1) {
// 将新客户端的 fd 注册到 epoll,关心可读事件
aeCreateFileEvent(server.el, fd, AE_READABLE, readQueryFromClient, c);
}
return c;
}
4.2 读取并解析命令 (readQueryFromClient)
当客户端发送命令(如 SET key value)时,其套接字 cfd 变为可读,epoll_wait 返回,并触发 readQueryFromClient。
cpp
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
client *c = (client*) privdata;
// 1. 从 socket 读取数据到 client 的 querybuf 缓冲区
nread = read(fd, c->querybuf+qblen, readlen);
// 2. 解析缓冲区中的 RESP 协议,生成 redisCommand
processInputBuffer(c);
}
processInputBuffer 会解析 RESP 协议,并将完整的命令放入 client 的 cmd 字段中。
4.3 执行命令并返回结果
在 processInputBuffer 的末尾,会调用 processCommandAndResetClient 来执行命令。
cpp
void processCommandAndResetClient(client *c) {
// 1. 执行具体的命令逻辑 (如 setCommand, getCommand)
if (processCommand(c) == C_OK) {
// 2. 如果客户端有数据要写回(通常是命令执行结果)
if (clientHasPendingReplies(c)) {
// 将该客户端的 fd 注册为可写事件
aeCreateFileEvent(server.el, c->fd, AE_WRITABLE, sendReplyToClient, c);
}
}
}
4.4 发送响应 (sendReplyToClient)
一旦客户端的套接字变为可写(通常很快),epoll_wait 会再次返回,并触发 sendReplyToClient。
cpp
void sendReplyToClient(aeEventLoop *el, int fd, void *privdata, int mask) {
client *c = privdata;
// 1. 将 client->reply 链表中的数据通过 write/send 写回 socket
nwritten = write(fd, c->buf + c->sentlen, objlen - c->sentlen);
// 2. 如果所有数据都已发送完毕
if (c->sentlen == c->bufpos) {
// 取消对该 fd 的可写事件监听
aeDeleteFileEvent(server.el, c->fd, AE_WRITABLE);
}
}
至此,一次完整的"连接 -> 请求 -> 响应"流程圆满完成。
五、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!