Redis 源碼簡潔剖析 09 - Reactor 模型

Reactor 模型

網絡服務器端,用了處理高併發網絡 IO請求的一種編程模型

處理 3 類事件:

  • 連接事件:客戶端→服務器的連接請求,對應服務端的連接事件
  • 寫事件:客戶端→服務器的讀請求,服務端處理後要寫回客戶端,對應服務端的寫事件
  • 讀事件:服務端要從客戶端讀取請求內容,對應服務端的讀事件

3 個關鍵角色:

  • acceptor:處理連接事件,接收連接、創建 handler
  • handler:處理讀寫事件
  • reactor:專門監聽和分配事件,連接請求 → acceptor、讀寫請求 → handler

事件驅動框架

事件驅動框架就是 Reactor 的具體實現。包括:

  • 事件初始化:創建要監聽的事件類型,及該類事件對應的 handler
  • 事件捕獲、分發和處理主循環
    • 捕獲發生的事件
    • 判斷事件類型
    • 根據事件類型,調用對應 handler 處理事件

Redis 如何實現 Reactor 模型

實現代碼:

  • 頭文件:ae.h
  • 實現:ae.c

事件的數據結構:aeFileEvent

Redis 的事件驅動框架定義了 2 類事件:

  • IO 事件
  • 時間事件

下面介紹 IO 事件 aeFileEvent 的數據結構:

/* File event structure */
typedef struct aeFileEvent {
    // 事件類型的掩碼,AE_(READABLE|WRITABLE|BARRIER)
    int mask;
    // AE_READABLE 事件的處理函數
    aeFileProc *rfileProc;
    // AE_WRITABLE 事件的處理函數
    aeFileProc *wfileProc;
    // 指向客戶端私有數據
    void *clientData;
} aeFileEvent;

主循環:aeMain 函數

是在 Redis 初始化時調用的,詳見 Redis 源碼簡潔剖析 07 - main 函數啓動

void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    // 循環調用
    while (!eventLoop->stop) {
        // 核心函數,處理事件的邏輯
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|
                                   AE_CALL_BEFORE_SLEEP|
                                   AE_CALL_AFTER_SLEEP);
    }
}

代碼非常簡單,就是循環調用 aeProcessEvents 函數。aeMain 是在 main 函數中被調用的:

// 事件驅動框架,循環處理各種觸發的事件
aeMain(server.el);
// 循環結束,刪除 eventLoop
aeDeleteEventLoop(server.el);

事件捕獲與分發:aeProcessEvents 函數

主體有 3 個 if 分支:

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;
 
    /* 若沒有事件處理,則立刻返回*/
    if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
    /*如果有 IO 事件發生,或者緊急的時間事件發生,則開始處理*/
    if (eventLoop->maxfd != -1 || ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
       …
    }
    /* 檢查是否有時間事件,若有,則調用 processTimeEvents 函數處理 */
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);
    /* 返回已經處理的文件或時間*/
    return processed; 
}

核心是第 2 個 if 語句:

// 有 IO 事件發生 || 緊急時間事件發生
if (eventLoop->maxfd != -1 ||
    ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
        ……
        // 調用 aeApiPoll 捕獲事件
        numevents = aeApiPoll(eventLoop, tvp);
        ……
}

aeApiPoll 函數如何捕獲事件?依賴於操作系統底層提供的 IO 多路複用機制,實現事件捕獲,檢查是否有新的連接、讀寫事件的發生。爲了適配不同的操作系統,Redis 對不同操作系統實現網絡 IO 多路複用函數,進行統一封裝,封裝後的代碼在 4 個文件中實現:

  • ae_epoll.c,對應 Linux 上的 IO 複用函數 epoll
  • ae_evport.c,對應 Solaris 上的 IO 複用函數 evport
  • ae_kqueue.c,對應 macOS 或 FreeBSD 上的 IO 複用函數 kqueue
  • ae_select.c,對應 Linux(或 Windows)的 IO 複用函數 select

ae_epoll.c 中 aeApiPoll 函數的實現,核心是調用了 epoll_wait 函數,並將 epoll 返回的事件信息保存起來。

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;

    // 調用 epoll_wait 獲取監聽到的事件
    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;

        // 獲取監聽到的事件數量
        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;
        }
    }
    return numevents;
}

在 Mac 上查看源碼,aeApiPoll 方法會進入 ae_kqueue.c 中:

static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;

    if (tvp != NULL) {
        struct timespec timeout;
        timeout.tv_sec = tvp->tv_sec;
        timeout.tv_nsec = tvp->tv_usec * 1000;
        retval = kevent(state->kqfd, NULL, 0, state->events, eventLoop->setsize,
                        &timeout);
    } else {
        retval = kevent(state->kqfd, NULL, 0, state->events, eventLoop->setsize,
                        NULL);
    }

    if (retval > 0) {
        int j;

        /* Normally we execute the read event first and then the write event.
         * When the barrier is set, we will do it reverse.
         * 
         * However, under kqueue, read and write events would be separate
         * events, which would make it impossible to control the order of
         * reads and writes. So we store the event's mask we've got and merge
         * the same fd events later. */
        for (j = 0; j < retval; j++) {
            struct kevent *e = state->events+j;
            int fd = e->ident;
            int mask = 0; 

            if (e->filter == EVFILT_READ) mask = AE_READABLE;
            else if (e->filter == EVFILT_WRITE) mask = AE_WRITABLE;
            addEventMask(state->eventsMask, fd, mask);
        }

        /* Re-traversal to merge read and write events, and set the fd's mask to
         * 0 so that events are not added again when the fd is encountered again. */
        numevents = 0;
        for (j = 0; j < retval; j++) {
            struct kevent *e = state->events+j;
            int fd = e->ident;
            int mask = getEventMask(state->eventsMask, fd);

            if (mask) {
                eventLoop->fired[numevents].fd = fd;
                eventLoop->fired[numevents].mask = mask;
                resetEventMask(state->eventsMask, fd);
                numevents++;
            }
        }
    }
    return numevents;
}

事件註冊:aeCreateFileEvent 函數

main 函數中調用了 createSocketAcceptHandler

if (createSocketAcceptHandler(&server.ipfd, acceptTcpHandler) != C_OK) {
    serverPanic("Unrecoverable error creating TCP socket accept handler.");
}

createSocketAcceptHandler 創建接收連接的 handler:

int createSocketAcceptHandler(socketFds *sfd, aeFileProc *accept_handler) {
    int j;

    for (j = 0; j < sfd->count; j++) {
        if (aeCreateFileEvent(server.el, sfd->fd[j], AE_READABLE, accept_handler,NULL) == AE_ERR) {
            /* Rollback */
            for (j = j-1; j >= 0; j--) aeDeleteFileEvent(server.el, sfd->fd[j], AE_READABLE);
            return C_ERR;
        }
    }
    return C_OK;
}

其主要是調用了 aeCreateFileEvent,aeCreateFileEvent 就是實現事件和處理函數註冊的核心函數。

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];

    // 核心
    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;
}

Linux 提供了 epoll_ctl API,用於增加新的觀察事件。而 Redis 在此基礎上,封裝了 aeApiAddEvent 函數,對 epoll_ctl 進行調用,註冊希望監聽的事件和相應的處理函數。

ae_epoll.c 中 aeApiAddEvent 實現如下:

static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
    aeApiState *state = eventLoop->apidata;
    struct epoll_event ee = {0};
    /* If the fd was already monitored for some event, we need a MOD
     * operation. Otherwise we need an ADD operation. */
    int op = eventLoop->events[fd].mask == AE_NONE ?
            EPOLL_CTL_ADD : EPOLL_CTL_MOD;

    ee.events = 0;
    mask |= eventLoop->events[fd].mask; /* Merge old events */
    if (mask & AE_READABLE) ee.events |= EPOLLIN;
    if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
    ee.data.fd = fd;
    // 增加新的觀察事件
    if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
    return 0;
}

註冊的函數 acceptTcpHandler 在 network.c 中:

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
    int cport, cfd, max = MAX_ACCEPTS_PER_CALL;
    char cip[NET_IP_STR_LEN];
    UNUSED(el);
    UNUSED(mask);
    UNUSED(privdata);

    // 每次處理 1000 個
    while(max--) {
        cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
        if (cfd == ANET_ERR) {
            if (errno != EWOULDBLOCK)
                serverLog(LL_WARNING,
                    "Accepting client connection: %s", server.neterr);
            return;
        }
        anetCloexec(cfd);
        serverLog(LL_VERBOSE,"Accepted %s:%d", cip, cport);
        acceptCommonHandler(connCreateAcceptedSocket(cfd),0,cip);
    }
}

總結

Redis 處理連接、客戶端請求是單線程的,但是這單個線程能夠處理上千個客戶端,就是因爲 Redis 是基於 Reactor 模型的。通過事件驅動框架,Redis 可以使用一個循環不斷捕獲、分發、處理客戶端產生的網絡連接、數據讀寫事件。當然這裏有一個前提,就是 Redis 幾乎所有數據讀取和處理都是在內存中操作的,服務端對單個客戶端的讀寫請求處理時間極短。

參考鏈接

Redis 源碼簡潔剖析系列

最簡潔的 Redis 源碼剖析系列文章

Java 編程思想-最全思維導圖-GitHub 下載鏈接,需要的小夥伴可以自取~

原創不易,希望大家轉載時請先聯繫我,並標註原文鏈接。

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