[redis 源碼走讀] 事件 - 文件事件

redis 服務底層採用了異步事件管理(aeEventLoop):管理時間事件和文件事件。對於大量網絡文件描述符(fd)的事件管理,redis 建立在安裝系統對應的事件驅動基礎上(例如 Linux 的 epoll)。

  • 關於事件驅動,本章主要講述 Linux 系統的 epoll 事件驅動。
  • 關於事件處理,本章主要講述文件事件,時間事件可以參考帖子 [redis 源碼走讀] 事件 - 定時器

🔥文章來源:wenfh2020.com

1. 事件驅動

redis 根據安裝系統選擇對應的事件驅動。

// ae.c
/* Include the best multiplexing layer supported by this system.
 * The following should be ordered by performances, descending. */
#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

2. 異步事件管理

epoll 是異步事件驅動,上層邏輯操作和下層事件驅動要通過 fd 文件描述符串聯起來。異步事件管理(aeEventLoop),對 epoll 做了一些封裝,方便異步事件回調處理。

有關 epoll 工作流程,可以參考我的帖子:epoll 多路複用 I/O工作流程

redis 文件事件封裝

層次 描述
ae.c 關聯異步業務事件和 epoll 接口,處理 fd 對應事件邏輯。
ae_epoll.c 對 epoll 接口進行封裝,方便上層操作。
epoll Linux 內核多路複用 I/O 模型,主要爲了高效處理大批量文件描述符事件。

2.1. 數據結構

// ae.c

// 文件事件結構
typedef struct aeFileEvent {
    int mask; // 事件類型組合(one of AE_(READABLE|WRITABLE|BARRIER))
    aeFileProc *rfileProc; // 讀事件回調操作。
    aeFileProc *wfileProc; // 寫事件回調操作。
    void *clientData;      // 業務傳入的私有數據。方便回調使用。
} aeFileEvent;

// 就緒事件
typedef struct aeFiredEvent {
    int fd;   // 文件描述符。
    int mask; // 事件類型組合。
} aeFiredEvent;

// 事件管理結構
typedef struct aeEventLoop {
    int maxfd;   // 監控的最大文件描述符。
    int setsize; // 處理文件描述符個數。
    ...
    aeFileEvent *events; // 根據 fd 監聽事件。
    aeFiredEvent *fired; // 從內核取出的就緒事件。
    ...
} aeEventLoop;
結構 描述
aeEventLoop 文件事件和時間事件管理。
aeFileEvent 文件事件結構,方便異步回調邏輯調用。aeEventLoop 會創建一個 aeFileEvent 數組,數組下標是 fd,fd 對應 aeFileEvent 數據結構。
aeFiredEvent 從內核獲取的就緒事件。(例如 Linux 系統通過 epoll_wait 接口獲取就緒事件,每個事件分別存儲在 aeFiredEvent 數組中)

2.2. 創建事件管理對象

創建事件管理對象,對監控的文件數量設置了上限。

  • 文件監控上限配置。
# redis.conf
#
# Set the max number of connected clients at the same time. By default
# this limit is set to 10000 clients, however if the Redis server is not
# able to configure the process file limit to allow for the specified limit
# the max number of allowed clients is set to the current file limit
# minus 32 (as Redis reserves a few file descriptors for internal uses).
#
# Once the limit is reached Redis will close all the new connections sending
# an error 'max number of clients reached'.
#
# maxclients 10000
  • 創建事件管理對象。
#define CONFIG_MIN_RESERVED_FDS 32
#define CONFIG_FDSET_INCR (CONFIG_MIN_RESERVED_FDS+96)

// server.c
void initServer(void) {
    ...
    server.el = aeCreateEventLoop(server.maxclients+CONFIG_FDSET_INCR);
    ...
}

int main(int argc, char **argv) {
    ...
    initServer();
    ...
}

2.3. 事件處理流程

  • 循環處理事件
// server.c
int main(int argc, char **argv) {
    ...
    aeMain(server.el);
    ...
}

// ae.c
// 循環處理事件
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
    }
}
  • 添加事件,關聯 fd 事件與異步回調相關信息。
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData) {
    if (fd >= eventLoop->setsize) {
        errno = ERANGE;
        return AE_ERR;
    }
    aeFileEvent *fe = &eventLoop->events[fd];

    // 調用底層 epoll_ctl 註冊事件。
    if (aeApiAddEvent(eventLoop, fd, mask) == -1)
        return AE_ERR;
    fe->mask |= mask;
    if (mask & AE_READABLE) fe->rfileProc = proc;
    if (mask & AE_WRITABLE) fe->wfileProc = proc;
    fe->clientData = clientData;
    if (fd > eventLoop->maxfd)
        eventLoop->maxfd = fd;
    return AE_OK;
}
  • 刪除事件,刪除對應 fd 的事件。
void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask) {
    if (fd >= eventLoop->setsize) return;
    aeFileEvent *fe = &eventLoop->events[fd];
    if (fe->mask == AE_NONE) return;

    // 如果刪除的是寫事件,要把寫事件優先處理的事件也去掉,恢復優先處理讀事件,再處理寫事件邏輯。
    if (mask & AE_WRITABLE) mask |= AE_BARRIER;

    // 調用底層 epoll_ctl 修改刪除事件。
    aeApiDelEvent(eventLoop, fd, mask);
    fe->mask = fe->mask & (~mask);
    if (fd == eventLoop->maxfd && fe->mask == AE_NONE) {
        /* Update the max fd */
        int j;

        for (j = eventLoop->maxfd-1; j >= 0; j--)
            if (eventLoop->events[j].mask != AE_NONE) break;
        eventLoop->maxfd = j;
    }
}

2.4. 事件處理邏輯

文件事件處理邏輯,從內核取出就緒事件,根據事件的讀寫類型,分別進行回調處理相關業務邏輯。

// ae.c
int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
    ...
    // 多路複用接口,從內核取出就緒事件。
    numevents = aeApiPoll(eventLoop, tvp);
    ...
    for (j = 0; j < numevents; j++) {
        // 根據就緒事件 fd,取出對應的異步文件事件進行邏輯處理。
        aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
        int mask = eventLoop->fired[j].mask;
        int fd = eventLoop->fired[j].fd;
        int fired = 0; /* Number of events fired for current fd. */

        /* AE_BARRIER 表示優先可寫事件。正常情況,一般先讀後寫。
         * AE_BARRIER 使用場景,有興趣的朋友,可以查找源碼關鍵字:CONN_FLAG_WRITE_BARRIER
         * 理解這部分的邏輯。 */
        int invert = fe->mask & AE_BARRIER;

        if (!invert && fe->mask & mask & AE_READABLE) {
            fe->rfileProc(eventLoop,fd,fe->clientData,mask);
            fired++;
        }

        if (fe->mask & mask & AE_WRITABLE) {
            if (!fired || fe->wfileProc != fe->rfileProc) {
                fe->wfileProc(eventLoop,fd,fe->clientData,mask);
                fired++;
            }
        }

        if (invert && fe->mask & mask & AE_READABLE) {
            if (!fired || fe->wfileProc != fe->rfileProc) {
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);
                fired++;
            }
        }
        ...
    }
    ...
}

2.5. 獲取待處理事件

通過 epoll_wait 從系統內核取出就緒文件事件進行處理。

// ae_epoll.c
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/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;

            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;

            // 就緒事件和fd保存到 fired。
            eventLoop->fired[j].fd = e->data.fd;
            eventLoop->fired[j].mask = mask;
        }
    }
    return numevents;
}

3. 總結

  • redis 沒有使用第三方庫,實現跨平臺的異步事件驅動。對文件事件驅動封裝也比較簡潔高效。

4. 參考

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章