引子
感覺這東西看過不記一下總會忘,所以手不能懶,及時總結一下。
本文主要針對Redis的服務端模型進行分析,力爭能有總體的思路和部分細緻的深入。源碼版本3.2.8.
正文
Redis服務端一個典型的單線程reactor模型,使用I/O多路複用來完成對文件描述符的監聽,然後主線程依次處理就緒的事件。
I/O多路複用
思路非常的簡單,首先我們知道I/O多路複用有好幾種方式,而這常常是和平臺相關的,所以爲了實現的簡潔,擴展性,跨平臺性,Redis在這裏進行了一層封裝,通過一套統一的API完成整個網絡通信部分。
以Linux下的epoll爲例,讓我們來看一下。
typedef struct aeApiState {
int epfd;
struct epoll_event *events;
} aeApiState;
static int aeApiCreate(aeEventLoop *eventLoop);
初始化,主要是分配內存和調用epoll_create生成epoll監聽fd,eventLoop結構體是服務端事件驅動的結構體。
static int aeApiResize(aeEventLoop *eventLoop, int setsize);
eventLoop記錄了能監控的事件最大數,這裏重新調整大小。
static void aeApiFree(aeEventLoop *eventLoop);
釋放直接分配給eventLoop的內存空間
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask);
向eventLoop裏面添加要監控的事件,對於epoll來說就是epoll_ctl(EPOLL_CTL_ADD)
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask);
刪除監控的事件,對於epoll來說就是epoll_ctl(EPOLL_CTL_DEL)
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp);
I/O多路複用的阻塞調用,對於epoll來說就是epoll_wait,第二個參數表示epoll要㩐待的最長時間。
static char *aeApiName(void);
返回封裝的I/O多路複用實現,對於epoll來說就是返回“epoll”
而在選擇I/O多路複用的實現時,是按照性能的從高到低。
在Redis的網絡庫ae.c最前面是這樣的
#ifdef HAVE_EVPORT
#include "ae_evport.c" // Solaris
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c" // Linux
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c" // BSD
#else
#include "ae_select.c"
#endif
#endif
#endif
自然,跨平臺的select作爲保留選擇,但是由於其性能原因,放在最後了。
兩種事件
Redis的服務端通過事件驅動,I/O事件和定時事件,I/O事件我們都很熟悉,便是可讀/寫或者accept返回的fd,而定時事件的處理之前則沒怎麼接觸,最近也趁機好好學習了《高性能服務端》裏面的 定時器 一章。所以在這裏順便展開小說一下定時器。
定時器
定時器裏面有兩個最基本的屬性,即超時時間和處理函數(回調函數),當然可能還有其他的屬性,比如是否需要重啓等等,一個定時器就是一個定時事件,我們可以通過鏈表、時間輪、最小堆來組織定時事件。
而在監控定時事件時可以通過信號(定時發信號檢測是否到時),I/O多路複用(超時參數)來實現。
服務端事件驅動流程
在服務端啓動之後,首先會進行各種初始化工作,在初始化結束之後,便進入aeMain,開始執行服務端的事件循環,等待客戶端連接。
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0; // 終止flag
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
可以看到這裏便是不斷調用aeProcessEvents,並且監聽兩種種類事件(AE_ALL_EVENTS)
事件種類
#define AE_FILE_EVENTS 1 // I/O事件
#define AE_TIME_EVENTS 2 // 定時事件
#define AE_ALL_EVENTS (AE_FILE_EVENTS|AE_TIME_EVENTS)
每次aeProcessEvents都是一次I/O多路複用的輪詢,讓我們來看一下這個核心函數的細節
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
eventLoop就是我們的事件驅動主結構體,flag則記錄了需要關心的事件(主循環似乎是會關心所有事件,這裏我認爲主要體現了Redis網網絡庫的拓展性,程序也許只需要關心一種事件)。
if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS))
return 0;
/*
既然只有兩種事件,那麼這兩種事件都不關心自然是直接結束了(return ASAP--as soon as possible)
*/
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;
aeGetTime(&now_sec, &now_ms);//獲取現在的時間(秒和毫秒)
tvp = &tv;
long long ms =
(shortest->when_sec - now_sec)*1000 +
shortest->when_ms - now_ms;
//計算距離到時還有多少時間
if (ms > 0) { // 如果定時事件還沒到達時間,將剩餘時間記錄在tvp中
tvp->tv_sec = ms/1000;
tvp->tv_usec = (ms % 1000)*1000;
} else {// 已經有定時事件到時了,那麼就不等待,將tvp設置爲0
tvp->tv_sec = 0;
tvp->tv_usec = 0;
}
} else { //shortest爲NULL,目前沒有等待的定時事件
if (flags & AE_DONT_WAIT) { /AE_DONT_WAIT被設置則不需要等待
tv.tv_sec = tv.tv_usec = 0;
tvp = &tv;
} else {
//永遠等待直到有事件發生
tvp = NULL;
}
}
經過這一步,我們已經計算出了多路複用需要的超時時間保存在tvp裏了。然後就是對監聽的fd進行輪詢了。
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;
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++;// 成功處理的事件數加1
}
}
// 可以看到,當一個fd 上既可讀又可寫時是優先處理可讀事件的
因爲我們剛纔設置的傳給aeApiPoll的時間是定時事件剛好到時的剩餘時間,所以現在定時事件已經到時了,我們還需要再去處理定時事件。
當然,因爲我們是處理完所有非定時事件之後,才處理定時事件的,所以實際處理定時事件可能要比預定的時間慢一點。
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);
return processed;
讓我們再來看processTimeEvents的細節。
首先是一個異常處理,如果系統時間發生錯亂被移到未來(moved to the future),這裏我們就不關心細節了。
int processed = 0;
aeTimeEvent *te, *prev;
long long maxId;
time_t now = time(NULL); // 獲取現在的時間
eventLoop->lastTime = now; // 設置時間
prev = NULL; // 前驅指針
te = eventLoop->timeEventHead;// te就是定時事件鏈表頭
maxId = eventLoop->timeEventNextId-1;//定時事件的最大ID
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)// 清理資源
te->finalizerProc(eventLoop, te->clientData);
zfree(te);// 釋放資源
te = next;
continue;
}
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++; //處理數+1
if (retval != AE_NOMORE) { //如果這個事件之後還要執行,也就是隔一段時間再執行的事件
aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms); //添加時間
} else {
te->id = AE_DELETED_EVENT_ID;//否則設置刪除標誌
}
}
prev = te;
te = te->next;//處理下一個事件
}
return processed;// 返回處理數
至此,一次對時間事件處理就結束了,aeProcessEvents也就結束了。
而服務端主要就是一個死循環aeProcessEvents來進行事件處理。
參考閱讀
《Redis設計與實現》中的 第12章事件,第14章服務器。
小夥伴Tanswer之前也分析了ae.c的源碼,並且還有之前學長利用ae寫的demo,請移步 Redis網絡庫源碼淺解