Redis源码探究系列—epoll、kqueue 在 Redis 中的封装实现(下)

欢迎各位同学关注我哦~
在这个 AI 喧嚣的时代
不忘初心,戒骄戒躁,认真沉淀
Deepincode

上一篇文章中,我们详细分析了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 家族kqueue2000年通用事件框架,支持多种事件源
Linuxepoll2002年 (2.5.44)专注于文件描述符I/O事件
Solaris/dev/poll2000年基于设备的轮询机制
Solarisevent ports2003年事件端口框架

那么为什么Linux要选择epoll而非kqueue呢,主要有一下几个原因:

  1. 许可证:kqueue的BSD许可证与GPL兼容,但Linux社区倾向于自主开发
  2. 设计理念:Linux团队更倾向于"做一件事并做好"的Unix哲学
  3. 性能: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;
  • kqfdkqueue() 返回的kqueue实例文件描述符
  • events:预分配的事件数组,用于存放 kevent() 返回的触发事件

与epoll的对比:

项目epollkqueue
实例fdepfd (epoll_create)kqfd (kqueue)
事件结构体struct epoll_eventstruct kevent
事件数组state->eventsstate->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;
}

关键点

  1. 分离注册:kqueue中读事件和写事件是独立的过滤器,需要分别注册
  2. EV_ADD:添加事件标志,如果事件已存在则修改
  3. kevent调用
    • changelistnchanges:要注册/修改的事件列表和数量
    • eventlistnevents:接收触发事件的缓冲区和大小(为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);
    }
}

关键点

  1. EV_DELETE:删除事件标志
  2. 分别删除:读事件和写事件需要分别删除

与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;
}

关键点

  1. 超时转换timeval(微秒)→ timespec(纳秒)
  2. 过滤器判断:通过 e->filter 判断是读事件还是写事件
  3. 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设计对比

操作epollkqueue
创建实例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_READEVFILT_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 性能对比

指标epollkqueue
注册事件一次系统调用注册读写两次系统调用注册读写
等待事件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 抽象层的优势

  1. 跨平台:同一份上层代码可在Linux、BSD、macOS上运行
  2. 可扩展:新增多路复用机制只需实现抽象层接口
  3. 可测试:可以使用简单的select实现进行测试
  4. 性能优化:每个平台可使用最优的原生机制

五、完整调用流程图

在这里插入图片描述

六、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.cae_kqueue.c
实例创建epoll_create(1024)kqueue()
事件结构体struct epoll_eventstruct 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/OUTe->filter == EVFILT_READ/WRITE

通过这两篇文章的分析,我们可以看到Redis如何优雅地封装不同的I/O多路复用机制,实现跨平台的高性能网络通信。

欢迎各位同学关注我哦~
在这个 AI 喧嚣的时代
不忘初心,戒骄戒躁,认真沉淀
Deepincode
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值