Redis的網絡模型是基於I/O多路複用程序來實現的。源碼中包含四種多路複用函數庫epoll、select、evport、kqueue。在程序編譯時會根據系統自動選擇這四種庫其中之一。下面以epoll爲例,來分析Redis的I/O模塊的源碼。redis
Redis網絡事件處理模塊的代碼都是圍繞epoll那三個系統方法來寫的。先把這三個方法弄清楚,後面就不難了。api
建立epoll實例
參數:表示該 epoll 實例最多可監聽的 socket fd(文件描述符)數量。
返回: epoll 專用的文件描述符。數組
管理epoll中的事件,對事件進行註冊、修改和刪除。服務器
參數: epfd:epoll實例的文件描述符; op:取值三種:EPOLL_CTL_ADD 註冊、EPOLL_CTL_MOD 修 改、EPOLL_CTL_DEL 刪除; fd:socket的文件描述符; epoll_event *event:事件
event表明一個事件,相似於Java NIO中的channel「通道」。epoll_event 的結構以下:網絡
typedef union epoll_data { void *ptr; int fd; /* socket文件描述符 */ __uint32_t u32; __uint64_t u64; } epoll_data_t; struct epoll_event { __uint32_t events; /* Epoll events 就是各類待監聽操做的操做碼求與的結果,例如EPOLLIN(fd可讀)、EPOLLOUT(fd可寫) */ epoll_data_t data; /* User data variable */ };
等待事件是否就緒,相似於Java NIO中 select 方法。若是事件就緒,將就緒的event存入events數組中。socket
參數 epfd:epoll實例的文件描述符; events:已就緒的事件數組; intmaxevents:每次能處理的事件數; timeout:阻塞時間,等待產生就緒事件的超時值。
Redis事件系統中將事件分爲兩種類型:函數
下面從事件的註冊、觸發兩個流程對源碼進行分析oop
在 initServer方法(由 redis.c 的 main 函數調用) 中,在創建 RedisDb 對象的同時,會初始化一個「eventLoop」對象,我稱之爲事件處理器對象。結構體的關鍵成員變量以下所示:源碼分析
struct aeEventLoop{ aeFileEvent *events;//已註冊的文件事件數組 aeFiredEvent *fired;//已就緒的文件事件數組 aeTimeEvent *timeEventHead;//時間事件數組 ... }
初始化 eventLoop 在 ae.c 的「aeCreateEventLoop」方法中執行。該方法中除了初始化 eventLoop 還調用以下方法初始化了一個 epoll 實例。ui
/* * ae_epoll.c * 建立一個新的 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); /* 1024 is just a hint for the kernel */ if (state->epfd == -1) { zfree(state->events); zfree(state); return -1; } // 賦值給 eventLoop eventLoop->apidata = state; return 0; }
也正是在此處調用了系統方法「epoll_create」。這裏的state是一個aeApiState結構,以下所示:
/* * 事件狀態 */ typedef struct aeApiState { // epoll 實例描述符 int epfd; // 事件槽 struct epoll_event *events; } aeApiState;
這個 state 由 eventLoop->apidata 來記錄。
經過 listenToPort 方法開啓TCP端口,每一個IP端口會對應一個文件描述符 ipfd(由於服務器可能會有多個ip地址)
// 打開 TCP 監聽端口,用於等待客戶端的命令請求 if (server.port != 0 && listenToPort(server.port,server.ipfd,&server.ipfd_count) == REDIS_ERR) exit(1);
注意:*eventLoop 和 ipfd 分別被 server.el 和 server.ipfd[] 引用。server 是結構體 RedisServer 的實例,是Redis的全局變量。
以下所示代碼,爲每個文件描述符綁定一個事件函數
// initServer方法: for (j = 0; j < server.ipfd_count; j++) { if (aeCreateFileEvent(server.el, server.ipfd[j], AE_READABLE, acceptTcpHandler,NULL) == AE_ERR) { redisPanic( "Unrecoverable error creating server.ipfd file event."); } } // ae.c 中的 aeCreateFileEvent 方法 /* * 根據 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; }
aeCreateFileEvent 函數中有一個方法調用:aeApiAddEvent,代碼以下
/* * ae_epoll.c * 關聯給定事件到 fd */ static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) { aeApiState *state = eventLoop->apidata; struct epoll_event ee; /* If the fd was already monitored for some event, we need a MOD * operation. Otherwise we need an ADD operation. * * 若是 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; }
這裏實際上就是調用系統方法「epoll_ctl」,將事件(文件描述符)註冊進 epoll 中。首先要封裝一個 epoll_event 結構,即 ee ,經過「epoll_ctl」將其註冊進 epoll 中。
除此以外,aeCreateFileEvent 還完成了下面兩個重要操做:
redis.c 的main函數會調用 ae.c 中的 main 方法,以下所示:
/* * 事件處理器的主循環 */ void aeMain(aeEventLoop *eventLoop) { eventLoop->stop = 0; while (!eventLoop->stop) { // 若是有須要在事件處理前執行的函數,那麼運行它 if (eventLoop->beforesleep != NULL) eventLoop->beforesleep(eventLoop); // 開始處理事件 aeProcessEvents(eventLoop, AE_ALL_EVENTS); } }
上述代碼會調用 aeProcessEvents 方法用於處理事件,方法以下所示
/* Process every pending time event, then every pending file event * (that may be registered by time event callbacks just processed). * * 處理全部已到達的時間事件,以及全部已就緒的文件事件。 * 函數的返回值爲已處理事件的數量 */ int aeProcessEvents(aeEventLoop *eventLoop, int flags) { int processed = 0, numevents; /* Nothing to do? return ASAP */ if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0; 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; /* Calculate the time missing for the nearest * timer to fire. */ // 計算距今最近的時間事件還要多久才能達到 // 並將該時間距保存在 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 we have to check for events but need to return * ASAP because of AE_DONT_WAIT we need to set the timeout * to zero */ if (flags & AE_DONT_WAIT) { // 設置文件事件不阻塞 tv.tv_sec = tv.tv_usec = 0; tvp = &tv; } else { /* Otherwise we can block */ // 文件事件能夠阻塞直到有事件到達爲止 tvp = NULL; /* wait forever */ } } // 處理文件事件,阻塞時間由 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. */ // 讀事件 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++; } } /* Check time events */ // 執行時間事件 if (flags & AE_TIME_EVENTS) processed += processTimeEvents(eventLoop); return processed; }
該函數中代碼大體分爲三個主要步驟
ae_epoll.c 中的 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; // 爲已就緒事件設置相應的模式 // 並加入到 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後,就緒的事件會被寫入 eventLoop->apidata->events 事件槽。後面的循環就是將事件槽中的事件寫入到 eventLoop->fired[] 中。具體描述:每個事件都是一個 epoll_event 結構,用e來指代,則e.data.fd表明文件描述符,e->events表示其操做碼,將操做碼轉化爲mask,最後將fd 和 mask 都寫入eventLoop->fired[j]中。
以後,在外層的 aeProcessEvents 方法中會執行函數指針 rfileProc 或者 wfileProc 指向的方法,例如前文提到已註冊的「acceptTcpHandler」。
Redis的網絡模塊實際上是一個簡易的Reactor模式。本文順着「服務端註冊事件——>接受客戶端鏈接——>監聽事件是否就緒——>執行事件」這樣的路線,來分析Redis源碼,描述了Redis接受客戶端connect的過程。實際上NIO的思想都基本相似。