Valkey源码剖析(9):基于多路复用实现的文件事件

前面的文章说过,负责处理事件的ae.c/aeProcessEvents()函数会调用ae.c/aeApiPoll()函数以休眠并等待文件事件就绪:

int aeProcessEvents(aeEventLoop *eventLoop, int flags) {

    // ...

    numevents = aeApiPoll(eventLoop, tvp);

    // ...
}

Valkey的文件事件本质上就是对底层多路复用库的包装,而这里对aeApiPoll()函数的调用实际上就是对底层多路复用库类Poll函数的调用。

根据ae.c文件的定义,编译器在编译Valkey服务器的时候,将根据系统对多路复用库的支持,选出该系统能够使用的性能最优的多路复用库:

#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

以Linux系统为例,编译后的Valkey服务器通常将使用ae_epoll.c库。在这种情况下,对aeApiPoll()的调用将引发对ae_epoll.c/epoll_wait()函数的调用,而后者将在指定的时间内监听指定的文件描述符,并在它们变为可写或者可读的时候将其转化为相应的Valkey文件事件——aeApiPoll()函数的定义很好地说明了这一点:

// 休眠并等待事件就绪
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;

    // 在指定的时间内休眠,然后返回就绪的文件描述符数量
    retval = epoll_wait(state->epfd, state->events, eventLoop->setsize,
                        tvp ? (tvp->tv_sec * 1000 + (tvp->tv_usec + 999) / 1000) : -1);
    if (retval > 0) {
        int j;

        // 根据就绪的事件,设置eventLoop->fired[j]中的fd属性和mask属性,
        // 从而分别记录已就绪的文件描述符,以及它们就绪的事件(读还是写)
        numevents = retval;
        for (j = 0; j < numevents; j++) {
            int mask = 0;
            struct epoll_event *e = state->events + j;

            if (e->events & EPOLLIN) mask |= AE_READABLE;
            if (e->events & EPOLLOUT) mask |= AE_WRITABLE;
            if (e->events & EPOLLERR) mask |= AE_WRITABLE | AE_READABLE;
            if (e->events & EPOLLHUP) mask |= AE_WRITABLE | AE_READABLE;
            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    } else if (retval == -1 && errno != EINTR) {
        panic("aeApiPoll: epoll_wait, %s", strerror(errno));
    }

    return numevents;
}

以上就是Valkey通过多路复用库实现文件事件的核心逻辑。

黄健宏
2026.1.2