文章基於redis-4.0.1源碼詳細介紹一下redis的事件模型。
一、redis事件模型概覽
redis是一個事件驅動的服務程序,在redis的服務程序中存在兩種類型的事件,分別是文件事件和時間事件。文件事件是對網絡通信操作的統稱,時間事件是redis中定時運行的任務或者是週期性的任務(目前redis中只有serverCron這一個週期性時間事件,並沒有定時時間事件)。對於事件驅動類的程序,非常適合使用Reactor模式進行設計(如果要詳細瞭解Reactor模式,請參考超鏈接中的博客)。redis也不例外,在文件事件處理的設計中採用了Reactor設計模式。
下面對應於鏈接博客中Reactor模式圖仔細講解一下redis如何使用Reactor模式實現高效的文件事件模型。爲了方便,首先將Reactor設計模式圖作爲圖1放在本文中。
圖 1 Reactor設計模式圖
Reactor模式包含四部分,分別是Handle(對於系統資源的一種抽象,在redis中就是監聽描述符或者是連接描述符)、Synchronous Event Demultiplexer(同步事件分離器,在redis中對應於IO多路複用程序)、Event Handler(事件處理器,在redis中對應於連接應答處理器、命令請求處理器以及命令回覆處理器、事件處理器等)和Initiation Dispatcher(事件分派器,在redis中對應於ae.c/aeProcessEvents函數)。
在redis中將感興趣的事件及類型(讀、寫)通過IO多路複用程序註冊到內核中並監聽每個事件是否發生。當IO多路複用程序返回的時候,如果有事件發生,redis在封裝IO多路複用程序時,將所有已經發生的事件及該事件的類型封裝爲aeFiredEvent類型,放到aeEventLoop的fired成員中,形成一個隊列。通過這個隊列,redis以有序、同步、每次一個套接字事件的方式向文件事件分派器傳送套接字,並處理髮生的文件事件。redis處理事件(無論是文件事件還是時間事件)都是以原子的方式進行的,中間不存在事件之間的搶佔。這很容易理解,redis是單線程模型,不存在處理上的併發操作。
最後需要說明的是redis首先處理髮生的文件事件,然後纔會處理時間事件,這點我們在介紹redis源碼aeProcessEvents的時候會詳細註釋和介紹。
二、redis實現事件模型使用的數據結構
redis表示事件模型的數據結構是對該事件標識、事件類型和事件處理函數的一種抽象,就是Reactor模式中的Handle和Event Handle的集合。redis使用了四種數據結構描述redis中的事件,前三種數據結構是對redis中某種特定類型事件的一種抽象,最後一種數據結構aeEventLoop是redis管理所有事件的一種抽象。aeTimeEvent中的id成員、aeFiredEvent中的fd成員都是Reactor模式中所說的Handle的具體表現,但是好像aeFileEvents並沒有對應的handle。其實,redis在aeEventLoop的events成員中使用每一個描述符fd作爲下標,該下標的對應值爲aeFileEvent成員,由此將描述符fd與對該fd感興趣的事件類型以及處理函數相關聯,對應於Reactor中Handle與Event Handler的關聯。當通過aeEventLoop中的fired獲取到已經發生的事件fd及其類型mask的時候,由fd和mask在aeEventLoop的events成員中獲取對應的事件處理器,處理已經發生的事件。也就是說,文件事件的處理是聯合使用了fired和events兩個成員變量;時間事件的處理使用aeTimeEvent變量。
文件事件數據結構。
/* 文件事件 */
typedef struct aeFileEvent {
/* 套接字發生的事件,讀事件或者寫事件其中的一種 */
int mask; /* one of AE_(READABLE|WRITABLE) */
/* 讀事件處理器,回調函數 */
aeFileProc *rfileProc;
/* 寫事件處理器,回調函數 */
aeFileProc *wfileProc;
/* 客戶端數據 */
void *clientData;
} aeFileEvent;
時間事件數據結構。
typedef struct aeTimeEvent {
/* 時間事件,每個時間事件通過id唯一標識 */
long long id;
/* 時間事件應該觸發的時間,單位:s */
long when_sec;
/* 時間事件被觸發的時間,單位:ms */
long when_ms;
/* 時間事件處理函數 */
aeTimeProc *timeProc;
aeEventFinalizerProc *finalizerProc;
/* 客戶端數據 */
void *clientData;
/* 時間事件形成的鏈條 */
struct aeTimeEvent *next;
} aeTimeEvent;
已經發生的文件事件數據結構。
/* 已經發生的文件事件 */
typedef struct aeFiredEvent {
int fd;
int mask;
} aeFiredEvent;
redis中時間管理結構體,包含了文件事件、時間事件、已發生的文件事件等相關信息。
/* redis中的事件管理結構體 */
typedef struct aeEventLoop {
/* 當前IO程序追蹤的最大的文件描述符,大於此值的setsize範圍內的值,沒有意義*/
int maxfd;
/* 當前感興趣集合的大小, setsize > maxfd */
int setsize;
/* 下一個時間事件的id */
long long timeEventNextId;
/* 用於修正系統時鐘的偏移,具體參考aeProcessTimeEvents */
time_t lastTime;
/* 註冊的感興趣的文件事件 */
aeFileEvent *events;
/* 被觸發的文件事件指針,也就是上文所說的已經發生的文件事件形成的隊列 */
aeFiredEvent *fired;
/* 時間事件形成的鏈表(無序鏈表) */
aeTimeEvent *timeEventHead;
/* 事件停止標誌 */
int stop;
/* 針對特定API需要的數據結構, 通過該數據結構屏蔽掉IO多路複用
* 不同底層實現的需要的不同數據結構
*/
void *apidata;
aeBeforeSleepProc *beforesleep;
aeBeforeSleepProc *aftersleep;
} aeEventLoop;
三、redis中的IO多路複用機制
redis中的IO多路複用機制對應於Reactor模式中的同步事件分離器。redis考慮到不同系統可能支持不同的的IO多路複用機制,因此實現了select、epoll、kqueue和evport四種不同的IO多路複用,並且每種IO多路複用機制都提供了完全相同的外部接口,根據ae.c中的條件編譯語句選擇的順序依次是evport、epoll、kequeue和select,隔離了系統對IO多路複用機制支持的差異。
關於IO多路複用機制本篇不做詳細介紹,以後會專門開一篇博客介紹同步IO、同步IO的多路複用以及異步IO。
本篇以epoll爲例,介紹redis如何封裝常見的幾種IO多路複用。redis對於所有IO多路複用機制的封裝都是類似的。
前面介紹redis中管理所有事件使用的結構體aeEventLoop的時候說過,apidata成員就是用於隔離不同IO多路複用機制需要的底層數據結構差異的。在redis封裝的所有IO多路複用機制中,apidata都是指向爲該機制封裝的aeApiState結構的,aeApiState封裝了該IO多路複用機制使用的底層變量。以epoll爲例。
typedef struct aeApiState {
/* 爲epoll重新創建新的文件描述符,管理所有註冊到內核的文件描述符 */
int epfd;
/* epoll機制使用的結構體,用於在epoll_wait調用中返回已經發生的文件事件信息 */
struct epoll_event *events;
} aeApiState;
redis爲每種IO多路複用機制提供了初始化函數aeApiCreate,被aeCreateEventLoop的函數調用。aeApiCreate就是爲了初始化該IO多路複用機制使用的數據結構。還是以epoll爲例。
static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState));
if (!state) return -1;
/* 根據aeEventLoop中的setsize確定要監控的文件事件的數量,在內存中分配能夠容納足夠epoll_event數量的內存空間 */
state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
if (!state->events) {
zfree(state);
return -1;
}
/* epoll_create(int size)的size參數只是對內核的一種建議,通知內核要監聽size個fd。
* size指的並不是最大的後備存儲設備,而是衡量內核內部結構大小的一個提示,當創建成功後會佔用一個監聽描述符(返回值),
* 所以在使用完之後,應該調用close(),否則fd可能會耗盡;
* Linux2.6.8版本之後,size值其實沒什麼用了,不過要大於0,因爲內核可以動態的分配大小,所以不需要size這個提示了
*/
state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */
if (state->epfd == -1) {
zfree(state->events);
zfree(state);
return -1;
}
eventLoop->apidata = state;
return 0;
}
redis爲每種IO多路複用機制提供了增加監聽特定類型的事件到內核中的接口,aeApiAddEvent;當然也提供了在內核中刪除被監聽事件的特定事件類型的接口,aeApiDeleteEvent,分別如下。
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee = {0}; /* avoid valgrind warning */
/* 如果fd已經與一些事件進行了關聯(fd有自己感興趣的事件),那麼修改對應的感興趣事件;
* 否則增加對應的感興趣事件
*/
int op = eventLoop->events[fd].mask == AE_NONE ?
EPOLL_CTL_ADD : EPOLL_CTL_MOD;
ee.events = 0;
/* 取監聽事件的並集 */
mask |= eventLoop->events[fd].mask;
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.fd = fd;
/* 添加或者修改fd對應的感興趣的事件類型到內核中 */
if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
return 0;
}
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee = {0}; /* avoid valgrind warning */
int mask = eventLoop->events[fd].mask & (~delmask);
ee.events = 0;
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
ee.data.fd = fd;
if (mask != AE_NONE) {
epoll_ctl(state->epfd,EPOLL_CTL_MOD,fd,&ee);
} else {
/* Note, Kernel < 2.6.9 requires a non null event pointer even for
* EPOLL_CTL_DEL. */
epoll_ctl(state->epfd,EPOLL_CTL_DEL,fd,&ee);
}
}
將事件的特定類型增加到內核之後,內核便針對所有已經添加到內核中的事件進行監控。當事件發生、等待超時或者接收到某種信號的時候,IO多路複用程序返回,但是隻有當其中監控的事件真正發生的時候返回大於0的值,其他情況返回的都是小於等於0的值。當被監聽的事件發生的時候,在每種IO多路複用機制中的aeApiPoll接口中對所有已經發生的事件執行入隊操作。
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {
aeApiState *state = eventLoop->apidata;
int retval, numevents = 0;
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;
/* struct epoll_event中的event成員保存了該文件描述符fd所發生的事件 */
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;
/* 敲黑板,這裏是我們上面所說到的redis中存放已經發生的事件時對隊列執行的入隊操作 */
/* 在這裏將已經發生的事件形成隊列存放在fired成員中,在時間分派器aeProcessEvents
* 中對該隊列中的事件進行處理
*/
eventLoop->fired[j].fd = e->data.fd;
eventLoop->fired[j].mask = mask;
}
}
return numevents;
}
四、redis的事件分派器
在redis中,ae.c文件提供的對外API屏蔽掉了操作系統底層實現的不同,將對文件事件和時間事件的處理通過統一的接口操作。下面我們詳細說明一下redis中作爲事件分派器的aeProcessEvents函數和時間事件處理函數processTimeEvents。
redis在aeProcessEvents函數中處理文件事件和時間事件,且先處理文件事件再處理時間事件。flags指定redis是處理時間事件還是文件事件又或者是兩種事件的並集,這點很容易理解,我們只是想說明一下flags中的另一個標誌位---就是獲取就緒文件事件的時候是否阻塞的標誌位,AE_DONT_WAIT標誌。按照Reactor設計模式,在文件事件分派器上調用同步事件分離器,獲取已經就緒的文件事件。調用同步事件分離器就是要調用IO多路複用函數,而IO多路複用函數有可能阻塞(依據傳入的時間參數,決定不阻塞、永久阻塞還是阻塞特定的時間段)。爲了防止redis線程長時間阻塞在文件事件等待就緒上而耽誤了及時處理到時的時間事件,並且防止redis過多重複性的遍歷時間事件形成的無序鏈表,redis在aeProcessEvents的實現中通過設置flags中的AE_DONT_WAIT標誌位達到以上目的。具體參考aeProcessEvents中的註釋。
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
int processed = 0, numevents;
/* 所有的事件都不進行處理 */
if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
/* 首先判斷是否存在需要監聽的文件事件,如果存在需要監聽的文件事件,那麼通過IO多路複用程序獲取
* 準備就緒的文件事件,至於IO多路複用程序是否等待以及等待多久的時間,依發生時間距離現在最近的時間事件確定;
* 如果eventLoop->maxfd == -1表示沒有需要監聽的文件事件,但是時間事件肯定是存在的(serverCron()),
* 如果此時沒有設置AE_DONT_WAIT標誌位,此時調用IO多路複用,其目的就不是爲了監聽文件事件準備就緒了,
* 而是爲了使線程休眠到發生時間距離現在最近的時間事件的發生時間(作用類似於unix中的sleep函數),
* 這種休眠操作的目的是爲了避免線程一直不停的遍歷時間事件形成的無序鏈表,造成不必要的資源浪費
*/
if (eventLoop->maxfd != -1 ||
((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
int j;
aeTimeEvent *shortest = NULL;
struct timeval tv, *tvp;
/* 尋找發生時間距離現在最近的時間事件,該時間事件的發生時間與當前時間之差就是IO多路複用程序應該等待的時間 */
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;
long long ms =
(shortest->when_sec - now_sec)*1000 +
shortest->when_ms - now_ms;
/* 如果時間之差大於0,說明時間事件到時時間未到,則等待對應的時間;
* 如果時間間隔小於0,說明時間事件已經到時,此時如果沒有
* 文件事件準備就緒,那麼IO多路複用程序應該立即返回,以免
* 耽誤處理時間事件
*/
if (ms > 0) {
tvp->tv_sec = ms/1000;
tvp->tv_usec = (ms % 1000)*1000;
} else {
tvp->tv_sec = 0;
tvp->tv_usec = 0;
}
} else {
/* 沒有找到距離現在最近的時間事件,且設置了AE_DONT_WAIT標誌位,
* 立即從IO多路複用程序返回
*/
if (flags & AE_DONT_WAIT) {
tv.tv_sec = tv.tv_usec = 0;
tvp = &tv;
} else {
/* 沒有設置AE_DONT_WAIT標誌位,且沒有找到發生時間距離現在最近的時間事件,
* IO多路複用程序可以無限等待
*/
tvp = NULL;
}
}
/* 典型的reator設計模式。作爲事件分派器,
* 將已經發生的文件事件交給對應的eventHandle處理
*/
numevents = aeApiPoll(eventLoop, tvp);
/* After sleep callback. */
if (eventLoop->aftersleep != NULL && flags & AE_CALL_AFTER_SLEEP)
eventLoop->aftersleep(eventLoop);
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;
/* 如果IO多路複用程序同時監聽fd的讀事件和寫事件,
* 則當該fd對應的讀、寫事件都返回可用的時候,
* 服務器首先處理讀套接字、後處理寫套接字
*/
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++;
}
}
/* 處理時間事件 */
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);
return processed; /* return the number of processed file/time events */
}
在redis中將對文件事件的處理直接放到了aeProcessEvents中,但是對於時間事件的處理卻是存在單獨的函數,aeProcessTimeEvents。
static int processTimeEvents(aeEventLoop *eventLoop) {
int processed = 0;
aeTimeEvent *te, *prev;
long long maxId;
time_t now = time(NULL);
/* 系統的始終如果發生了漂移,那麼所有的時間事件應該立即被處理;
* 將te->when_sec設置爲0,表示所有的時間事件都能夠被處理。如果時間事件沒有到時,
* 那麼當前立即處理也不存在什麼問題;如果時間事件確實已經到時,那確實應該被處理
*/
if (now < eventLoop->lastTime) {
te = eventLoop->timeEventHead;
while(te) {
te->when_sec = 0;
te = te->next;
}
}
/* 糾正系統時鐘 */
eventLoop->lastTime = now;
prev = NULL;
te = eventLoop->timeEventHead;
maxId = eventLoop->timeEventNextId-1;
while(te) {
long now_sec, now_ms;
long long id;
/* 在aeDeleteTimeEvent函數中刪除掉時間事件只是將時間事件的id置爲無效的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)
te->finalizerProc(eventLoop, te->clientData);
/* 釋放時間事件 */
zfree(te);
te = next;
continue;
}
/* Make sure we don't process time events created by time events in
* this iteration. Note that this check is currently useless: we always
* add new timers on the head, however if we change the implementation
* detail, this check may be useful again: we keep it here for future
* defense. */
if (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))
{
int retval;
id = te->id;
retval = te->timeProc(eventLoop, id, te->clientData);
processed++;
/* 要求timeProc返回該時間事件是否需要繼續,如果不需要再繼續那麼返回AE_NOMOER;
* 如果是週期性的事件,那麼需要需要繼續,則返回下一次發生的時間距離現在的毫秒數。
* 如果是定時事件,則該事件不需要再次執行,返回AE_NOMORE
*/
/* 週期性時間,在處理完這次事件之後,重新設定下一次該事件應該執行的時間,以便週期性進行調度 */
if (retval != AE_NOMORE) {
aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);
} else {
/* 重新留下了無效的時間事件id,等待下一次調用處理時間事件的函數的時候,刪除掉該事件 */
te->id = AE_DELETED_EVENT_ID;
}
}
prev = te;
te = te->next;
}
return processed;
}
redis所有的事件都是在aeProcessEvents中處理的,aeProcessEvents被aeMain調用。
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
/* 在整個循環中不斷地處理時間事件和文件事件,構成了redis運行的主體 */
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
}
}
上面理解如果有不正確的地方,歡迎吐槽。