Redis AE異步事件庫實例分析
https://www.jianshu.com/p/da092472080e
Redis使用了一個稱爲“A simple event-driven programming library”的自制異步事件庫(以下簡稱“AE”)。整個事件庫的代碼量少於1k行,是個優秀的C異步事件庫學習材料。
源碼結構
版本 Redis 4.0.8
redis的src目錄下,ae開頭的幾個文件就是AE事件庫的源碼。
文件 | 用途 |
---|---|
ae.h | AE事件庫接口定義 |
ae.c | AE事件庫實現 |
ae_epoll.c | epoll綁定 |
ae_evport.c | evport綁定 |
ae_kqueue.c | kqueue綁定 |
ae_select.c | select綁定 |
文件數量有點多,我們把IO多路複用的綁定都“精簡”掉。在“ae.c”的開頭有這麼一段代碼:
/* Include the best multiplexing layer supported by this system.
* The following should be ordered by performances, descending. */
#ifdef HAVE_EVPORT
#include "ae_evport.c"
#else
#ifdef HAVE_EPOLL
#include "ae_epoll.c"
#else
#ifdef HAVE_KQUEUE
#include "ae_kqueue.c"
#else
#include "ae_select.c"
#endif
#endif
#endif
Redis根據所處系統的不同,包含不同的IO多路複用實現代碼。每種IO多路複用都實現了以下的接口:
struct aeApiState;
static int aeApiCreate(aeEventLoop *eventLoop);
static int aeApiResize(aeEventLoop *eventLoop, int setsize);
static void aeApiFree(aeEventLoop *eventLoop);
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask);
static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int mask);
static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp);
static char *aeApiName(void);
任意的IO多路複用技術,只要封裝出以上的接口就可以被AE事件庫作爲底層實現使用。我們“精簡”掉4個IO多路複用綁定代碼之後,就剩下“ae.h、ae.c”這兩個文件了。
AE事件模型
AE異步事件庫支持以下的事件類型:
- 文件事件
- 定時器事件
Redis本身是一個KV數據庫,主要就是接收客戶端的查詢請求並返回結果,以及對KV數據的有效性維護。所以文件事件(IO事件)和定時器事件就足以支撐服務端的全部功能。
文件(IO)事件
與文件事件相關的定義和接口有:
#define AE_NONE 0
#define AE_READABLE 1
#define AE_WRITABLE 2
typedef void aeFileProc(struct aeEventLoop *eventLoop, int fd, void *clientData, int mask);
/* File event structure */
typedef struct aeFileEvent {
int mask; /* one of AE_(READABLE|WRITABLE) */
aeFileProc *rfileProc;
aeFileProc *wfileProc;
void *clientData;
} aeFileEvent;
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask, aeFileProc *proc, void *clientData);
void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask);
定時器事件
與定時器事件相關的定義和接口有:
typedef int aeTimeProc(struct aeEventLoop *eventLoop, long long id, void *clientData);
typedef void aeEventFinalizerProc(struct aeEventLoop *eventLoop, void *clientData);
/* Time event structure */
typedef struct aeTimeEvent {
long long id; /* time event identifier. */
long when_sec; /* seconds */
long when_ms; /* milliseconds */
aeTimeProc *timeProc;
aeEventFinalizerProc *finalizerProc;
void *clientData;
struct aeTimeEvent *next;
} aeTimeEvent;
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
aeTimeProc *proc, void *clientData,
aeEventFinalizerProc *finalizerProc);
int aeDeleteTimeEvent(aeEventLoop *eventLoop, long long id);
每種事件類型都定義了回調函數、事件結構體、事件添加/刪除接口,以支持該類型事件的操作。
AE異步事件庫的典型用法
下面的例子使用AE事件庫進行網絡讀寫和定時器操作:
/* file: example-libae.c */
#include <stdio.h>
#include <unistd.h>
#include <time.h>
#include <ae.h>
static const int MAX_SETSIZE = 64;
static const int MAX_BUFSIZE = 128;
static void
file_cb(struct aeEventLoop *eventLoop, int fd, void *clientData, int mask)
{
char buf[MAX_BUFSIZE] = {0};
int rc;
rc = read(fd, buf, MAX_BUFSIZE);
if (rc < 0)
{
aeStop(eventLoop);
return;
}
else
{
buf[rc - 1] = '\0'; /* 最後一個字符是回車 */
}
printf("file_cb, read %s, fd %d, mask %d, clientData %s\n", buf, fd, mask, (char *)clientData);
}
static int
timer_cb(struct aeEventLoop *eventLoop, long long id, void *clientData)
{
printf("timer_cb, timestamp %ld, id %lld, clientData %s\n", time(NULL), id, (char *)clientData);
return (5 * 1000);
}
static void
timer_fin_cb(struct aeEventLoop *eventLoop, void *clientData)
{
printf("timer_fin_cb, timestamp %ld, clientData %s\n", time(NULL), (char *)clientData);
}
int main(int argc, char *argv[])
{
aeEventLoop *ae;
long long id;
int rc;
ae = aeCreateEventLoop(MAX_SETSIZE);
if (!ae)
{
printf("create event loop error\n");
goto err;
}
/* 添加文件IO事件 */
rc = aeCreateFileEvent(ae, STDIN_FILENO, AE_READABLE, file_cb, (void *)"test ae file event");
/* 添加定時器事件 */
id = aeCreateTimeEvent(ae, 5 * 1000, timer_cb, (void *)"test ae time event", timer_fin_cb);
if (id < 0)
{
printf("create time event error\n");
aeDeleteEventLoop(ae);
goto err;
}
aeMain(ae);
aeDeleteEventLoop(ae);
return (0);
err:
return (-1);
}
可以把這個文件放在Redis的deps/hiredis/examples目錄下,修改hiredis目錄的Makefile
AE_DIR=/path/to/redis/src
example-libae: examples/example-libae.c $(STLIBNAME)
$(CC) -o examples/$@ $(REAL_CFLAGS) $(REAL_LDFLAGS) -I. -I$(AE_DIR) $< $(AE_DIR)/ae.o $(AE_DIR)/zmalloc.o $(AE_DIR)/../deps/jemalloc/lib/libjemalloc.a -pthread $(STLIBNAME)
編譯執行看一下效果:
$ make example-libae
$ examples/example-libae
123456
file_cb, read 123456, fd 0, mask 1, clientData test ae file event
timer_cb, timestamp 1519137082, id 0, clientData test ae time event
^C
可以看到AE異步事件庫的使用還是比較簡單,沒有複雜的概念和接口。
AE事件循環
一個異步事件庫除了事件模型外,最重要的部分就是事件循環了。來看看AE的事件循環“aeMain”是怎麼實現的:
/* file: ae.c */
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
while (!eventLoop->stop) { /* 運行狀態判斷 */
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop); /* 事件循環前回調執行 */
aeProcessEvents(eventLoop, AE_ALL_EVENTS|AE_CALL_AFTER_SLEEP);
}
}
看來“aeProcessEvents”函數纔是事件循環的主體:
/* file: ae.c */
int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
int processed = 0, numevents;
/* 如果啥事件都不關注,立即返回 */
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;
/* 略去計算tvp的代碼 */
…… ……
/* 調用IO多路複用接口 */
numevents = aeApiPoll(eventLoop, tvp);
/* 略去IO多路複用後回調執行代碼 */
…… ……
for (j = 0; j < numevents; j++) { /* 循環處理IO事件 */
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++;
}
}
/* 執行定時器事件 */
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);
return processed; /* return the number of processed file/time events */
}
在“aeProcessEvents”函數主體中,對於文件(IO)事件的處理邏輯已經比較清晰,調用IO多路複用接口並循環處理返回的有效文件描述符。定時器事件在“processTimeEvents”函數中處理:
/* file: ae.c */
static int processTimeEvents(aeEventLoop *eventLoop) {
int processed = 0;
aeTimeEvent *te, *prev;
long long maxId;
time_t now = time(NULL);
/* 發現系統時間修改過,爲防止定時器永遠無法執行,將定時器設置爲立即執行 */
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;
/* 略去刪除失效定時器代碼 */
…… ……
/* 略去作者都註釋說沒什麼用的代碼囧 */
…… ……
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++;
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;
}
AE庫的定時器事件很“簡單粗暴”的使用了鏈表。新事件都往表頭添加(詳見“aeCreateFileEvent”函數實現),循環遍歷鏈表執行定時器事件。沒有使用複雜的時間堆和時間輪,簡單可用,只是查找和執行定時器事件的事件複雜度都是O(n)。作者在註釋中也解釋說,現在基於鏈表的定時器事件處理機制已經足夠Redis使用:
/* Search the first timer to fire.
* This operation is useful to know how many time the select can be
* put in sleep without to delay any event.
* If there are no timers NULL is returned.
*
* Note that's O(N) since time events are unsorted.
* Possible optimizations (not needed by Redis so far, but...):
* 1) Insert the event in order, so that the nearest is just the head.
* Much better but still insertion or deletion of timers is O(N).
* 2) Use a skiplist to have this operation as O(1) and insertion as O(log(N)).
*/
總結
從前面的分析可以看到,AE異步事件庫本身的實現很簡潔,卻支撐起了業界最流行的KV內存數據庫的核心功能。讓我想起了業界的一句雞湯,“要麼架構優雅到不怕Bug,要麼代碼簡潔到不會有Bug”。