Redis 原始碼學習之事件驅動

發表於2017-06-11

Redis基於多路複用技術實現了一套簡單的事件驅動庫,程式碼在ae.hae.c以及ae_epoll.cae_evport.cae_kqueue.cae_select.c這幾個檔案中。其中ae表示的是antirez eventloop的意思。

Redis裡面包含兩種事件型別:FileEventTimeEvent

Redis採用IO多路複用技術,所有的事件都是在一個執行緒中進行處理。Redis的事件驅動模型可以以以下為程式碼進行表示:

在一個死迴圈中等待事件的到來,然後對事件進行處理,以此往復。這就是一個最經典的網路程式設計模型。

1.基本資料結構

aeEventLoop

aeEventLoopRedis中事件驅動模型的核心,封裝了整個事件迴圈,其中每個欄位解釋如下:

  • maxfd:已經接受的最大的檔案描述符。
  • setsize:當前迴圈中所能容納的檔案描述符的數量。
  • timeEventNextId:下一個時間事件的ID.
  • lastTime:上一次被訪問的時間,用來檢測系統時鐘是否被修改。
  • events:指標,指向儲存所有註冊的事件的陣列首地址。
  • fired:指標,儲存所有已經買被觸發的事件的陣列首地址。
  • timeEventHead:Redis用一個連結串列來儲存所有的時間事件,timeEventHead是指向這個連結串列的首節點指標。
  • stop:停止整個事件迴圈。
  • apiData:指標,指向epoll結構。
  • beforeSleep:函式指標。每次實現迴圈的時候,在阻塞直到時間到來之前,會先呼叫這個函式。

aeFileEvent和aeTimeEvent

這兩個結構分別表示檔案事件和時間事件,定義如下

其中mask表示檔案事件型別掩碼,可以是AE_READABLE表示是可讀事件,AE_WRITABLE為可寫事件。aeFileProc是函式指標。

aeFiredEvent

aeFiredEvent結構表示一個已經被觸發的事件,結果如下:

fd表示事件發生在哪個檔案描述符上面,mask用來表示具體事件的型別。

aeApiState

Redis底層採用IO多路複用技術實現高併發,具體實現可以採用kqueueselectepoll等技術。對於Linux來說,epoll的效能要優於select,所以以epoll為例來進行分析。

aeApiState封裝了跟epoll相關的資料,epfd儲存epoll_create()返回的檔案描述符。

具體實現細節

事件迴圈啟動:aeMain()

事件驅動的啟動程式碼位於ae.caeMain()函式中,程式碼如下:

aeMain()方法中可以看到,整個事件驅動是在一個while()迴圈中不停地執行aeProcessEvents()方法,在這個方法中執行從客戶端傳送過來的請求。

初始化:aeCreateEventLoop()

aeEventLoop的初始化是在aeCreateEventLoop()方法中進行的,這個方法是在server.c中的initServer()中呼叫的。實現如下:

在這個方法中主要就是給aeEventLoop物件分配記憶體然後並進行初始化。其中關鍵的地方有:

1、呼叫aeApiCreate()初始化epoll相關的資料。aeApiCreate()實現如下:

aeApiCreate()方法中主要完成以下三件事:
1. 分配aeApiState結構需要的記憶體。
2. 呼叫epoll_create()方法生成epoll的檔案描述符,並儲存在aeApiState.epfd欄位中。
3. 把第一步分配的aeApiState的記憶體地址儲存在EventLoop->apidata欄位中。

2、初始化events中的mask欄位為為AE_NONE

生成fileEvent:aeCreateFileEvent()

Redis使用aeCreateFileEvent()來生成fileEvent,程式碼如下:

aeCreateFileEvent()方法主要做了一下三件事:

  1. 檢查新增的fd是否超過所能容納最大值。
  2. 呼叫aeApiAddEvent()方法把對應的fd以mask模式新增到epoll監聽器中。

設定相應的欄位值。其中最關鍵的步驟是第二步,aeApiAddEvent()方法如下:

生成timeEvent:aeCreateTimeEvent()

aeCreateTimeEvent()方法主要是用來生成timeEvent節點,其實現比較簡單,程式碼如下所示:

處理timeEevnt:processTimeEvents()

RedisprocessTimeEvents()方法中來處理所有的timeEvent,實現如下:

在這個方法中會

  1. 判斷系統時間有沒有調整過,如果調整過,則會把timeEvent連結串列中的所有的timeEvent的觸發時間設定為0,表示立即執行。
  2. timeEvent連結串列進行遍歷,對於每個timeEvent節點,如果有:
    1. 如果已經被標記為刪除(AE_DELETED_EVENT_ID),則立即釋放對應節點記憶體,遍歷下個節點。
    2. 如果id大於maxId,則表示當前節點為本次迴圈中新增節點,咋本次迴圈中不錯處理,繼續下個節點。
    3. 如果當前節點的觸發時間大於當前時間,則呼叫對應節點的timeProc()方法執行任務。根據timeProc()方法的返回,又分為兩種情況:
      1. 返回為AE_NOMORE,表示當前timeEvent節點屬於一次性事件,標記該節點IDAE_DELETED_EVENT_ID,表示刪除節點,該節點將會在下一輪的迴圈中被刪除。
      2. 返回不是AE_NOMORE,表示當前timeEvent節點屬於週期性事件,需要多次執行,呼叫aeAddMillisecondsToNow()方法設定下次被執行時間。

處理所有事件:aeProcessEvents()

Redis中所有的事件,包括timeEventfileEvent都是在aeProcessEvents()方法中進行處理的,剛方法實現如下:

該方法的入參flag表示要處理哪些事件,可以取以下幾個值 :

  • AE_ALL_EVENTS:timeEventfileEvent都會處理。
  • AE_FILE_EVENTS:只處理fileEvent
  • AE_TIME_EVENTS:只處理timeEvent
  • AE_DONT_WAIT:要麼立馬返回,要麼處理完那些不需要等待的事件之後再立馬返回。

aeProcessEvents()方法會做下面幾件事:

  1. 判斷傳入的flag的值,如果既不包含AE_TIME_EVENTS也不包含AE_FILE_EVENTS則直接返回。
  2. 計算如果有aeFileEvent事件需要進行處理,則先計算epoll_wait()方法需要阻塞等待的時間,計算方式如下:
    1. 先從aeTimeEvent事件連結串列中找到最近的需要被觸發的aeTimeEvent節點並計算需要被觸發的時間,該被觸發時間則為epoll_wait()需要等待的時間。
    2. 如果沒有找到最近的aeTimeEvent節點,表示沒有aeTimeEvent節點被加入連結串列,則判斷傳入的flags是否包含AE_DONT_WAIT選項,則設定epoll_wait()需要等待時間為0,表示立即返回。
    3. 如果沒有設定AE_DONT_WAIT,則設定需要等待時間為NULL,表示epoll_wait()一直阻塞等待知道有fileEvent事件到來。
  3. 呼叫aeApiPoll()方法阻塞等待事件的到來,阻塞時間為第二步中計算的時間。aeApiPoll()實現如下:

    aeApiPoll()會做下面幾件事:
    1. 根據傳入的tvp計算需要阻塞的時間,然後呼叫epoll_wait()進行阻塞等待。
    2. 有事件到來之後先計算對應事件的型別。
    3. 把事件發生的fd以及對應的型別mask拷貝到fired陣列中。
  4. aeApiPoll()方法返回之後,所有事件已經就緒了的fd以及對應事件的型別mask已經儲存在eventLoop->fired[]陣列中。依次遍歷fired陣列,根據mask型別,執行對應的frileProc()或者wfileProce()方法。
  5. 如果傳入的flags中有AE_TIME_EVENTS,則呼叫processTimeEvents()執行所有已經到時間了的timeEvent

相關文章