|
欢迎各位同学关注我哦~
在这个 AI 喧嚣的时代 不忘初心,戒骄戒躁,认真沉淀 | |
在上一篇文章中,我们详细分析了Redis 对 epoll 的封装实现。本文将继续分析Redis对 kqueue 的封装,这是BSD和macOS系统上的高性能I/O多路复用机制。
kqueue与epoll设计理念相似但API差异较大,Redis通过统一的抽象层屏蔽了这些差异,使得上层代码可以无缝运行在不同操作系统上。
一、kqueue基础知识
1.1 kqueue的核心优势
kqueue是由 Jonathan Lemon 设计并实现的可扩展事件通知机制,于 2000 年 随FreeBSD 4.1首次发布。它最初是为了解决传统 select()/poll() 在高并发场景下的性能瓶颈而设计的。kqueue的设计理念是提供一个统一、高效、可扩展的事件通知框架。
历史背景
在kqueue出现之前,BSD系统上的事件通知主要依赖 select() 和 poll(),它们存在以下问题:
| 问题 | 说明 |
|---|---|
| O(n) 复杂度 | 每次调用需要遍历所有已注册的文件描述符 |
| 重复传递 | 每次调用都需要将完整的 fd集合从用户态拷贝到内核态 |
| 单一事件源 | 只能监听文件描述符,无法监听信号、进程状态等 |
| 扩展性差 | 随着并发连接数增加,性能急剧下降 |
kqueue的设计目标是创建一个通用的内核事件队列,它不仅能处理传统的文件描述符I/O事件,还能统一处理信号、进程状态变化、定时器、文件系统变化等多种事件源。
这种设计使得应用程序只需维护一个事件循环,就能响应所有类型的异步事件。
系统支持
kqueue并未被Linux采用。Linux选择了不同的技术路线:
| 系统 | 事件机制 | 引入时间 | 设计理念 |
|---|---|---|---|
| BSD 家族 | kqueue | 2000年 | 通用事件框架,支持多种事件源 |
| Linux | epoll | 2002年 (2.5.44) | 专注于文件描述符I/O事件 |
| Solaris | /dev/poll | 2000年 | 基于设备的轮询机制 |
| Solaris | event ports | 2003年 | 事件端口框架 |
那么为什么Linux要选择epoll而非kqueue呢,主要有一下几个原因:
- 许可证:kqueue的BSD许可证与GPL兼容,但Linux社区倾向于自主开发
- 设计理念:Linux团队更倾向于"做一件事并做好"的Unix哲学
- 性能:epoll在纯文件描述符场景下可能有更简洁的实现
1.2 kqueue的核心系统调用
// 创建kqueue实例,返回文件描述符
int kqueue(void);
// 注册/修改/删除事件
int kevent(int kq, const struct kevent *changelist, int nchanges,
struct kevent *eventlist, int nevents,
const struct timespec *timeout);
kqueue()创建一个内核事件队列,返回其文件描述符kevent()既可以注册事件,也可以等待事件,通过不同参数组合实现
1.3 kevent结构体
struct kevent {
uintptr_t ident; // 事件标识符(通常是fd)
short filter; // 事件过滤器(EVFILT_READ、EVFILT_WRITE等)
u_short flags; // 事件动作(EV_ADD、EV_DELETE、EV_ENABLE等)
u_int fflags; // 过滤器特定标志
intptr_t data; // 过滤器特定数据
void *udata; // 用户数据指针
};
// 用于初始化kevent结构体的宏
EV_SET(&kev, ident, filter, flags, fflags, data, udata);
常用过滤器:
| 过滤器 | 说明 |
|---|---|
EVFILT_READ | 文件描述符可读 |
EVFILT_WRITE | 文件描述符可写 |
EVFILT_TIMER | 定时器 |
EVFILT_SIGNAL | 信号 |
EVFILT_VNODE | 文件系统事件 |
常用标志有以下几个:
| 标志 | 说明 |
|---|---|
EV_ADD | 添加事件 |
EV_DELETE | 删除事件 |
EV_ENABLE | 启用事件 |
EV_DISABLE | 禁用事件(暂不触发) |
EV_ONESHOT | 触发后自动删除 |
EV_CLEAR | 触发后清除状态 |
二、Redis中kqueue的封装实现
2.1 数据结构定义
// src/ae_kqueue.c:36-39
typedef struct aeApiState {
int kqfd; // kqueue实例的文件描述符
struct kevent *events; // 用于接收kevent返回的事件数组
} aeApiState;
kqfd:kqueue()返回的kqueue实例文件描述符events:预分配的事件数组,用于存放kevent()返回的触发事件
与epoll的对比:
| 项目 | epoll | kqueue |
|---|---|---|
| 实例fd | epfd (epoll_create) | kqfd (kqueue) |
| 事件结构体 | struct epoll_event | struct kevent |
| 事件数组 | state->events | state->events |
2.2 创建kqueue实例 aeApiCreate
// src/ae_kqueue.c:41-58
static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState)); // 分配状态结构体
if (!state) return -1;
// 预分配事件数组,大小为setsize
// 每个事件占用一个kevent结构体
state->events = zmalloc(sizeof(struct kevent)*eventLoop->setsize);
if (!state->events) {
zfree(state);
return -1;
}
// 创建kqueue实例
// kqueue()不需要参数,比epoll_create更简洁
state->kqfd = kqueue();
if (state->kqfd == -1) {
// 创建失败,释放已分配的资源
zfree(state->events);
zfree(state);
return -1;
}
// 将kqueue状态保存到事件循环的apidata字段
eventLoop->apidata = state;
return 0;
}
与epoll的对比:
// epoll版本
state->epfd = epoll_create(1024); // 参数已被忽略,但仍需传入
// kqueue版本
state->kqfd = kqueue(); // 无需参数
2.3 调整容量 aeApiResize
// src/ae_kqueue.c:60-65
static int aeApiResize(aeEventLoop *eventLoop, int setsize) {
aeApiState *state = eventLoop->apidata;
// 重新分配事件数组的大小
state->events = zrealloc(state->events, sizeof(struct kevent)*setsize);
return 0;
}
2.4 释放资源 aeApiFree
// src/ae_kqueue.c:67-73
static void aeApiFree(aeEventLoop *eventLoop) {
aeApiState *state = eventLoop->apidata;
close(state->kqfd); // 关闭kqueue实例文件描述符
zfree(state->events); // 释放事件数组
zfree(state); // 释放状态结构体
}
2.5 注册事件 aeApiAddEvent
// src/ae_kqueue.c:75-88
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct kevent ke;
// kqueue需要分别注册读事件和写事件
// 不能像epoll那样通过一次调用同时注册读写事件
if (mask & AE_READABLE) {
// 注册可读事件
// EV_SET宏用于初始化kevent结构体
// 参数:kevent指针, ident(fd), filter, flags, fflags, data, udata
EV_SET(&ke, fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
// kevent调用:kqfd, changelist, nchanges, eventlist, nevents, timeout
// 这里只注册事件,不等待事件(nchanges=1, nevents=0)
if (kevent(state->kqfd, &ke, 1, NULL, 0, NULL) == -1) return -1;
}
if (mask & AE_WRITABLE) {
// 注册可写事件
EV_SET(&ke, fd, EVFILT_WRITE, EV_ADD, 0, 0, NULL);
if (kevent(state->kqfd, &ke, 1, NULL, 0, NULL) == -1) return -1;
}
return 0;
}
关键点:
- 分离注册:kqueue中读事件和写事件是独立的过滤器,需要分别注册
- EV_ADD:添加事件标志,如果事件已存在则修改
- kevent调用:
changelist和nchanges:要注册/修改的事件列表和数量eventlist和nevents:接收触发事件的缓冲区和大小(为0时立即返回)timeout:超时时间,因nevents=0,kevent会立即返回,此参数无实际意义
与epoll的对比:
// epoll版本:通过mask合并,一次调用注册读写事件
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
epoll_ctl(state->epfd, op, fd, &ee);
// kqueue版本:需要分别注册
if (mask & AE_READABLE) {
EV_SET(&ke, fd, EVFILT_READ, EV_ADD, 0, 0, NULL);
kevent(state->kqfd, &ke, 1, NULL, 0, NULL);
}
if (mask & AE_WRITABLE) {
EV_SET(&ke, fd, EVFILT_WRITE, EV_ADD, 0, 0, NULL);
kevent(state->kqfd, &ke, 1, NULL, 0, NULL);
}
2.6 删除事件 aeApiDelEvent
// src/ae_kqueue.c:90-102
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct kevent ke;
// kqueue删除事件同样需要分别处理读和写
if (mask & AE_READABLE) {
// 删除可读事件
// EV_DELETE标志表示删除事件
EV_SET(&ke, fd, EVFILT_READ, EV_DELETE, 0, 0, NULL);
kevent(state->kqfd, &ke, 1, NULL, 0, NULL);
}
if (mask & AE_WRITABLE) {
// 删除可写事件
EV_SET(&ke, fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL);
kevent(state->kqfd, &ke, 1, NULL, 0, NULL);
}
}
关键点:
- EV_DELETE:删除事件标志
- 分别删除:读事件和写事件需要分别删除
与epoll的对比:
// epoll版本:需要计算剩余事件,决定MOD还是 DEL
mask = eventLoop->events[fd].mask & (~mask);
if (mask != AE_NONE) {
epoll_ctl(state->epfd, EPOLL_CTL_MOD, fd, &ee);
} else {
epoll_ctl(state->epfd, EPOLL_CTL_DEL, fd, &ee);
}
// kqueue 版本:直接删除指定类型的事件,无需关心剩余事件
if (mask & AE_READABLE) {
EV_SET(&ke, fd, EVFILT_READ, EV_DELETE, 0, 0, NULL);
kevent(state->kqfd, &ke, 1, NULL, 0, NULL);
}
kqueue 的优势:删除操作更简单,不需要查询当前注册的事件状态。
2.7 等待事件 aeApiPoll
// src/ae_kqueue.c:104-134
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, numevents = 0;
// 转换超时时间为 timespec 结构
if (tvp != NULL) {
struct timespec timeout;
timeout.tv_sec = tvp->tv_sec;
timeout.tv_nsec = tvp->tv_usec * 1000; // 微秒转纳秒
// 调用 kevent 等待事件
// changelist=NULL, nchanges=0 表示仅等待,不注册事件
// eventlist=state->events, nevents=setsize 接收触发的事件
retval = kevent(state->kqfd, NULL, 0, state->events, eventLoop->setsize, &timeout);
} else {
// tvp 为 NULL 表示无限等待
retval = kevent(state->kqfd, NULL, 0, state->events, eventLoop->setsize, NULL);
}
if (retval > 0) {
int j;
numevents = retval;
// 遍历所有触发的事件
for (j = 0; j < numevents; j++) {
int mask = 0;
struct kevent *e = state->events+j;
// 根据过滤器类型确定事件类型
if (e->filter == EVFILT_READ) mask |= AE_READABLE;
if (e->filter == EVFILT_WRITE) mask |= AE_WRITABLE;
// 将触发的事件信息填充到fired数组
eventLoop->fired[j].fd = e->ident; // ident存储的是fd
eventLoop->fired[j].mask = mask;
}
}
return numevents;
}
关键点:
- 超时转换:
timeval(微秒)→timespec(纳秒) - 过滤器判断:通过
e->filter判断是读事件还是写事件 - fd获取:通过
e->ident获取触发事件的文件描述符
与epoll的对比:
// epoll版本:通过events字段判断事件类型
if (e->events & EPOLLIN) mask |= AE_READABLE;
if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
// kqueue版本:通过filter字段判断事件类型
if (e->filter == EVFILT_READ) mask |= AE_READABLE;
if (e->filter == EVFILT_WRITE) mask |= AE_WRITABLE;
2.8 获取机制名称 aeApiName
// src/ae_kqueue.c:136-138
static char *aeApiName(void) {
return "kqueue";
}
三、epoll与kqueue的详细对比
3.1 API设计对比
| 操作 | epoll | kqueue |
|---|---|---|
| 创建实例 | epoll_create(size) | kqueue() |
| 注册事件 | epoll_ctl(epfd, EPOLL_CTL_ADD, fd, &ev) | kevent(kqfd, &ke, 1, NULL, 0, NULL) |
| 修改事件 | epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ev) | kevent(kqfd, &ke, 1, NULL, 0, NULL) |
| 删除事件 | epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL) | kevent(kqfd, &ke, 1, NULL, 0, NULL) |
| 等待事件 | epoll_wait(epfd, events, maxevents, timeout_ms) | kevent(kqfd, NULL, 0, events, maxevents, &timeout) |
3.2 事件注册对比
epoll:
- 一个fd对应一个
epoll_event - 读写事件通过位掩码组合(
EPOLLIN | EPOLLOUT) - 添加新事件需要检查是否已存在(决定 ADD 还是 MOD)
kqueue:
- 一个fd对应多个
kevent(每种过滤器一个) - 读写事件通过不同过滤器区分(
EVFILT_READ、EVFILT_WRITE) - 添加事件直接使用
EV_ADD,已存在则修改
3.3 事件删除对比
epoll:
// 删除特定事件需要:查询当前状态 → 计算剩余事件 → 决定MOD/DEL
mask = eventLoop->events[fd].mask & (~mask);
if (mask != AE_NONE) {
// 还有其他事件,使用MOD
epoll_ctl(epfd, EPOLL_CTL_MOD, fd, &ee);
} else {
// 没有事件了,使用DEL
epoll_ctl(epfd, EPOLL_CTL_DEL, fd, NULL);
}
kqueue:
// 直接删除指定类型的事件,无需查询当前状态
EV_SET(&ke, fd, EVFILT_READ, EV_DELETE, 0, 0, NULL);
kevent(kqfd, &ke, 1, NULL, 0, NULL);
3.4 性能对比
| 指标 | epoll | kqueue |
|---|---|---|
| 注册事件 | 一次系统调用注册读写 | 两次系统调用注册读写 |
| 等待事件 | O(1) | O(1) |
| 批量操作 | 不支持 | 支持批量注册/删除 |
| 事件类型 | 文件描述符 | 文件描述符、信号、定时器等 |
注意:虽然kqueue注册读写事件需要两次系统调用,但Redis的典型使用场景中,连接建立时只注册读事件,写事件是按需注册的,所以影响很小。
四、Redis 的统一抽象层
4.1 抽象层接口定义
Redis定义了一组统一的内部接口,每种多路复用机制都需要实现:
// 所有实现必须提供的接口
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);
4.2 上层调用示例
// ae.c 中对抽象层的调用
// 创建事件循环
aeEventLoop *aeCreateEventLoop(int setsize) {
aeEventLoop *eventLoop = zmalloc(sizeof(aeEventLoop));
// ...
if (aeApiCreate(eventLoop) == -1) goto err; // 调用抽象层
// ...
}
// 注册文件事件
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
aeFileProc *proc, void *clientData) {
// ...
if (aeApiAddEvent(eventLoop, fd, mask) == -1) return AE_ERR; // 调用抽象层
// ...
}
// 处理事件
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
// ...
numevents = aeApiPoll(eventLoop, tvp); // 调用抽象层
// ...
}
4.3 抽象层的优势
- 跨平台:同一份上层代码可在Linux、BSD、macOS上运行
- 可扩展:新增多路复用机制只需实现抽象层接口
- 可测试:可以使用简单的select实现进行测试
- 性能优化:每个平台可使用最优的原生机制
五、完整调用流程图

六、select作为兜底方案
当系统不支持 epoll/kqueue/evport 时,Redis使用select作为兜底方案:
// src/ae_select.c
static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState));
if (!state) return -1;
// select使用fd_set来管理文件描述符
FD_ZERO(&state->rfds);
FD_ZERO(&state->wfds);
eventLoop->apidata = state;
return 0;
}
select的限制:
- 单个进程可监听的fd数量有限(FD_SETSIZE,默认1024)
- 每次调用需要重新设置fd_set
- 效率较低,需要遍历所有fd
适用场景:
- 不支持更高级机制的系统
- 测试和调试目的
七、与epoll的封装对比
| 项目 | epoll实现 | kqueue实现 |
|---|---|---|
| 头文件 | ae_epoll.c | ae_kqueue.c |
| 实例创建 | epoll_create(1024) | kqueue() |
| 事件结构体 | struct epoll_event | struct kevent |
| 注册读事件 | epoll_ctl(EPOLL_CTL_ADD/MOD) | kevent(EV_ADD, EVFILT_READ) |
| 注册写事件 | 同一次调用合并 | 单独调用 kevent(EV_ADD, EVFILT_WRITE) |
| 删除事件 | 需要MOD/DEL判断 | 直接 kevent(EV_DELETE) |
| 等待事件 | epoll_wait() | kevent() |
| 事件类型判断 | e->events & EPOLLIN/OUT | e->filter == EVFILT_READ/WRITE |
通过这两篇文章的分析,我们可以看到Redis如何优雅地封装不同的I/O多路复用机制,实现跨平台的高性能网络通信。
|
欢迎各位同学关注我哦~
在这个 AI 喧嚣的时代 不忘初心,戒骄戒躁,认真沉淀 | |
&spm=1001.2101.3001.5002&articleId=160090568&d=1&t=3&u=f4c5d04a47f9439cbba07cff77c6bf4b)
389

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



