Redis
基於多路複用技術實現了一套簡單的事件驅動庫,程式碼在ae.h
、ae.c
以及ae_epoll.c
、ae_evport.c
和ae_kqueue.c
、ae_select.c
這幾個檔案中。其中ae
表示的是antirez eventloop
的意思。
Redis
裡面包含兩種事件型別:FileEvent
和TimeEvent
。
Redis
採用IO
多路複用技術,所有的事件都是在一個執行緒中進行處理。Redis
的事件驅動模型可以以以下為程式碼進行表示:
1 2 3 4 5 6 7 |
int main(int argc,char **argv) { while(true) { // 等待事件到來:wait4Event(); // 處理事件:processEvent() } } |
在一個死迴圈中等待事件的到來,然後對事件進行處理,以此往復。這就是一個最經典的網路程式設計模型。
1.基本資料結構
aeEventLoop
1 2 3 4 5 6 7 8 9 10 11 12 13 |
/* State of an event based program */ typedef struct aeEventLoop { int maxfd; /* highest file descriptor currently registered */ int setsize; /* max number of file descriptors tracked */ 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; /* This is used for polling API specific data */ aeBeforeSleepProc *beforesleep; } aeEventLoop; |
aeEventLoop
是Redis
中事件驅動模型的核心,封裝了整個事件迴圈,其中每個欄位解釋如下:
maxfd
:已經接受的最大的檔案描述符。setsize
:當前迴圈中所能容納的檔案描述符的數量。timeEventNextId
:下一個時間事件的ID
.lastTime
:上一次被訪問的時間,用來檢測系統時鐘是否被修改。events
:指標,指向儲存所有註冊的事件的陣列首地址。fired
:指標,儲存所有已經買被觸發的事件的陣列首地址。timeEventHead
:Redis
用一個連結串列來儲存所有的時間事件,timeEventHead
是指向這個連結串列的首節點指標。stop
:停止整個事件迴圈。apiData
:指標,指向epoll
結構。beforeSleep
:函式指標。每次實現迴圈的時候,在阻塞直到時間到來之前,會先呼叫這個函式。
aeFileEvent和aeTimeEvent
這兩個結構分別表示檔案事件和時間事件,定義如下
1 2 3 4 5 6 |
typedef struct aeFileEvent { int mask; /* one of AE_(READABLE|WRITABLE) */ aeFileProc *rfileProc; // 函式指標,寫事件處理 aeFileProc *wfileProc; // 函式指標,讀事件處理 void *clientData; // 具體的資料 } aeFileEvent; |
其中mask
表示檔案事件型別掩碼,可以是AE_READABLE
表示是可讀事件,AE_WRITABLE
為可寫事件。aeFileProc
是函式指標。
1 2 3 4 5 6 7 8 9 10 |
/* Time event structure */ typedef struct aeTimeEvent { long long id; // 事件ID long when_sec; // 事件觸發的時間:s long when_ms; // 事件觸發的時間:ms aeTimeProc *timeProc; // 函式指標 aeEventFinalizerProc *finalizerProc; // 函式指標:在對應的aeTieEvent節點被刪除前呼叫,可以理解為aeTimeEvent的解構函式 void *clientData; // 指標,指向具體的資料 struct aeTimeEvent *next; // 指向下一個時間事件指標 } aeTimeEvent; |
aeFiredEvent
aeFiredEvent
結構表示一個已經被觸發的事件,結果如下:
1 2 3 4 5 |
/* A fired event */ typedef struct aeFiredEvent { int fd; // 事件被觸發的檔案描述符 int mask; // 被觸發事件的掩碼,表示被觸發事件的型別 } aeFiredEvent; |
fd
表示事件發生在哪個檔案描述符上面,mask
用來表示具體事件的型別。
aeApiState
Redis
底層採用IO
多路複用技術實現高併發,具體實現可以採用kqueue
、select
、epoll
等技術。對於Linux
來說,epoll
的效能要優於select
,所以以epoll
為例來進行分析。
1 2 3 4 |
typedef struct aeApiState { int epfd; struct epoll_event *events; } aeApiState; |
aeApiState
封裝了跟epoll
相關的資料,epfd
儲存epoll_create()
返回的檔案描述符。
具體實現細節
事件迴圈啟動:aeMain()
事件驅動的啟動程式碼位於ae.c
的aeMain()
函式中,程式碼如下:
1 2 3 4 5 6 7 8 |
void aeMain(aeEventLoop *eventLoop) { eventLoop->stop = 0; while (!eventLoop->stop) { if (eventLoop->beforesleep != NULL) eventLoop->beforesleep(eventLoop); aeProcessEvents(eventLoop, AE_ALL_EVENTS); } } |
從aeMain()
方法中可以看到,整個事件驅動是在一個while()
迴圈中不停地執行aeProcessEvents()
方法,在這個方法中執行從客戶端傳送過來的請求。
初始化:aeCreateEventLoop()
aeEventLoop
的初始化是在aeCreateEventLoop()
方法中進行的,這個方法是在server.c
中的initServer()
中呼叫的。實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
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; eventLoop->stop = 0; eventLoop->maxfd = -1; eventLoop->beforesleep = NULL; // 呼叫aeApiCreate()初始化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++) /** * 把每個剛新建的aeFileEvent.mask設定為AE_NONE * 這點是必須的 */ eventLoop->events[i].mask = AE_NONE; return eventLoop; err: if (eventLoop) { zfree(eventLoop->events); zfree(eventLoop->fired); zfree(eventLoop); } return NULL; } |
在這個方法中主要就是給aeEventLoop
物件分配記憶體然後並進行初始化。其中關鍵的地方有:
1、呼叫aeApiCreate()
初始化epoll
相關的資料。aeApiCreate()
實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
static int aeApiCreate(aeEventLoop *eventLoop) { // 1.分配記憶體 aeApiState *state = zmalloc(sizeof(aeApiState)); if (!state) return -1; // 1.分配events記憶體,epoll_event的大小為setsize state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize); if (!state->events) { zfree(state); return -1; } // 2.呼叫epoll_create()生成epoll檔案描述符,並儲存在epfd這個域中 state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */ if (state->epfd == -1) { zfree(state->events); zfree(state); return -1; } // 把apidata指標指向第一步中分配的記憶體地址 eventLoop->apidata = state; return 0; } |
在aeApiCreate()
方法中主要完成以下三件事:
1. 分配aeApiState
結構需要的記憶體。
2. 呼叫epoll_create()
方法生成epoll
的檔案描述符,並儲存在aeApiState.epfd
欄位中。
3. 把第一步分配的aeApiState
的記憶體地址儲存在EventLoop->apidata
欄位中。
2、初始化events
中的mask
欄位為為AE_NONE
。
生成fileEvent
:aeCreateFileEvent()
Redis
使用aeCreateFileEvent()
來生成fileEvent
,程式碼如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask, aeFileProc *proc, void *clientData) { // 1. 檢查新增的fd是否超過所能容納最大值 if (fd >= eventLoop->setsize) { errno = ERANGE; return AE_ERR; } aeFileEvent *fe = &eventLoop->events[fd]; // 2. 呼叫aeApiAddEvent()方法把對應的fd以mask模式新增到epoll監聽器中 if (aeApiAddEvent(eventLoop, fd, mask) == -1) return AE_ERR; // 3. 設定相應的欄位值 fe->mask |= mask; if (mask & AE_READABLE) fe->rfileProc = proc; if (mask & AE_WRITABLE) fe->wfileProc = proc; fe->clientData = clientData; if (fd > eventLoop->maxfd) // 如果有需要則修改maxfd欄位的值 eventLoop->maxfd = fd; return AE_OK; } |
aeCreateFileEvent()
方法主要做了一下三件事:
- 檢查新增的fd是否超過所能容納最大值。
- 呼叫aeApiAddEvent()方法把對應的fd以mask模式新增到epoll監聽器中。
設定相應的欄位值。其中最關鍵的步驟是第二步,aeApiAddEvent()
方法如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) { aeApiState *state = eventLoop->apidata; struct epoll_event ee = {0}; /* avoid valgrind warning */ /* 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; } |
生成timeEvent
:aeCreateTimeEvent()
aeCreateTimeEvent()
方法主要是用來生成timeEvent
節點,其實現比較簡單,程式碼如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds, aeTimeProc *proc, void *clientData, aeEventFinalizerProc *finalizerProc) { // 1. 獲取id long long id = eventLoop->timeEventNextId++; aeTimeEvent *te; // 2. 分配記憶體 te = zmalloc(sizeof(*te)); if (te == NULL) return AE_ERR; // 3. 設定aeTimeEvent的各個欄位的值 te->id = id; aeAddMillisecondsToNow(milliseconds,&te->when_sec,&te->when_ms); te->timeProc = proc; te->finalizerProc = finalizerProc; te->clientData = clientData; // timeEventHead總是指向最新新增的timeEvent節點 te->next = eventLoop->timeEventHead; eventLoop->timeEventHead = te; return id; } |
處理timeEevnt
:processTimeEvents()
Redis
在processTimeEvents()
方法中來處理所有的timeEvent
,實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 |
static int processTimeEvents(aeEventLoop *eventLoop) { int processed = 0; aeTimeEvent *te, *prev; long long maxId; time_t now = time(NULL); /** * 如果系統時間被調整到將來某段時間然後又被設定回正確的時間, * 這種情況下連結串列中的timeEvent有可能會被隨機的延遲執行,因 * 此在這個情況下把所有的timeEvent的觸發時間設定為0表示及執行 */ if (now < eventLoop->lastTime) { te = eventLoop->timeEventHead; while(te) { te->when_sec = 0; te = te->next; } } eventLoop->lastTime = now; // 設定上次執行時間為now prev = NULL; te = eventLoop->timeEventHead; maxId = eventLoop->timeEventNextId-1; while(te) { long now_sec, now_ms; long long id; /** * 刪除已經被標誌位 刪除 的時間事件 */ if (te->id == AE_DELETED_EVENT_ID) { aeTimeEvent *next = te->next; if (prev == NULL) eventLoop->timeEventHead = te->next; else prev->next = te->next; if (te->finalizerProc) // 在時間事件節點被刪除前呼叫finlizerProce()方法 te->finalizerProc(eventLoop, te->clientData); zfree(te); te = next; continue; } if (te->id > maxId) { /** * te->id > maxId 表示當前te指向的timeEvent為當前迴圈中新新增的, * 對於新新增的節點在本次迴圈中不作處理。 * PS:為什麼會出現這種情況呢?有可能是在timeProc()裡面會註冊新的timeEvent節點? * 對於當前的Redis版本中不會出現te->id > maxId這種情況 */ te = te->next; continue; } aeGetTime(&now_sec, &now_ms); if (now_sec > te->when_sec || (now_sec == te->when_sec && now_ms >= te->when_ms)) { // 如果當前時間已經超過了對應的timeEvent節點設定的觸發時間, // 則呼叫timeProc()方法執行對應的任務 int retval; id = te->id; retval = te->timeProc(eventLoop, id, te->clientData); processed++; if (retval != AE_NOMORE) { // 要執行多次,則計算下次執行時間 aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms); } else { // 如果只需要執行一次,則把id設定為-1,再下次迴圈中刪除 te->id = AE_DELETED_EVENT_ID; } } prev = te; te = te->next; } return processed; } |
在這個方法中會
- 判斷系統時間有沒有調整過,如果調整過,則會把
timeEvent
連結串列中的所有的timeEvent
的觸發時間設定為0,表示立即執行。 - 對
timeEvent
連結串列進行遍歷,對於每個timeEvent
節點,如果有:- 如果已經被標記為刪除(
AE_DELETED_EVENT_ID
),則立即釋放對應節點記憶體,遍歷下個節點。 - 如果
id
大於maxId
,則表示當前節點為本次迴圈中新增節點,咋本次迴圈中不錯處理,繼續下個節點。 - 如果當前節點的觸發時間大於當前時間,則呼叫對應節點的
timeProc()
方法執行任務。根據timeProc()
方法的返回,又分為兩種情況:- 返回為
AE_NOMORE
,表示當前timeEvent
節點屬於一次性事件,標記該節點ID
為AE_DELETED_EVENT_ID
,表示刪除節點,該節點將會在下一輪的迴圈中被刪除。 - 返回不是
AE_NOMORE
,表示當前timeEvent
節點屬於週期性事件,需要多次執行,呼叫aeAddMillisecondsToNow()
方法設定下次被執行時間。
- 返回為
- 如果已經被標記為刪除(
處理所有事件:aeProcessEvents()
Redis
中所有的事件,包括timeEvent
和fileEvent
都是在aeProcessEvents()
方法中進行處理的,剛方法實現如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 |
/* Process every pending time event, then every pending file event * (that may be registered by time event callbacks just processed). * Without special flags the function sleeps until some file event * fires, or when the next time event occurs (if any). * * If flags is 0, the function does nothing and returns. * if flags has AE_ALL_EVENTS set, all the kind of events are processed. * if flags has AE_FILE_EVENTS set, file events are processed. * if flags has AE_TIME_EVENTS set, time events are processed. * if flags has AE_DONT_WAIT set the function returns ASAP until all * the events that's possible to process without to wait are processed. * * The function returns the number of events processed. */ int aeProcessEvents(aeEventLoop *eventLoop, int flags) { int processed = 0, numevents; /** * 既沒有時間事件也沒有檔案事件,則直接返回 */ if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0; /** * -1 == eventloop->maxfd 表示還麼有任何aeFileEvent被新增到epoll * 事件迴圈中進行監聽 */ if (eventLoop->maxfd != -1 || ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) { int j; aeTimeEvent *shortest = NULL; struct timeval tv, *tvp; /** * 如果有aeFileEvent需要處理,就先要從所有待處理的 * aeTimeEvent事件中找到最近的將要被執行的aeTimeEvent節點 * 並結算該節點觸發時間 */ if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT)) shortest = aeSearchNearestTimer(eventLoop); if (shortest) { long now_sec, now_ms; aeGetTime(&now_sec, &now_ms); tvp = &tv; /* How many milliseconds we need to wait for the next * time event to fire? */ // 計算epoll_wait()需要等待的時間 long long ms = (shortest->when_sec - now_sec)*1000 + shortest->when_ms - now_ms; if (ms > 0) { tvp->tv_sec = ms/1000; tvp->tv_usec = (ms % 1000)*1000; } else { tvp->tv_sec = 0; tvp->tv_usec = 0; } } else { // 如果flags設定了AE_DONT_WAIT,則設定epoll_wait()等待時間為0, // 即立刻從epoll中返回 if (flags & AE_DONT_WAIT) { tv.tv_sec = tv.tv_usec = 0; tvp = &tv; } else { /* Otherwise we can block */ tvp = NULL; /* wait forever */ } } // 呼叫aeApiPoll()進行阻塞等待事件的到來,等待時間為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. */ // fe->mask && mask 的目的是確保對應事件時候還有效 if (fe->mask & mask & AE_READABLE) { 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) // 處理aeTimeEvent processed += processTimeEvents(eventLoop); return processed; /* return the number of processed file/time events */ } |
該方法的入參flag
表示要處理哪些事件,可以取以下幾個值 :
AE_ALL_EVENTS
:timeEvent
和fileEvent
都會處理。AE_FILE_EVENTS
:只處理fileEvent
。AE_TIME_EVENTS
:只處理timeEvent
。AE_DONT_WAIT
:要麼立馬返回,要麼處理完那些不需要等待的事件之後再立馬返回。
aeProcessEvents()
方法會做下面幾件事:
- 判斷傳入的
flag
的值,如果既不包含AE_TIME_EVENTS
也不包含AE_FILE_EVENTS
則直接返回。 - 計算如果有
aeFileEvent
事件需要進行處理,則先計算epoll_wait()
方法需要阻塞等待的時間,計算方式如下:- 先從
aeTimeEvent
事件連結串列中找到最近的需要被觸發的aeTimeEvent
節點並計算需要被觸發的時間,該被觸發時間則為epoll_wait()
需要等待的時間。 - 如果沒有找到最近的
aeTimeEvent
節點,表示沒有aeTimeEvent
節點被加入連結串列,則判斷傳入的flags
是否包含AE_DONT_WAIT
選項,則設定epoll_wait()
需要等待時間為0,表示立即返回。 - 如果沒有設定
AE_DONT_WAIT
,則設定需要等待時間為NULL
,表示epoll_wait()
一直阻塞等待知道有fileEvent
事件到來。
- 先從
- 呼叫
aeApiPoll()
方法阻塞等待事件的到來,阻塞時間為第二步中計算的時間。aeApiPoll()
實現如下:
1234567891011121314151617181920212223242526272829static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {aeApiState *state = eventLoop->apidata;int retval, numevents = 0;// 1. 根據傳入的tvp計算需要等待時間,然後呼叫epoll_wait()進行阻塞等待retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/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;// 2. 計算到來的event的型別if (e->events & EPOLLIN) mask |= AE_READABLE;if (e->events & EPOLLOUT) mask |= AE_WRITABLE;if (e->events & EPOLLERR) mask |= AE_WRITABLE;if (e->events & EPOLLHUP) mask |= AE_WRITABLE;// 3. 把有事件發生的fd以及對應的mask型別拷貝到eventloop->fired陣列中eventLoop->fired[j].fd = e->data.fd;eventLoop->fired[j].mask = mask;}}return numevents;}
aeApiPoll()
會做下面幾件事:- 根據傳入的
tvp
計算需要阻塞的時間,然後呼叫epoll_wait()
進行阻塞等待。 - 有事件到來之後先計算對應事件的型別。
- 把事件發生的
fd
以及對應的型別mask
拷貝到fired
陣列中。
- 根據傳入的
- 從
aeApiPoll()
方法返回之後,所有事件已經就緒了的fd
以及對應事件的型別mask
已經儲存在eventLoop->fired[]
陣列中。依次遍歷fired
陣列,根據mask
型別,執行對應的frileProc()
或者wfileProce()
方法。 - 如果傳入的
flags
中有AE_TIME_EVENTS
,則呼叫processTimeEvents()
執行所有已經到時間了的timeEvent
。