Redis中的很重要的一部分是對於事件的管理,ae事件庫的最大特點就是簡潔明瞭且高效。本章以epoll爲例,分析Redis對於事件的處理過程。
Redis事件
Redis中事件分成兩種類型,一種是文件事件,一種是時間事件。Redis採用的是單線程Reactor模式(Reactor基本組件:事件,事件處理器,具體事件處理器,事件分發器)。這裏需要強調一點,就是Redis看似事件處理的結構很簡單,但是其性能十分高效,原因就是Redis中的大部分邏輯處理都採用單線程(BGSAVE等除外),這樣使得其避開了線程之間切換的開銷和加鎖解鎖的同步開銷。
EPOLL的封裝
Redis對於epoll的封裝十分簡單,基本上就是添加事件和刪除事件,然後是返回觸發的可執行事件。這裏可以聯合後面的數據結構,仔細看看其設計過程。
/*
* 事件狀態
*/
typedef struct aeApiState {
// epoll_event 實例描述符,用於監聽時間事件和文件事件
int epfd;
// 事件槽
struct epoll_event *events;
} aeApiState;
/*
* 事件處理器的狀態
*/
typedef struct aeEventLoop {
// 目前已註冊的最大描述符
int maxfd;
// 目前已追蹤的最大描述符
int setsize;
// 用於生成時間事件 id
long long timeEventNextId;
// 最後一次執行時間事件的時間
time_t lastTime;
// 已註冊的文件事件,這裏是以fd作爲索引
aeFileEvent *events;
// 已就緒的文件事件,這裏是以fd作爲索引
aeFiredEvent *fired;
// 時間事件
aeTimeEvent *timeEventHead;
// 事件處理器的開關
int stop;
// 多路複用庫的私有數據
void *apidata;
// 在處理事件前要執行的函數
aeBeforeSleepProc *beforesleep;
} aeEventLoop;
/*
* 創建一個新的 epoll 實例,並將它賦值給 eventLoop
*/
static int aeApiCreate(aeEventLoop *eventLoop) {
aeApiState *state = zmalloc(sizeof(aeApiState));
if (!state) return -1;
// 初始化事件槽空間
state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
if (!state->events) {
zfree(state);
return -1;
}
// 創建 epoll 實例
state->epfd = epoll_create(1024); /*看了epoll內部實現就發現1024參數沒啥用*/
if (state->epfd == -1) {
zfree(state->events);
zfree(state);
return -1;
}
// 賦值給 eventLoop
eventLoop->apidata = state;
return 0;
}
/*
* 關聯給定事件到 fd
*/
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee;
/*
* 如果 fd 沒有關聯任何事件,那麼這是一個 ADD 操作。
* 如果已經關聯了某個/某些事件,那麼這是一個 MOD 操作。
*/
int op = eventLoop->events[fd].mask == AE_NONE ?
EPOLL_CTL_ADD : EPOLL_CTL_MOD;
// 註冊事件到 epoll
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.u64 = 0; /* avoid valgrind warning */
ee.data.fd = fd;
if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
return 0;
}
/*
* 從 fd 中刪除給定事件
*/
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask) {
aeApiState *state = eventLoop->apidata;
struct epoll_event ee;
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.u64 = 0; /* avoid valgrind warning */
ee.data.fd = fd;
if (mask != AE_NONE) {
epoll_ctl(state->epfd,EPOLL_CTL_MOD,fd,&ee);
} else {
epoll_ctl(state->epfd,EPOLL_CTL_DEL,fd,&ee);
}
}
/*
* 獲取可執行事件
*/
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;
// 爲已就緒事件設置相應的模式
// 並加入到 eventLoop 的 fired 數組中
numevents = retval;
for (j = 0; j < numevents; j++) {
int mask = 0;
struct epoll_event *e = state->events+j;
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;
eventLoop->fired[j].fd = e->data.fd;
eventLoop->fired[j].mask = mask;
}
}
// 返回已就緒事件個數
return numevents;
}
文件事件
多路複用機制可以讓其在單線程中同時監聽多個文件描述符的連接、讀寫事件。文件事件都是由epoll_wait觸發,而且優先級高於時間事件。
/* File event structure
*
* 文件事件結構
*/
typedef struct aeFileEvent {
// 監聽事件類型掩碼,
// 值可以是 AE_READABLE 或 AE_WRITABLE ,
// 或者 AE_READABLE | AE_WRITABLE
int mask;
// 讀事件處理器
aeFileProc *rfileProc;
// 寫事件處理器
aeFileProc *wfileProc;
// 多路複用庫的私有數據
void *clientData;
} aeFileEvent;
//下面三個函數就是對文件事件的基本操作
/*
* 根據 mask 參數的值,監聽 fd 文件的狀態,
* 當 fd 可用時,執行 proc 函數
*/
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 的指定事件
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;
}
/*
* 將 fd 從 mask 指定的監聽隊列中刪除
*/
void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask)
{
if (fd >= eventLoop->setsize) return;
// 取出文件事件結構
aeFileEvent *fe = &eventLoop->events[fd];
// 未設置監聽的事件類型,直接返回
if (fe->mask == AE_NONE) return;
// 計算新掩碼
fe->mask = fe->mask & (~mask);
if (fd == eventLoop->maxfd && fe->mask == AE_NONE) {
/* Update the max fd */
int j;
for (j = eventLoop->maxfd-1; j >= 0; j--)
if (eventLoop->events[j].mask != AE_NONE) break;
eventLoop->maxfd = j;
}
// 取消對給定 fd 的給定事件的監視
aeApiDelEvent(eventLoop, fd, mask);
}
/*
* 獲取給定 fd 正在監聽的事件類型
*/
int aeGetFileEvents(aeEventLoop *eventLoop, int fd) {
if (fd >= eventLoop->setsize) return 0;
aeFileEvent *fe = &eventLoop->events[fd];
return fe->mask;
}
時間事件
時間事件主要是指定時事件,當超過定時時間的時候,就會觸發時間事件,並調用已經指定的超時事件函數,然後將這個事件刪除。總結一下超時函數做的事件:
- 更新服務器的各類統計信息,比如時間、內存佔用、數據庫佔用等
- 清理數據庫中的過期鍵值對
- 關閉和清理連接失效的客戶端
- 嘗試進行AOF和RDB持久化操作
- 如果是主服務器,就對從服務器進行定期同步
- 如果是集羣模式,對集羣進行定期同步和連接測試
/*
* 時間事件結構
*/
typedef struct aeTimeEvent {
// 時間事件的唯一標識符
long long id;
// 事件的到達時間,秒與毫秒
long when_sec;
long when_ms;
// 事件處理函數
aeTimeProc *timeProc;
// 事件釋放函數
aeEventFinalizerProc *finalizerProc;
// 多路複用庫的私有數據
void *clientData;
// 指向下個時間事件結構,形成鏈表
struct aeTimeEvent *next;
} aeTimeEvent;
/*
* 創建時間事件
*/
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
aeTimeProc *proc, void *clientData,
aeEventFinalizerProc *finalizerProc)
{
// 更新時間計數器
long long id = eventLoop->timeEventNextId++;
// 創建時間事件結構
aeTimeEvent *te;
te = zmalloc(sizeof(*te));
if (te == NULL) return AE_ERR;
// 設置 ID
te->id = id;
// 設定處理事件的時間
aeAddMillisecondsToNow(milliseconds,&te->when_sec,&te->when_ms);
// 設置事件處理器
te->timeProc = proc;
te->finalizerProc = finalizerProc;
// 設置私有數據
te->clientData = clientData;
// 將新事件放入表頭
te->next = eventLoop->timeEventHead;
eventLoop->timeEventHead = te;
return id;
}
/*
* 刪除給定 id 的時間事件
*/
int aeDeleteTimeEvent(aeEventLoop *eventLoop, long long id)
{
aeTimeEvent *te, *prev = NULL;
// 遍歷鏈表
te = eventLoop->timeEventHead;
while(te) {
// 發現目標事件,刪除
if (te->id == id) {
if (prev == NULL)
eventLoop->timeEventHead = te->next;
else
prev->next = te->next;
// 執行清理處理器
if (te->finalizerProc)
te->finalizerProc(eventLoop, te->clientData);
// 釋放時間事件
zfree(te);
return AE_OK;
}
prev = te;
te = te->next;
}
return AE_ERR;
}
事件調度過程
在redis.c中的main函數中首先調用主循環aeMain()函數。然後其中代用aeProcessEvents進行處理事件,這裏注意一下Redis對於事件處理過程是先處理文件事件(如果是讀事件,這裏要根據觸發的fd判斷,是連接請求還是命令請求),再處理時間事件。
這裏還有一個設計思想,因爲要同時處理文件事件和時間事件,所以兩者儘量都減少阻塞的時間,所以文件事件最長阻塞時間設置爲下一個時間事件的到達時間,這樣兩者都不會過長的阻塞。
/*
* 事件處理器的主循環
*/
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) {
// 如果有需要在事件處理前執行的函數,那麼運行它
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
// 開始處理事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
/*
* 處理所有已到達的時間事件,以及所有已就緒的文件事件。
* 如果不傳入特殊 flags 的話,那麼函數睡眠直到文件事件就緒,
* 或者下個時間事件到達(如果有的話)。
* 如果 flags 爲 0 ,那麼函數不作動作,直接返回。
* 如果 flags 包含 AE_ALL_EVENTS ,所有類型的事件都會被處理。
* 如果 flags 包含 AE_FILE_EVENTS ,那麼處理文件事件。
* 如果 flags 包含 AE_TIME_EVENTS ,那麼處理時間事件。
* 如果 flags 包含 AE_DONT_WAIT ,
* 那麼函數在處理完所有不許阻塞的事件之後,即刻返回。
* 函數的返回值爲已處理事件的數量
*/
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
int processed = 0, numevents;
//如果AE_TIME_EVENTS 和 AE_FILE_EVENTS都沒有則返回
if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
/* Note that we want call select() even if there are no
* file events to process as long as we want to process time
* events, in order to sleep until the next time event is ready
* to fire. */
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 是否設置來決定是否阻塞,以及阻塞的時間長度
if (flags & AE_DONT_WAIT) {
// 設置文件事件不阻塞
tv.tv_sec = tv.tv_usec = 0;
tvp = &tv;
} else {
// 文件事件可以阻塞直到有事件到達爲止
tvp = NULL;
}
}
// 處理文件事件,阻塞時間由 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;
// 讀事件
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++;
}
}
// 執行時間事件
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);
return processed; /*返回處理的文件事件和時間事件數目和 */
}
參考:
- Redis設計與實現
- Redis的ae實現