
18. 事件模型——ae_epoll 事件循环源码走读,命令执行 pipeline 图解
(基于 优快云 前文 152161640 的上下文,继续深入)
- 定位
前一章我们把aeEventLoop的骨架拆完了:
- 如何创建、如何注册文件事件、如何注册时间事件;
- 如何把
aeApiState里的epfd与epoll_event数组挂到 Redis 主线程; - 以及
aeProcessEvents的主循环伪代码。
本章只回答两件事:
- 在 ae_epoll 实现层,一次
epoll_wait返回后,Redis 如何把“就绪的 fd”映射到“回调函数”,再扔回上层; - 当这条 fd 恰好是 客户端连接 时,Redis 如何沿着“读→解析→执行→写回”这条 pipeline 把命令一次性推到发送缓冲区,而 不阻塞 主线程。
- ae_epoll 层:从内核事件到 Redis 文件事件
源码位置:src/ae_epoll.c(Redis 7.2 分支,commit 7b0c25c)
1.1 aeApiPoll
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, numevents = 0;
/* 1. 等内核给结果 */
retval = epoll_wait(state->epfd, state->events, eventLoop->setsize,
tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);
if (retval > 0) {
int j;
numevents = retval;
/* 2. 把内核格式转成 Redis 格式 */
for (j = 0; j < numevents; j++) {
int mask = 0;
struct epoll_event *e = state->events+j;
/* 3. 双向映射:epoll_data 存的是 fd */
int fd = e->data.fd;
/* 4. 位掩码翻译 */
if (e->events & EPOLLIN) mask |= AE_READABLE;
if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
if (e->events & EPOLLERR) mask |= AE_WRITABLE;
if (e->events & EPOLLHUP) mask |= AE_WRITABLE;
/* 5. 写到 eventLoop->fired[] 数组,上层可见 */
eventLoop->fired[j].fd = fd;
eventLoop->fired[j].mask = mask;
}
}
return numevents;
}
要点
setsize就是server.maxclients+128,在initServer里resize过;fired[]是 线程本地 的临时快照,后面aeProcessEvents会顺序遍历;- 没有用到
EPOLLONESHOT,因此 fd 会一直被 epoll 监视,Redis 靠“读干净”或“写干净”来避免忙等; - 错误掩码统一交给
AE_WRITABLE,目的是让sendReplyToClient立即感知EPIPE并关闭连接。
1.2 回调路由
回到 aeProcessEvents:
for (j = 0; j < numevents; j++) {
aeFileEvent *fe = &eventLoop->events[fired[j].fd];
int mask = fired[j].mask;
int fd = fired[j].fd;
int rfired = 0;
/* 先读后写,避免短连接写端立即关闭造成的 SIGPIPE */
if (fe->mask & mask & AE_READABLE) {
rfired = 1;
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
}
if (fe->mask & mask & AE_WRITABLE) {
if (!rfired || fe->wfileProc != fe->rfileProc)
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
}
}
rfileProc/wfileProc就是我们在aeCreateFileEvent时传进来的函数指针;- 对 TCP 监听 fd 来说,
rfileProc = acceptTcpHandler; - 对 已连接客户端 fd 来说,
rfileProc = readQueryFromClient,wfileProc = sendReplyToClient。
- 命令执行 pipeline 图解
下面把“客户端发来一条 SET” 时,Redis 主线程 在单次 epoll_wait 返回后 所走的路径画成一张泳道。
| 内核 | Redis 主线程 | 客户端 |
|---|---|---|
① 收到 SYN+ACK 完成三次握手 | acceptTcpHandler 被 epoll 唤醒,创建 client *c,注册 readQueryFromClient 读事件 | |
② 收到 *3\r\n$3\r\nSET\r\n$4\r\nkey1\r\n$6\r\nvalue1\r\n | readQueryFromClient 循环 read(fd, c->querybuf, 16KB),直到 EAGAIN;把新数据追加到 c->querybuf | |
| ③ | 调用 processInputBuffer(c),按 RESP 格式拆成 argv[3],生成 struct redisCommand *cmd = lookupCommand("set") | |
| ④ | 调用 call(c,cmd),内部走 setCommand() → setKey() → signalModifiedKey() → propagate() 写 AOF 和副本 | |
| ⑤ | addReply(c, shared.ok),把 "+OK\r\n" 写进 c->buf(16 KB 静态缓冲),如果放不下则挂 c->reply 链表 | |
| ⑥ | 注册 sendReplyToClient 写事件,等待 EPOLLOUT | |
| ⑦ 内核 TCP 发送缓冲区有空间 | sendReplyToClient 被唤醒,调用 write(fd, c->buf, len),写完或到 EAGAIN 后,若 c->buf 清空则 删除 写事件 | |
| ⑧ | 客户端收到 +OK\r\n |
- 为什么不会阻塞?
- 读阶段:
readQueryFromClient只读到EAGAIN就停,不阻塞; - 解析阶段:纯内存操作,O(N) N 很小;
- 执行阶段:所有 Redis 命令都是 O(1) 或 O(logN),且 单线程 无锁;
- 写阶段:
write到EAGAIN立即停,剩余数据留在c->buf/c->reply,等待下次EPOLLOUT; - 大 key 写放大:若
reply链表超过 32 MB,则client被标记为CLIENT_CLOSE_ASAP,由beforeSleep顺序关闭,防止写死。
- 小结与下一章预告
本章我们补完了 ae_epoll 的最后一公里:
- 内核
epoll_event如何映射到 RedisaeFileEvent; readQueryFromClient→processInputBuffer→call→addReply→sendReplyToClient的 零拷贝 pipeline;- 单线程如何通过“读到干净、写到干净”来保持 非阻塞。
下一章《19. 多 IO 线程模型——redis 6.0 之后如何把“写回”并行化》将回答:
- 为什么只并行 写 而不并行 读;
IO_THREADS_OP_WRITE如何与主线程的beforeSleep做无锁交接;- 以及
lazyfree与io-threads的线程池复用细节。
更多技术文章见公众号: 大城市小农民

被折叠的 条评论
为什么被折叠?



