|
欢迎各位同学关注我哦~
在这个 AI 喧嚣的时代 不忘初心,戒骄戒躁,认真沉淀 |
|
在之前的文章中我们分析过Redis事件循环框架,了解过aeEventLoop 的整体设计,其中 apidata 字段用于存储底层多路复用机制的私有数据。
Redis通过统一的抽象层,屏蔽了不同操作系统I/O多路复用接口的差异:
| 机制 | 操作系统 | 性能 |
|---|---|---|
| epoll | Linux | 高 |
| kqueue | BSD/macOS | 高 |
| evport | Solaris | 高 |
| select | 跨平台 | 低(兜底方案) |
本文将深入剖析Redis对 epoll 的封装实现,下一篇文章将分析 kqueue 的封装。
一、多路复用机制的选择策略
1.1 编译时选择
Redis在编译时根据操作系统自动选择最优的多路复用机制:
// src/ae.c:49-61
#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
编译时按性能从高到低的顺序检测系统支持的多路复用机制,选择第一个可用的:首先是Solaris的evport,其次是Linux的epoll,然后是BSD/macOS的kqueue,
最后是作为兜底方案的select。
1.2 统一的API抽象
Redis定义了一套统一的多路复用API,每种实现都遵循相同的接口规范:
// 以下是需要实现的统一接口(内部使用)
static int aeApiCreate(aeEventLoop *eventLoop); // 创建多路复用实例
static int aeApiResize(aeEventLoop *eventLoop, int setsize); // 调整容量
static void aeApiFree(aeEventLoop *eventLoop); // 释放资源
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask); // 注册事件
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask); // 删除事件
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp); // 等待事件
static char *aeApiName(void); // 返回机制名称
二、epoll基础知识
epoll是Linux 2.6内核引入的I/O多路复用机制,相比传统的select/poll,它在处理大量并发连接时有显著的性能优势。
2.1 epoll的核心优势
相比传统的select/poll,epoll具有以下优势:
| 特性 | select/poll | epoll |
|---|---|---|
| fd数量限制 | 有限制(FD_SETSIZE,默认1024) | 几乎无限制 |
| 效率 | O(n)遍历所有fd | O(1)只返回就绪的fd |
| 内存拷贝 | 每次调用需要拷贝 | 共享内存,无需拷贝 |
| 触发模式 | 仅水平触发 | 支持边缘触发(ET) |
2.2 epoll的三个核心系统调用
// 创建epoll实例,返回epoll文件描述符
int epoll_create(int size);
// 管理epoll实例中的文件描述符(添加、修改、删除)
int epoll_ctl(int epfd, int op, int fd, struct epoll_event *event);
// 等待事件发生
int epoll_wait(int epfd, struct epoll_event *events, int maxevents, int timeout);
2.3 epoll_event结构体
epoll_event 是epoll机制中用于描述事件的核心结构体:
struct epoll_event {
uint32_t events; // 事件类型(EPOLLIN、EPOLLOUT 等)
epoll_data_t data; // 用户数据(可存储fd或指针)
};
typedef union epoll_data {
void *ptr;
int fd;
uint32_t u32;
uint64_t u64;
} epoll_data_t;
events 是一个位掩码字段,用于表示关注的事件类型。最常用的是 EPOLLIN(可读)和 EPOLLOUT(可写),
此外还有 EPOLLERR和 EPOLLHUP等事件。通过按位或操作可以同时监听多种事件。还有一个特殊的 EPOLLET 标志用于启用边缘触发模式,
不过Redis并没有使用这个特性,它采用的是默认的水平触发模式。
data 用于存储用户自定义数据。最常见的用法是 data.fd 存储文件描述符,或者 data.ptr 存储一个指针指向更复杂的结构体。
Redis选择了前者,因为它的 aeEventLoop 中已经维护了一个以fd为索引的 events 数组,只需要从epoll拿到触发事件的fd,就能快速定位到对应的事件处理信息。
三、Redis中epoll的封装实现
3.1 数据结构定义
Redis使用 aeApiState 结构体封装epoll相关状态:
// src/ae_epoll.c:34-37
typedef struct aeApiState {
int epfd; // epoll实例的文件描述符
struct epoll_event *events; // 用于接收epoll_wait返回的事件数组
} aeApiState;
结构体很简单,只有两个字段:epfd 保存 epoll_create 返回的epoll实例文件描述符,
events 则是预分配的事件数组,用于接收 epoll_wait 返回的就绪事件。预先分配好这个数组可以避免每次调用 epoll_wait 时临时分配内存。
3.2 创建epoll实例 aeApiCreate
// src/ae_epoll.c:39-56
static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState)); // 分配状态结构体
if (!state) return -1;
// 预分配事件数组,大小为setsize
// setsize决定了Redis能处理的最大fd数量
state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
if (!state->events) {
zfree(state);
return -1;
}
// 创建epoll实例
// 参数size在Linux 2.6.8后已被忽略,但必须>0
state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */
if (state->epfd == -1) {
// 创建失败,释放已分配的资源
zfree(state->events);
zfree(state);
return -1;
}
// 将epoll状态保存到事件循环的apidata字段
eventLoop->apidata = state;
return 0;
}
整个函数的逻辑比较清晰:先分配 aeApiState 结构体,然后预分配 epoll_event 数组,数组大小由 eventLoop->setsize 决定,这个值对应Redis能处理的最大文件描述符数量。
接着调用 epoll_create 创建epoll实例,参数1024在Linux 2.6.8以后已经被忽略,所以只需要传一个大于0的值即可。
最后将状态结构体保存到 eventLoop->apidata,完成初始化。
3.3 调整容量 aeApiResize
// src/ae_epoll.c:58-63
static int aeApiResize(aeEventLoop *eventLoop, int setsize) {
aeApiState *state = eventLoop->apidata;
// 重新分配事件数组的大小
// 当Redis需要动态调整最大连接数时调用
state->events = zrealloc(state->events, sizeof(struct epoll_event)*setsize);
return 0;
}
这个函数用于动态调整事件数组的大小。当通过 CONFIG SET maxclients 修改最大客户端连接数时,
会触发容量调整,对应的 setsize 也会变化,此时需要重新分配 events 数组以适应新的容量。
3.4 释放资源 aeApiFree
// src/ae_epoll.c:65-71
static void aeApiFree(aeEventLoop *eventLoop) {
aeApiState *state = eventLoop->apidata;
close(state->epfd); // 关闭epoll实例文件描述符
zfree(state->events); // 释放事件数组
zfree(state); // 释放状态结构体
}
3.5 注册事件 aeApiAddEvent
// src/ae_epoll.c:73-88
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee = {0};
// 判断操作类型:如果该fd已有事件注册,则使用MOD;否则使用ADD
int op = eventLoop->events[fd].mask == AE_NONE ?
EPOLL_CTL_ADD : EPOLL_CTL_MOD;
ee.events = 0;
// 合并已有的事件类型(MOD操作时保留之前注册的事件)
mask |= eventLoop->events[fd].mask; /* Merge old events */
// 将 Redis 的事件类型转换为epoll的事件类型
if (mask & AE_READABLE) ee.events |= EPOLLIN; // 可读事件
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT; // 可写事件
ee.data.fd = fd; // 将fd存储在epoll_event.data中
// 调用epoll_ctl注册或修改事件监听
if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
return 0;
}
这个函数的关键在于判断该使用 EPOLL_CTL_ADD 还是 EPOLL_CTL_MOD。如果这个fd之前没有任何事件注册(mask == AE_NONE),
就使用ADD操作;如果已经有事件在监听,就使用MOD操作来修改。MOD操作时要注意合并已有的事件,比如一个fd已经注册了可读事件,
现在又要注册可写事件,就需要把两种事件合并后一起设置,否则会覆盖掉之前的监听。
事件类型转换也很直观,Redis的 AE_READABLE 对应 epoll 的 EPOLLIN,AE_WRITABLE 对应 EPOLLOUT。
事件注册示例:
假设fd=5当前无任何事件注册:
1. 调用aeApiAddEvent(eventLoop, 5, AE_READABLE)
2. op = EPOLL_CTL_ADD(因为mask == AE_NONE)
3. ee.events = EPOLLIN
4. 调用epoll_ctl(epfd, EPOLL_CTL_ADD, 5, &ee)
假设fd=5已注册AE_READABLE,现在要添加AE_WRITABLE:
1. 调用aeApiAddEvent(eventLoop, 5, AE_WRITABLE)
2. op = EPOLL_CTL_MOD(因为mask != AE_NONE)
3. ee.events = EPOLLIN | EPOLLOUT(合并已有事件)
4. 调用 epoll_ctl(epfd, EPOLL_CTL_MOD, 5, &ee)
3.6 删除事件 aeApiDelEvent
// src/ae_epoll.c:90-106
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee = {0}; /* avoid valgrind warning */
// 计算删除后剩余的事件类型
int mask = eventLoop->events[fd].mask & (~delmask);
ee.events = 0;
// 重新构建 epoll 事件掩码
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.fd = fd;
if (mask != AE_NONE) {
// 还有其他事件,使用MOD操作更新
epoll_ctl(state->epfd,EPOLL_CTL_MOD,fd,&ee);
} else {
// 所有事件都已删除,从epoll中移除该fd
/* Note, Kernel < 2.6.9 requires a non null event pointer even for
* EPOLL_CTL_DEL. */
epoll_ctl(state->epfd,EPOLL_CTL_DEL,fd,&ee);
}
}
删除事件时需要考虑一个问题:这个fd可能同时注册了可读和可写事件,我们只想删除其中一个。
所以函数首先计算删除后剩余的事件类型,用位运算 mask & (~delmask) 来实现。如果删除后还有剩余事件,
就用MOD操作更新;如果全部删完了,就用DEL操作把这个fd从epoll中移除。
注意内核2.6.9之前的版本要求DEL 操作时event参数不能为NULL,所以这里即使删除也传了一个有效的 ee 指针。
事件删除示例:
假设 fd=5已注册AE_READABLE | AE_WRITABLE:
场景1:删除AE_WRITABLE
1. mask = (AE_READABLE | AE_WRITABLE) & (~AE_WRITABLE) = AE_READABLE
2. ee.events = EPOLLIN
3. 调用epoll_ctl(epfd, EPOLL_CTL_MOD, 5, &ee)
场景2:删除AE_READABLE | AE_WRITABLE(全部删除)
1. mask = (AE_READABLE | AE_WRITABLE) & (~(AE_READABLE | AE_WRITABLE)) = AE_NONE
2. 调用epoll_ctl(epfd, EPOLL_CTL_DEL, 5, &ee)
3.7 等待事件 aeApiPoll
// src/ae_epoll.c:108-131
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, numevents = 0;
// 调用epoll_wait等待事件发生
// 参数:epoll实例fd、事件数组、数组大小、超时时间(毫秒)
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;
// 遍历所有触发的事件
for (j = 0; j < numevents; j++) {
int mask = 0;
struct epoll_event *e = state->events+j;
// 将 epoll 事件类型转换回Redis事件类型
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;
// 将触发的事件信息填充到fired数组
eventLoop->fired[j].fd = e->data.fd;
eventLoop->fired[j].mask = mask;
}
}
// 返回触发的事件数量
return numevents;
}
这是事件循环的核心函数。它调用 epoll_wait 阻塞等待事件发生,超时参数 tvp 为NULL时表示无限等待,
否则需要把秒和微秒转换成毫秒传给epoll。当有事件返回时,遍历所有就绪的事件,把epoll的事件类型转换回Redis内部的类型,
然后填充到 eventLoop->fired 数组中供上层使用。
值得注意的是对 EPOLLERR 和 EPOLLHUP 的处理。这两个异常事件被标记为可写事件,这样上层的写回调函数就能被触发去处理异常情况。
这是一种巧妙的设计,避免了单独处理异常事件的复杂性。
3.8 获取机制名称 aeApiName
// src/ae_epoll.c:133-135
static char *aeApiName(void) {
return "epoll";
}
用于日志输出和调试信息。
四、epoll封装的完整调用链

五、Redis对epoll的优化点
5.1 预分配事件数组
state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
在创建epoll实例时就预先分配好事件数组,避免每次调用 epoll_wait 时临时分配内存。对于Redis这种高频调用I/O多路复用的服务来说,
减少内存分配次数对性能有实际意义。
5.2 事件合并策略
mask |= eventLoop->events[fd].mask; /* Merge old events */
添加新事件时,先查询该fd已注册的事件,然后合并后再设置。这样即使一个fd需要分别注册可读和可写事件,也不需要调用两次 epoll_ctl,减少系统调用次数。
5.3 使用水平触发(LT)
Redis使用epoll的默认触发模式——水平触发(Level Triggered),而非边缘触发(Edge Triggered)。
水平触发的特点是只要fd处于可读或可写状态,epoll_wait 就会持续通知。相比之下,边缘触发只在状态变化时通知一次,必须一次性把数据读完或写完,否则可能遗漏事件。
Redis选择水平触发的原因很简单:编程模型更直观,可以灵活控制每次读写的数据量,配合非阻塞I/O使用也不会有阻塞风险。
边缘触发虽然理论上性能更高,但编程复杂度也随之增加,对于Redis这种单线程事件驱动的架构来说,水平触发已经足够高效。
5.4 动态容量调整
static int aeApiResize(aeEventLoop *eventLoop, int setsize) {
aeApiState *state = eventLoop->apidata;
state->events = zrealloc(state->events, sizeof(struct epoll_event)*setsize);
return 0;
}
Redis支持运行时通过 CONFIG SET maxclients 调整最大连接数,事件数组的大小也需要相应调整。这个函数使得Redis无需重启就能根据负载情况动态调整容量。
六、总结
本文详细分析了Redis对epoll的封装实现,核心要点如下:
| API | epoll系统调用 | 功能 |
|---|---|---|
aeApiCreate | epoll_create | 创建epoll实例 |
aeApiAddEvent | epoll_ctl(ADD/MOD) | 注册/修改事件 |
aeApiDelEvent | epoll_ctl(MOD/DEL) | 删除事件 |
aeApiPoll | epoll_wait | 等待事件触发 |
aeApiFree | close(epfd) | 释放资源 |
下一篇文章将分析Redis对 kqueue 的封装实现,展示Redis是如何在BSD/macOS系统上实现高性能I/O多路复用。
|
欢迎各位同学关注我哦~
在这个 AI 喧嚣的时代 不忘初心,戒骄戒躁,认真沉淀 |
|
&spm=1001.2101.3001.5002&articleId=160062116&d=1&t=3&u=ee916cf9937346939c25d709b22413fd)
368

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



