redisserver事件模型

晴天哥發表於2018-06-06

事件驅動

 redis伺服器是一個事件驅動的程式,內部需要處理兩類事件,一類是檔案事件(file event),一類是時間事件(time event),前者對應著處理各種io事件,後者對應著處理各種定時任務。
 印象中我們都知道redis是單執行緒服務,其實本質上的確就是這樣子的,上面說的file event 和 time event都是由單個執行緒驅動的,file event 底層其實是通過select/epoll等模型去執行驅動的,time event是通過內部維持一個定時任務列表來實現的。
 redis server的事件模型其實就是經典的NIO模型,底層通過select/epoll等機制實現非同步NIO,通過檢測到event到來後for迴圈實現序列處理。

事件註冊

server啟動流程

 redis server的main函式在啟動過程中按照以下三個步驟進行初始化並進入執行狀態:

  • 初始化伺服器配置
  • 載入配置檔案初始化配置(redis的監聽埠在這一步完成配置)
  • 建立並初始化伺服器的資料結構監聽埠等(完成埠的監聽,事件註冊等)
  • 迴圈處理事件(迴圈處理file event 和 time event)
int main(int argc, char **argv) {
    
    // 初始化伺服器
    initServerConfig();

    // 載入配置檔案, options 是前面分析出的給定選項
    loadServerConfig(configfile,options);

    // 建立並初始化伺服器資料結構
    initServer();

    // 執行事件處理器,一直到伺服器關閉為止
    aeSetBeforeSleepProc(server.el,beforeSleep);
    aeMain(server.el);

    // 伺服器關閉,停止事件迴圈
    aeDeleteEventLoop(server.el);

    return 0;
}

server初始化流程

 整個server初始化過程刪除了不相關的程式碼保留跟這部分文章相關的內容,整個初始化過程核心邏輯如下:

  • 建立監聽的socket用於accept連線
  • 註冊監聽socket到底層時間驅動如select/epoll當中(aeCreateFileEvent)
  • 建立時間事件(aeCreateTimeEvent)
void initServer() {
    // 開啟 TCP 監聽埠,用於等待客戶端的命令請求
    if (server.port != 0 &&
        listenToPort(server.port,server.ipfd,&server.ipfd_count) == REDIS_ERR)
        exit(1);


    // 為 serverCron() 建立時間事件
    if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
        redisPanic("Can`t create the serverCron time event.");
        exit(1);
    }

    // 為 TCP 連線關聯連線應答(accept)處理器,用於接受並應答客戶端的 connect() 呼叫
    for (j = 0; j < server.ipfd_count; j++) {
        if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE,
            acceptTcpHandler,NULL) == AE_ERR)
            {
                redisPanic(
                    "Unrecoverable error creating server.ipfd file event.");
            }
    }
}

server繫結檔案事件監聽過程

  aeCreateFileEvent方法內部關鍵點在於aeApiAddEvent這個方法,將fd繫結到具體的eventLoop當中

/*
 * 根據 mask 引數的值,監聽 fd 檔案的狀態,
 * 當 fd 可用時,執行 proc 函式
 */
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData)
{
    if (fd >= eventLoop->setsize) {
        errno = ERANGE;
        return AE_ERR;
    }

    if (fd >= eventLoop->setsize) return AE_ERR;

    // 取出檔案事件結構
    aeFileEvent *fe = &eventLoop->events[fd];

    // 監聽指定 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;

    // 如果有需要,更新事件處理器的最大 fd
    if (fd > eventLoop->maxfd)
        eventLoop->maxfd = fd;

    return AE_OK;
}

server處理accpet事件過程

 整個accept的處理過程按照anetTcpAccept -> acceptCommonHandler -> createClient -> aeCreateFileEvent實現socket的accept並註冊到eventLoop。

  void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {

    while(max--) {
        // accept 客戶端連線
        cfd = anetTcpAccept(server.neterr, fd, cip, sizeof(cip), &cport);
      
        acceptCommonHandler(cfd,0);
    }
}

  static void acceptCommonHandler(int fd, int flags) {

    // 建立客戶端
    redisClient *c;
    if ((c = createClient(fd)) == NULL) {
        redisLog(REDIS_WARNING,
            "Error registering fd event for the new client: %s (fd=%d)",
            strerror(errno),fd);
        close(fd); /* May be already closed, just ignore errors */
        return;
    }
}

redisClient *createClient(int fd) {

    // 分配空間
    redisClient *c = zmalloc(sizeof(redisClient));

    // 當 fd 不為 -1 時,建立帶網路連線的客戶端
    // 如果 fd 為 -1 ,那麼建立無網路連線的偽客戶端
    // 因為 Redis 的命令必須在客戶端的上下文中使用,所以在執行 Lua 環境中的命令時
    // 需要用到這種偽終端
    if (fd != -1) {
        // 繫結讀事件到事件 loop (開始接收命令請求)
        if (aeCreateFileEvent(server.el,fd,AE_READABLE,
            readQueryFromClient, c) == AE_ERR)
        {
            close(fd);
            zfree(c);
            return NULL;
        }
    }
//此處省略很多程式碼
}

事件處理過程

 redis server啟動事件處理的主迴圈,會迴圈執行aeProcessEvents方法。

/*
 * 事件處理器的主迴圈
 */
void aeMain(aeEventLoop *eventLoop) {

    eventLoop->stop = 0;

    while (!eventLoop->stop) {

        // 如果有需要在事件處理前執行的函式,那麼執行它
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);

        // 開始處理事件
        aeProcessEvents(eventLoop, AE_ALL_EVENTS);
    }
}

 aeProcessEvents方法有一個非常巧妙的實現,在內部需要同時處理time event 和 file event,那麼如何處理先後問題呢。思路如下:

  • 首先計算time event距離當前的時間最近的時間點得出時間差X
  • 其次在處理file event的時候wait的設定超時時間為X
  • 如果在超時時間X內有file event發生那麼就處理file event然後處理time event
  • 如果在超時時間X內沒有file event那麼剛好可以處理time event事件
  • 最後說明的是在處理event的過程中我們其實通過for迴圈的序列讀寫事件
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;

    /* Nothing to do? return ASAP */
    if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;

    /* Note that we want call select() even if there are no
     * file events to process as long as we want to process time
     * events, in order to sleep until the next time event is ready
     * to fire. */
    if (eventLoop->maxfd != -1 ||
        ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
        int j;
        aeTimeEvent *shortest = NULL;
        struct timeval tv, *tvp;

        // 獲取最近的時間事件
        if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
            shortest = aeSearchNearestTimer(eventLoop);
        if (shortest) {
            // 如果時間事件存在的話
            // 那麼根據最近可執行時間事件和現在時間的時間差來決定檔案事件的阻塞時間
            long now_sec, now_ms;

            /* Calculate the time missing for the nearest
             * timer to fire. */
            // 計算距今最近的時間事件還要多久才能達到
            // 並將該時間距儲存在 tv 結構中
            aeGetTime(&now_sec, &now_ms);
            tvp = &tv;
            tvp->tv_sec = shortest->when_sec - now_sec;
            if (shortest->when_ms < now_ms) {
                tvp->tv_usec = ((shortest->when_ms+1000) - now_ms)*1000;
                tvp->tv_sec --;
            } else {
                tvp->tv_usec = (shortest->when_ms - now_ms)*1000;
            }

            // 時間差小於 0 ,說明事件已經可以執行了,將秒和毫秒設為 0 (不阻塞)
            if (tvp->tv_sec < 0) tvp->tv_sec = 0;
            if (tvp->tv_usec < 0) tvp->tv_usec = 0;
        } else {
            
            // 執行到這一步,說明沒有時間事件
            // 那麼根據 AE_DONT_WAIT 是否設定來決定是否阻塞,以及阻塞的時間長度

            /* If we have to check for events but need to return
             * ASAP because of AE_DONT_WAIT we need to set the timeout
             * to zero */
            if (flags & AE_DONT_WAIT) {
                // 設定檔案事件不阻塞
                tv.tv_sec = tv.tv_usec = 0;
                tvp = &tv;
            } else {
                /* Otherwise we can block */
                // 檔案事件可以阻塞直到有事件到達為止
                tvp = NULL; /* wait forever */
            }
        }

        // 處理檔案事件,阻塞時間由 tvp 決定
        numevents = aeApiPoll(eventLoop, tvp);
        for (j = 0; j < numevents; j++) {
            // 從已就緒陣列中獲取事件
            aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];

            int mask = eventLoop->fired[j].mask;
            int fd = eventLoop->fired[j].fd;
            int rfired = 0;

           /* note the fe->mask & mask & ... code: maybe an already processed
             * event removed an element that fired and we still didn`t
             * processed, so we check if the event is still valid. */
            // 讀事件
            if (fe->mask & mask & AE_READABLE) {
                // rfired 確保讀/寫事件只能執行其中一個
                rfired = 1;
                fe->rfileProc(eventLoop,fd,fe->clientData,mask);
            }
            // 寫事件
            if (fe->mask & mask & AE_WRITABLE) {
                if (!rfired || fe->wfileProc != fe->rfileProc)
                    fe->wfileProc(eventLoop,fd,fe->clientData,mask);
            }

            processed++;
        }
    }

    /* Check time events */
    // 執行時間事件
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);

    return processed; /* return the number of processed file/time events */
}


相關文章