Redis深度歷險-IO模型 Redis深度歷險-IO模型

Redis深度歷險-IO模型

Redis是單進程單線程實現的服務器,網絡併發還是值得學習一下的,本文在Redis6.2.5的代碼上進行分析,這部分的代碼主要放在ae開頭的源碼文件當中

主線程

跨平臺宏定義

//ae.c
#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

ae.c中根據宏定義來區分使用不同的多路複用機制,這裏面主要是因爲不同平臺的多路複用機制是不一樣的

具體多路複用的實現則是在ae_epoll.c等文件中

事件主循環

//ae.c
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        aeProcessEvents(eventLoop, AE_ALL_EVENTS|
                                   AE_CALL_BEFORE_SLEEP|
                                   AE_CALL_AFTER_SLEEP);
    }
}
AE_ALL_EVENTS                   //所有類型事件,主要是IO和定時器
AE_FILE_EVENTS              //IO事件
AE_TIME_EVENTS              //定時器

redis核心主要是處理IO和定時任務兩種的事件,核心就是一個循環不斷的調用epoll_waitselect等函數等待事件

定時任務

定時任務結構體

//ae.h
typedef struct aeTimeEvent {
    long long id;               //定時任務的ID
    monotime when;                                                      //定時任務的時間
    aeTimeProc *timeProc;                                           //定時任務執行的回調函數
    aeEventFinalizerProc *finalizerProc;            //定時任務終結時的回調函數,被刪除時執行
    void *clientData;                           //回調參數
    struct aeTimeEvent *prev;           //雙向鏈表前節點
    struct aeTimeEvent *next;           //雙向鏈表後節點
    int refcount;                               //引用計數
} aeTimeEvent;

在Redis中的定時任務是通過鏈表的形式存儲的,可以指定一個回調函數和回調函數的參數

等待時間

//ae.c
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;

    
    if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;

    //即使當前沒有IO任務要執行,同樣進入此判斷因爲可以通過多路複用實現sleep的效果來處理定時任務
    if (eventLoop->maxfd != -1 ||
        ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
        int j;
        struct timeval tv, *tvp;
        int64_t usUntilTimer = -1;
                
        //計算當前最快的一個定時任務的時間
        if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
            usUntilTimer = usUntilEarliestTimer(eventLoop);
                
        if (usUntilTimer >= 0) {
            tv.tv_sec = usUntilTimer / 1000000;
            tv.tv_usec = usUntilTimer % 1000000;
            tvp = &tv;
        } else {
            if (flags & AE_DONT_WAIT) {
                tv.tv_sec = tv.tv_usec = 0;
                tvp = &tv;
            } else {
                tvp = NULL;
            }
        }

        if (eventLoop->flags & AE_DONT_WAIT) {
            tv.tv_sec = tv.tv_usec = 0;
            tvp = &tv;
        }

        if (eventLoop->beforesleep != NULL && flags & AE_CALL_BEFORE_SLEEP)
            eventLoop->beforesleep(eventLoop);

        //將最近的一個定時任務的時間設置爲多路複用的等待時間,如果沒有觸發IO事件那麼此函數結束時就是定時任務到期時
        numevents = aeApiPoll(eventLoop, tvp);
.......
    //處理定時任務
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);
.......

無論是selctepoll都是支持設置等待時間的

Redis的定時任務的實現就是每次將等待時間設置爲最近定時任務的時間,然後在休眠結束後處理定時任務

static int64_t usUntilEarliestTimer(aeEventLoop *eventLoop) {
    aeTimeEvent *te = eventLoop->timeEventHead;
    if (te == NULL) return -1;
        
    //循環遍歷所有的定時任務,找到最近的一個定時任務
    aeTimeEvent *earliest = NULL;
    while (te) {
        if (!earliest || te->when < earliest->when)
            earliest = te;
        te = te->next;
    }

    monotime now = getMonotonicUs();
    return (now >= earliest->when) ? 0 : earliest->when - now;
}

由於定時任務是鏈表的結構,這裏實際的時間複雜度是O(n),此函數就是統計出最快的一個定時任務

定時任務的執行

/* Process time events */
static int processTimeEvents(aeEventLoop *eventLoop) {
    int processed = 0;
    aeTimeEvent *te;
    long long maxId;

    te = eventLoop->timeEventHead;
    maxId = eventLoop->timeEventNextId-1;
    monotime now = getMonotonicUs();
    while(te) {
        long long id;

        //處理標記爲刪除的定時任務
        if (te->id == AE_DELETED_EVENT_ID) {
            aeTimeEvent *next = te->next;
            if (te->refcount) {
                te = next;
                continue;
            }
            if (te->prev)
                te->prev->next = te->next;
            else
                eventLoop->timeEventHead = te->next;
            if (te->next)
                te->next->prev = te->prev;
            if (te->finalizerProc) {
                //執行定時任務終結的回調函數
                te->finalizerProc(eventLoop, te->clientData);
                now = getMonotonicUs();
            }
            //釋放資源,從鏈表中刪除掉這個定時任務
            zfree(te);
            te = next;
            continue;
        }

        if (te->id > maxId) {
            te = te->next;
            continue;
        }
                
        //執行時間已經到了的定時任務
        if (te->when <= now) {
            int retval;

            id = te->id;
            te->refcount++;
            retval = te->timeProc(eventLoop, id, te->clientData);
            te->refcount--;
            processed++;
            now = getMonotonicUs();
          
            //根據回調函數的返回值來決定是否在再次註冊定時任務,不是-1就會再次註冊定時任務
            if (retval != AE_NOMORE) {
                te->when = now + retval * 1000;
            } else {
                //對於需要刪除的任務不是直接刪除,而是打標記
                te->id = AE_DELETED_EVENT_ID;
            }
        }
        te = te->next;
    }
    return processed;
}

由於多路複用的機制在有IO進來時就會提前返回,所以在這裏還是需要輪詢遍歷所有的定時任務然後根據時間對比來處理

定時任務總結

定時任務由於受限於多路複用的機制,最多設置爲ms級別,同時從上面來看定時任務使用鏈表存儲性能較差

IO事件處理

事件結構體

//ae.h
typedef struct aeFileEvent {
    int mask;                               //事件掩碼,讀、寫兩種事件類型
    aeFileProc *rfileProc;      //讀取回調函數
    aeFileProc *wfileProc;      //寫入回調函數
    void *clientData;                   //回調函數參數
} aeFileEvent;

事件定義

#define AE_NONE 0       //無
#define AE_READABLE 1   //可讀
#define AE_WRITABLE 2   //可寫
#define AE_BARRIER 4        //用來控制讀寫順序

對於一個fd同時設置了讀寫事件,默認是先讀後寫,如果設置了AE_BARRIER則是先寫後讀

事件循環結構體

//ae.h
typedef struct aeEventLoop {
    int maxfd;                                      /* 當前註冊的最大文件描述符 */
    int setsize;                                    /* 允許監控、註冊的最大文件描述符 */
    long long timeEventNextId;                          
    aeFileEvent *events;                    /* 當前註冊進來的的IO事件數組,事件的fd就是數組下標 */
    aeFiredEvent *fired;                    /* 多路複用接口返回的觸發事件數組 */
    aeTimeEvent *timeEventHead;     /* 註冊的定時任務鏈表 */
    int stop;                                           /* 是否停止循環*/
    void *apidata; 
    aeBeforeSleepProc *beforesleep; /* 多路複用等待前的回調程序 */
    aeBeforeSleepProc *aftersleep;  /* 多路複用調用結束後的回調函數 */
    int flags;
} aeEventLoop;

事件處理

//ae.c
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
.........
        //多路複用機制,最終會調用select、epoll等,觸發的IO事件則放在fired中
        numevents = aeApiPoll(eventLoop, tvp);
        
        if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)
            eventLoop->aftersleep(eventLoop);


        for (j = 0; j < numevents; j++) {
                //events的大小就是setsize的大小,直接以文件描述符作爲數組的下標來存儲事件結構題
            aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];
            int mask = eventLoop->fired[j].mask;
            int fd = eventLoop->fired[j].fd;
            int fired = 0; 

            //判斷當前的處理順序
            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++;
                }
            }
            processed++;
        }
    }

    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);

    return processed; /* 返回處理的事件個數 file+time events */
}

這裏用了一個比較巧妙的設計就是,在eventLoop->events存儲註冊的事件時直接就是以fd作爲索引,實現了類似hash的方式

多路複用實現

這裏只介紹一下epoll的實現,注意這裏是直接include源文件的方式加載到ae.c中,所以雖然函數都是static但是都是可以用的

結構體定義

typedef struct aeApiState {
    int epfd;                                       //epoll專用的文件描述符
    struct epoll_event *events;     //用來接收epoll_wait觸發的事件
} aeApiState;

等待事件

//ae_epoll.c
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
    aeApiState *state = eventLoop->apidata;
    int retval, numevents = 0;
        //等待IO事件進入
    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;
                
                //遍歷所有觸發的事件,放在fired中返回
        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;
}

和正常的處理相同, 調用epoll_wait等待事件觸發, 將觸發的事件存儲到fired中返回

註冊事件

//ae.c
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,
        aeFileProc *proc, void *clientData)
{
    //不允許超過最大允許註冊的文件描述符
    if (fd >= eventLoop->setsize) {
        errno = ERANGE;
        return AE_ERR;
    }
    //以fd作爲下標索引將事件放到events中去
    aeFileEvent *fe = &eventLoop->events[fd];
        
    //註冊到epoll中去
    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;
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章