18.事件模型——ae_epoll 事件循环源码走读,命令执行 pipeline 图解

在这里插入图片描述
18. 事件模型——ae_epoll 事件循环源码走读,命令执行 pipeline 图解
(基于 优快云 前文 152161640 的上下文,继续深入)


  1. 定位
    前一章我们把 aeEventLoop 的骨架拆完了:
  • 如何创建、如何注册文件事件、如何注册时间事件;
  • 如何把 aeApiState 里的 epfdepoll_event 数组挂到 Redis 主线程;
  • 以及 aeProcessEvents 的主循环伪代码。

本章只回答两件事:

  1. ae_epoll 实现层,一次 epoll_wait 返回后,Redis 如何把“就绪的 fd”映射到“回调函数”,再扔回上层;
  2. 当这条 fd 恰好是 客户端连接 时,Redis 如何沿着“读→解析→执行→写回”这条 pipeline 把命令一次性推到发送缓冲区,而 不阻塞 主线程。

  1. 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,在 initServerresize 过;
  • 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 = readQueryFromClientwfileProc = sendReplyToClient

  1. 命令执行 pipeline 图解

下面把“客户端发来一条 SET” 时,Redis 主线程 在单次 epoll_wait 返回后 所走的路径画成一张泳道。

内核Redis 主线程客户端
① 收到 SYN+ACK 完成三次握手acceptTcpHandlerepoll 唤醒,创建 client *c,注册 readQueryFromClient 读事件
② 收到 *3\r\n$3\r\nSET\r\n$4\r\nkey1\r\n$6\r\nvalue1\r\nreadQueryFromClient 循环 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

  1. 为什么不会阻塞?
  • 读阶段readQueryFromClient 只读到 EAGAIN 就停,不阻塞;
  • 解析阶段:纯内存操作,O(N) N 很小;
  • 执行阶段:所有 Redis 命令都是 O(1)O(logN),且 单线程 无锁;
  • 写阶段writeEAGAIN 立即停,剩余数据留在 c->buf/c->reply,等待下次 EPOLLOUT
  • 大 key 写放大:若 reply 链表超过 32 MB,则 client 被标记为 CLIENT_CLOSE_ASAP,由 beforeSleep 顺序关闭,防止写死。

  1. 小结与下一章预告

本章我们补完了 ae_epoll 的最后一公里:

  • 内核 epoll_event 如何映射到 Redis aeFileEvent
  • readQueryFromClientprocessInputBuffercalladdReplysendReplyToClient零拷贝 pipeline
  • 单线程如何通过“读到干净、写到干净”来保持 非阻塞

下一章《19. 多 IO 线程模型——redis 6.0 之后如何把“写回”并行化》将回答:

  • 为什么只并行 而不并行
  • IO_THREADS_OP_WRITE 如何与主线程的 beforeSleep 做无锁交接;
  • 以及 lazyfreeio-threads 的线程池复用细节。
    更多技术文章见公众号: 大城市小农民
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

乔丹搞IT

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值