Redis网络模型-IO多路复用
一、前言:单线程的“魔法”从何而来?
Redis 以其惊人的性能闻名于世,而其核心秘密之一,就是单线程 + IO 多路复用的网络模型。一个线程如何能同时处理数万甚至数十万的并发连接?这听起来像是一个悖论。
答案就在于 IO 多路复用(I/O Multiplexing) 技术。它并非 Redis 的发明,而是操作系统提供的一种高效 IO 机制。Redis 的伟大之处,在于它精准地选择了这项技术,并将其发挥到了极致。
💡 核心价值:
IO 多路复用允许一个线程同时监控多个文件描述符(如 socket),并在其中任何一个就绪(可读/可写)时得到通知。这彻底解决了传统阻塞 IO 模型在高并发下的资源瓶颈问题!
本文将带你:
- 彻底搞懂 IO 多路复用的核心思想
- 深度对比
select/poll/epoll三大实现 - 剖析 Redis 源码中如何封装和使用
epoll
二、IO 多路复用:一个高效的“侦察兵”
2.1 核心思想
想象你是一家大型餐厅的经理,手下有成千上万的餐桌(socket 连接)。你的任务是,当任何一桌客人需要服务(数据到达)时,你都要立刻知道。
- 阻塞 IO 模型:你需要为每一桌都配一个服务员(线程),服务员站在桌边傻等。成本极高。
- 非阻塞 IO 模型:你亲自不停地在所有桌子间跑来跑去问“需要服务吗?”。CPU 被你跑废了。
- IO 多路复用模型:你雇佣了一个超级高效的侦察兵(
epoll)。你只需要告诉侦察兵:“帮我盯着这些桌子”。然后你就可以去休息(epoll_wait阻塞)。一旦有任何一桌客人举手,侦察兵会立刻跑来叫醒你,并告诉你具体是哪几桌。
✅ 本质:将“轮询”的工作交给内核(侦察兵)去做,应用程序(经理)只需被动等待通知。
2.2 技术定义
IO 多路复用是一种同步 IO 模型,它允许一个线程通过一个系统调用,同时监视多个文件描述符的 IO 状态。当被监视的 fd 中至少有一个进入就绪状态(例如,socket 接收缓冲区有数据可读),该系统调用就会返回。
三、Linux 三大 IO 多路复用实现详解
Linux 提供了三种 IO 多路复用的系统调用:select、poll 和 epoll。它们代表了这项技术的演进历程。
3.1 select:开山鼻祖,但力不从心
int select(int nfds, fd_set *readfds, fd_set *writefds, fd_set *exceptfds, struct timeval *timeout);
- 工作原理:
- 应用程序将所有要监听的 fd 打包到一个
fd_set位图中,传给内核。 - 内核收到后,会线性遍历这个集合中的每一个 fd,检查其状态。
- 如果有就绪的 fd,内核会修改传入的
fd_set,标记出就绪的 fd,然后返回。
- 应用程序将所有要监听的 fd 打包到一个
- 致命缺陷:
- FD 数量限制:
fd_set的大小固定(通常为 1024),即最多只能监听 1024 个连接。 - O(N) 时间复杂度:每次调用,内核和用户态都需要线性扫描整个 fd 集合,无论有多少 fd 是活跃的。对于 10,000 个连接,即使只有 1 个活跃,也要扫描 10,000 次。
- 重复拷贝:每次调用都需要将整个
fd_set从用户空间拷贝到内核空间,返回时再拷贝回来,开销巨大。
- FD 数量限制:
3.2 poll:select 的小修小补
int poll(struct pollfd *fds, nfds_t nfds, int timeout);
- 改进:
- 使用
pollfd结构体数组代替fd_set位图,突破了 1024 的连接数限制。
- 使用
- 未解决的问题:
- 依然是 O(N) 时间复杂度,内核依然需要线性遍历所有 fd。
- 依然存在用户态与内核态之间的大量数据拷贝。
3.3 epoll:为高性能而生的终极解决方案
epoll 是 Linux 2.6 内核引入的,专为解决 select/poll 在高并发场景下的性能瓶颈。
// 核心 API
int epoll_create(int size); // 创建一个 epoll 实例
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event); // 注册/修改/删除监听的 fd
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout); // 等待事件
- 革命性设计:
- 分离关注点:
epoll将“注册监听”和“等待事件”分成了两个独立的系统调用 (epoll_ctl和epoll_wait)。这意味着,添加/删除监听的 fd 不需要每次都传递整个列表。 - 内核级高效数据结构:
- 红黑树 (Red-Black Tree):内核使用红黑树来存储所有被监听的 fd。这使得
epoll_ctl的增删改操作时间复杂度仅为 O(log N)。 - 就绪链表 (Ready List):当某个被监听的 fd 上有事件发生时,内核的中断处理程序会直接将该 fd 加入到一个就绪链表中。
- 红黑树 (Red-Black Tree):内核使用红黑树来存储所有被监听的 fd。这使得
- 零无效遍历:
epoll_wait调用时,内核只需检查就绪链表是否为空。如果非空,就将链表中的元素(就绪的 fd)批量拷贝到用户提供的events数组中。时间复杂度是 O(1),与总的监听 fd 数量无关,只与本次就绪的 fd 数量有关。 - 内存映射优化:
epoll在内核和用户空间之间使用了mmap等技术,极大地减少了数据拷贝的开销。
- 分离关注点:
✅ 总结优势:
- 无连接数上限(受限于系统资源)
- O(1) 的事件检测效率
- 极低的 CPU 和内存开销
四、Redis 源码中的 IO 多路复用
Redis 并没有直接使用裸的 epoll API,而是对其进行了一层精巧的抽象,以兼容不同操作系统的多路复用机制(如 BSD 的 kqueue)。
4.1 核心抽象:aeEventLoop
Redis 定义了一个名为 aeEventLoop 的结构体,作为其事件循环的核心。
// server.h
typedef struct aeEventLoop {
int maxfd; /* highest file descriptor currently registered */
int setsize; /* max number of file descriptors tracked */
long long timeEventNextId;
aeFileEvent *events; /* Registered events */ // 存储所有注册的文件事件
aeFiredEvent *fired; /* Fired events */ // 存储已触发的事件
// ... 其他字段,如时间事件等
} aeEventLoop;
4.2 封装不同后端
Redis 通过条件编译,为不同的系统选择最优的后端:
// ae.c
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c" // Linux
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c" // BSD/macOS
#else
#include "ae_select.c" // fallback
#endif
#endif
#endif
4.3 ae_epoll.c 的关键实现
让我们看看 ae_epoll.c 中的核心函数是如何工作的:
aeApiCreate:调用epoll_create创建 epoll 实例。aeApiAddEvent:调用epoll_ctl(EPOLL_CTL_ADD/MOD)将新的 fd 及其关心的事件(EPOLLIN/EPOLLOUT)注册到 epoll 实例中。aeApiPoll:这是最关键的函数,它封装了epoll_wait。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; eventLoop->fired[j].mask = state->events[j].events & EPOLLIN ? AE_READABLE : 0 | state->events[j].events & EPOLLOUT ? AE_WRITABLE : 0; } return numevents; }
4.4 Redis 主循环
Redis 的主事件循环 aeMain 非常简洁:
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// 处理一些定时任务
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
// 核心:等待并处理 IO 事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
aeProcessEvents 内部最终会调用 aeApiPoll(即 epoll_wait),拿到就绪事件后,再逐个调用对应的事件处理器(如 acceptTcpHandler 处理新连接,readQueryFromClient 处理客户端请求)。
五、结语
感谢您的阅读!如果你有任何疑问或想要分享的经验,请在评论区留言交流!
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)