Redis源碼學習之 網絡庫模塊 ae.c / ae_epoll.c


前言

因爲 Redis 的網絡模塊是一個採用 epoll 的但線程模型, 閱讀起來相對更加簡單, 就先從這一部分入手
文章主要包括 :

  • TCP socket accept 建立連接的過程
  • Redis 的 epoll 模型
  • 處理時間事件和文件事件的流程
  • TCP 數據的讀寫
  • 處理非活動連接

必要數據結構

封裝 epoll 的必要成員

typedef struct aeApiState
{

    // epoll_event 實例描述符
    int epfd;

    // 事件槽
    struct epoll_event *events;

} aeApiState;

文件事件

typedef struct aeFileEvent 
{
    // 監聽事件類型掩碼,
    // 值可以是 AE_READABLE 或 AE_WRITABLE ,
    // 或者 AE_READABLE | AE_WRITABLE
    int mask; /* one of AE_(READABLE|WRITABLE) */
    // 讀事件處理器
    aeFileProc *rfileProc;
    // 寫事件處理器
    aeFileProc *wfileProc;
    // 多路複用庫的私有數據
    void *clientData;
} aeFileEvent;

時間事件結構

typedef struct aeTimeEvent 
{
    // 時間事件的唯一標識符
    long long id; /* time event identifier. */
    // 事件的到達時間
    long when_sec; /* seconds */
    long when_ms; /* milliseconds */
    // 事件處理函數
    aeTimeProc *timeProc;
    // 事件釋放函數
    aeEventFinalizerProc *finalizerProc;
    // 多路複用庫的私有數據
    void *clientData;
    // 指向下個時間事件結構,形成鏈表
    struct aeTimeEvent *next;
} aeTimeEvent;

已就緒事件

typedef struct aeFiredEvent
 {
    // 已就緒文件描述符
    int fd;
    // 事件類型掩碼,
    // 值可以是 AE_READABLE 或 AE_WRITABLE
    // 或者是兩者的或
    int mask;
} aeFiredEvent;

事件處理器的狀態 (就是最主要的 aeEventLoop

typedef struct aeEventLoop 
{
    // 目前已註冊的最大描述符
    int maxfd;   /* highest file descriptor currently registered */
    // 目前已追蹤的最大描述符
    int setsize; /* max number of file descriptors tracked */
    // 用於生成時間事件 id
    long long timeEventNextId;
    // 最後一次執行時間事件的時間
    time_t lastTime;     /* Used to detect system clock skew */
    // 已註冊的文件事件
    aeFileEvent *events; /* Registered events */
    // 已就緒的文件事件
    aeFiredEvent *fired; /* Fired events */
    // 時間事件
    aeTimeEvent *timeEventHead;
    // 事件處理器的開關
    int stop;
    // 多路複用庫的私有數據
    void *apidata;  //我們只分析指向 epoll
    // 在處理事件前要執行的函數
    aeBeforeSleepProc *beforesleep;
} aeEventLoop;

初始化事件處理器狀態

ae.c 中

/*
 * 初始化事件處理器狀態
 */
aeEventLoop *aeCreateEventLoop(int setsize)
{
    aeEventLoop *eventLoop;
    int i;

    // 創建事件狀態結構
    if ((eventLoop = zmalloc(sizeof(*eventLoop))) == NULL)
        goto err;

    // 初始化文件事件結構和已就緒文件事件結構數組
    eventLoop->events = zmalloc(sizeof(aeFileEvent) * setsize);
    eventLoop->fired = zmalloc(sizeof(aeFiredEvent) * setsize);
    if (eventLoop->events == NULL || eventLoop->fired == NULL)
        goto err;
    // 設置數組大小
    eventLoop->setsize = setsize;
    // 初始化執行最近一次執行時間
    eventLoop->lastTime = time(NULL);

    // 初始化時間事件結構
    eventLoop->timeEventHead = NULL; //定時事件鏈表置空
    eventLoop->timeEventNextId = 0;  //定時事件id爲0

    eventLoop->stop = 0;
    eventLoop->maxfd = -1;
    eventLoop->beforesleep = NULL;

    //給EPOLL申請空間
    if (aeApiCreate(eventLoop) == -1)
        goto err;

    /* Events with mask == AE_NONE are not set. So let's initialize the
     * vector with it. */
    // 初始化監聽事件
    for (i = 0; i < setsize; i++)
        eventLoop->events[i].mask = AE_NONE;

    // 返回事件循環
    return eventLoop;

err:
    if (eventLoop)
    {
        zfree(eventLoop->events);
        zfree(eventLoop->fired);
        zfree(eventLoop);
    }
    return NULL;
}

先調用 aeCreateEventLoop 函數, 動態分配內存, 進行初始化

初始化事件處理器其中包含有:

  1. 文件事件數組 (內部是一個指針, 初始化時動態分配固定內存大小, 形如數組)
  2. 已就緒文件事件數組 (同上)
  3. 時間事件鏈表 (指針置空) Redis 的時間事件是一個單鏈表, 且無序, 所以查找複雜度爲O(n), 每次新的時間事件放入表頭

Redis 只使用一個 serverCron 一個時間事件, 在其中執行所有的週期性任務, 所以 redis 幾乎將這個無序鏈表當成指針在用, 沒有造成性能上的影響

  1. void* 指針指向封裝的 epoll 結構體, 其中有 epfd 和 epoll_event 數組(也是一個 epoll_event 指針)

返回 eventloop 指針

創建 listenfd 並加入 epoll

初始化完後, 需要調用anetTcpServer函數(anet.c 中)

int anetTcpServer(char *err, int port, char *bindaddr, int backlog)
{
    return _anetTcpServer(err, port, bindaddr, AF_INET, backlog);
}

這個函數中調用_anetTcpServer去真正調用::socket函數創建一個listenfd, 經過 bind listen 後, 調用aeCreateFileEvent(ae.c) 將其添加進 epoll 的監聽事件合集

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 的指定事件, 本質上調用 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;

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

    return AE_OK;
}

執行主循環

void aeMain(aeEventLoop *eventLoop)
{

    eventLoop->stop = 0;

    while (!eventLoop->stop)
    {

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

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

處理事件 aeProcessEvents

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;

    /* flag 沒有指定監聽事件*/
    if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS))
        return 0;

    /*這裏會調用epoll阻塞直到時間事件到期*/
    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;

            // 計算距今最近的時間事件還要多久才能達到
            // 並將該時間距保存在 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 是否設置來決定是否阻塞,以及阻塞的時間長度

            //如果flag設置的這個,需要儘快返回,阻塞時間爲0
            if (flags & AE_DONT_WAIT)
            {
                // 設置文件事件不阻塞
                tv.tv_sec = tv.tv_usec = 0;
                tvp = &tv;
            }
            else
            {
                //不是的話我們可以阻塞
                // 文件事件可以阻塞直到有事件到達爲止
                tvp = NULL; //爲NULL時,epoll_wait的timeout設置爲-1,一直等待
            }
        }

        // 處理文件事件,阻塞時間由 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 */
}

該函數flag默認設置爲AE_ALL_EVENTS, 即處理所有類型事件
如果沒有設置不能阻塞立刻返回的AE_DONT_WAIT標誌且沒有一個最近要到期的時間事件, epoll本來會一直阻塞下去,
但是 Redis 默認是有一個週期性檢查自身資源和狀態的時間事件(上面也提到了, 即serverCron), 所以會設置epoll的超時時間爲時間事件的到期時間間隔, 到期後執行時間事件
當然, 根據代碼能看出來, 如果等待期間有文件事件發生, 會先解決文件事件, 然後再處理時間事件, 所以時間事件的實際執行時間總是稍晚一點

數據讀寫

Redis 的客戶端與服務器交互數據時, 都按照 Redis 定義的協議對格式進行編碼, 這樣就使消息之間有了 “邊界”, 來應對 TCP 協議的流特性

比如說 : 客戶端發送SET msg “helloworld”
那麼客戶端實際發送的數據是:
*3\r\n$3\r\nSET\r\n$3\r\nmsg\r\n$11\r\nhelloworld\r\n

*3即該命令有3個參數, $3第一個參數長度爲3, 值爲SET, 也就是要執行的命令, 同上第二個參數長度爲3, 值爲msg, 第三個參數長度爲11, 值爲hello world

處理非活動連接

// 檢查客戶端是否已經超時,如果超時就關閉客戶端,並返回 1 ;
// 否則返回 0 。
int clientsCronHandleTimeout(redisClient *c) {

    // 獲取當前時間
    time_t now = server.unixtime;

    // 服務器設置了 maxidletime 時間
    if (server.maxidletime &&
        // 不檢查作爲從服務器的客戶端
        !(c->flags & REDIS_SLAVE) &&    /* no timeout for slaves */
        // 不檢查作爲主服務器的客戶端
        !(c->flags & REDIS_MASTER) &&   /* no timeout for masters */
        // 不檢查被阻塞的客戶端
        !(c->flags & REDIS_BLOCKED) &&  /* no timeout for BLPOP */
        // 不檢查訂閱了頻道的客戶端
        dictSize(c->pubsub_channels) == 0 && /* no timeout for pubsub */
        // 不檢查訂閱了模式的客戶端
        listLength(c->pubsub_patterns) == 0 &&
        // 客戶端最後一次與服務器通訊的時間已經超過了 maxidletime 時間
        (now - c->lastinteraction > server.maxidletime))
    {
        redisLog(REDIS_VERBOSE,"Closing idle client");
        // 關閉超時客戶端
        freeClient(c);
        return 1;
    } else if (c->flags & REDIS_BLOCKED) {

        /* Blocked OPS timeout is handled with milliseconds resolution.
         * However note that the actual resolution is limited by
         * server.hz. */
        // 獲取最新的系統時間
        mstime_t now_ms = mstime();

        // 檢查被 BLPOP 等命令阻塞的客戶端的阻塞時間是否已經到達
        // 如果是的話,取消客戶端的阻塞
        if (c->bpop.timeout != 0 && c->bpop.timeout < now_ms) {
            // 向客戶端返回空回覆
            replyToBlockedClientTimedOut(c);
            // 取消客戶端的阻塞狀態
            unblockClient(c);
        }
    }

    // 客戶度沒有被關閉
    return 0;
}

我們發現 Redis 簡單粗暴的比較客戶端上一次的訪問時間, 如果唱過閾值, 就直接斷開連接

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