Reactor事件模型在Redis中的应用
1 模型简介
Redis
没有使用第三方的libevent等网络库,而是自己开发了一个单线程的Reactor模型的事件处理模型。而Memcached
内部使用的libevent库,多线程模型。
综合对比可见:nginx,memcached,redis网络模型总结
Redis在主循环中统一处理文件事件和时间事件,信号事件则由专门的handler来处理。
文件事件,我理解为IO事件,Redis将产生事件套接字放入一个就绪队列中,即redisServer.aeEventLoop.fired数组,然后在aeProcessEvents
会依次分派给文件事件处理器;
Redis编写了多个文件事件处理器。
Redis中文件事件包括:客户端的连接、命令请求、数据回复、连接断开,当上述事件发生时,会造成相应的描述符可读可写,再调用相应类型的文件事件处理器。
文件事件处理器有:
- 连接应答处理器
networking.c/acceptTcpHandler
; - 命令请求处理器
networking.c/readQueryFromClinet
; - 命令回复处理器
networking.c/sendReplyToClient
;
时间事件包含定时事件
和周期性事件
,Redis将其放入一个无序链表中,每当时间事件执行器运行时,就遍历链表,查找已经到达的时间事件,调用相应的处理器。
(1) 主循环
def ae_Main(): #一直循环处理事件 while(not_stop){ aeProcessEvents() }
(2)aeProcessEvents调度文件事件和时间事件的过程:
def aeProcessEvents(): time_event = aeSearchNearestTimer() #获取当前时间最近的时间事件 remaind_ms = time_event.when - unix_ts_now() #获取最近的时间事件达到的毫秒时间 if remaind_ms < 0 : #时间为负数,赋值0 remaind_ms = 0 timeval = create_timeval_with_ms(remainds_ms) #创建等待的时间结构 aeApiPoll(timeval) #等待文件事件产生,时间取决于remainds_ms processFileEvent() #处理文件事件 processTimeEvent() #处理时间事件
2 Reactor事件模型在Redis中的应用
下面主要结合文件事件的处理过程讲解Reactor事件模型在Redis中的应用。其中,Reactor事件模型框图如下所示:
2.1 Initiation Dispatcher在Redis中的实现
(1) handle_events()
在Redis中,对于文件事件,相应的处理函数为Ae.c/aeProcessEvents,其关键处理流程如下:
(1)底层调用接口返回,将就绪事件拷贝到eventLoop->fired数组;
(2)遍历就绪数组,获取相关fd,进而获取fd对应的aeFileEvent : eventLoop->events[fd],从而得到相关回调函数;
int aeProcessEvents(aeEventLoop *eventLoop, int flags){ ....省略 // 获取就绪文件事件,阻塞时间由最近的时间事件决定 numevents = aeApiPoll(eventLoop, tvp); for (j = 0; j < numevents; j++) { // 从已就绪数组中获取包装后的文件事件aeFileEvent aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd]; // 获取文件事件的详细参数:fd, mask 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); }
(2)register_handler/remove_handler 事件处理器的注册与删除等
在Redis中,相关的处理函数也在Ae.c文件中:
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask, eFileProc *proc, void *clientData); //创建文件事件(fd:mask),相关的回掉函数为eFileProc void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask); //将 fd 从 mask 指定的监听队列中删除 int aeGetFileEvents(aeEventLoop *eventLoop, int fd); //获取fd被监控的事件mask
long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds, aeTimeProc *proc, void *clientData, aeEventFinalizerProc *finalizerProc); int aeDeleteTimeEvent(aeEventLoop *eventLoop, long long id);
2.2 Synchronous Event Demultiplexer在Redis中的实现
针对IO复用方法,比如select
,poll
,epoll
,kqueue
等,每种方法的效率和使用方法都不相同,Redis通过统一包装各方法,来屏蔽它们的不同之处。
(1) IO复用跨平台
首先,Redis会根据平台,自动选择性能最好的IO复用函数库。该过程提现在Ae.c
头文件包含中,如下:
#ifdef HAVE_EVPORT #include "ae_evport.c" //evport优先级最高 #else #ifdef HAVE_EPOLL #include "ae_epoll.c" //epoll优先级较次 #else #ifdef HAVE_KQUEUE #include "ae_kqueue.c" //kqueue优先级还次 #else #include "ae_select.c" //select优先级最低 #endif #endif #endif
(2) 统一事件接口
ae_select.c
、ae_epoll.c
、ae_kqueue.c
、ae_evport.c
都提供一套统一的事件注册、删除接口,使得在ae.c
中可以直接使用以下接口,其中针对epoll的包装实现如下:
/* 事件状态*/ typedef struct aeApiState { int epfd; //epoll_event 实例描述符 struct epoll_event *events; // 事件槽,存储返回的就绪事件,大小为eventLoop->setsize } aeApiState; static int aeApiCreate(aeEventLoop *eventLoop) //创建aeApiState实例,并赋值于eventLoop->apidata
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) //增加关注的事件 static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask) //删除关注的事件 static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) //等待事件就绪返回,并存储于eventLoop->fired数组 static char *aeApiName(void) //获取底层调用的IO复用接口,如epoll
2.3 Concrete Event Handler
文件事件相关的一些具体的事件处理器如下:
连接请求acceptTcpHandler:在 redis.c/initServer中,程序会为redisServer.eventLoop关联一个客户连接的事件处理器。
命令请求readQueryFromClinet : 当新连接来的时候,需要调用networking.c/createClient创建客户端,在其中为客户端套接字注册读事件,关联处理器readQueryFromClinet。
命令回复sendReplyToClient : 当Redis调用networking.c/addReply时,会调用prepareClientToWrite来注册写事件,当套接字可写时,触发sendReplyToClient发送命令回复。
2.4 相关数据结构
从上面的相关接口可以发现,大多用到了结构体:aeEventLoop, aeFileEvent, aeFiredEvent。 它们之间的关系图如下:
(1) aeFileEvent
/* File event structure * * 文件事件结构 */ typedef struct aeFileEvent { // 监听事件类型掩码, // 值可以是 AE_READABLE 或 AE_WRITABLE , // 或者 AE_READABLE | AE_WRITABLE int mask; /* one of AE_(READABLE|WRITABLE) */ // 读事件处理器 aeFileProc *rfileProc; // 写事件处理器 aeFileProc *wfileProc; // 多路复用库的私有数据 void *clientData; } aeFileEvent;
可以发现aeFileEvent中没有fd信息,获取fd对应的aeFileEvent,需要到eventLoop->events[fd]处提取,因为在调用aeCreateFileEvent事件处理器注册函数时,将fd对应的aeFileEvent函数存储于eventLoop->events[fd]处。
(2)aeFiredEvent
/* A fired event * * 已就绪事件 */ typedef struct aeFiredEvent { // 已就绪文件描述符 int fd; // 事件类型掩码, // 值可以是 AE_READABLE 或 AE_WRITABLE // 或者是两者的或 int mask; } aeFiredEvent;
aeFiredEvent刚好包含一个就绪事件的所有有用信息,在aeApiPoll调用底层IO复用函数(如epoll)返回时,会将就绪事件从底层的就绪数组aeApiState.events拷贝到eventLoop->fired就绪数组中;通过aeFiredEvent中的fd可以找到对应的aeFileEvent,进而获取相关的回调函数。
(3) aeEventLoop
// 事件处理器的状态
typedef struct aeEventLoop { // 目前已注册的最大描述符 int maxfd; /* highest file descriptor currently registered */ // 目前已追踪的最大描述符 int setsize; /* max number of file descriptors tracked */ // 用于生成时间事件 id long long timeEventNextId; // 最后一次执行时间事件的时间 time_t lastTime; /* Used to detect system clock skew */ // 已注册的文件事件 aeFileEvent *events; /* Registered events,events数组下标与fd对应 */ // 已就绪的文件事件 aeFiredEvent *fired; /* Fired events */ // 时间事件 aeTimeEvent *timeEventHead; // 事件处理器的开关 int stop; // 多路复用库的私有数据 void *apidata; /* This is used for polling API specific data */ // 在处理事件前要执行的函数 aeBeforeSleepProc *beforesleep;
该结构的初始化创建过程如下:
/* * 初始化事件处理器状态 */ aeEventLoop *aeCreateEventLoop(int setsize) { aeEventLoop *eventLoop; int i; ... // 初始化文件事件结构和已就绪文件事件结构数组 eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize); //aeFileEvent中没有fd,如何获取fd信息,将fd对应的aeFileEvent存储于eventLoop->events[fd]处 eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize); ... // 设置数组大小 eventLoop->setsize = setsize; // 初始化执行最近一次执行时间 eventLoop->stop = 0; eventLoop->maxfd = -1; eventLoop->beforesleep = NULL; if (aeApiCreate(eventLoop) == -1) goto err; /* Events with mask == AE_NONE are not set. So let's initialize the * vector with it. */ // 初始化监听事件 for (i = 0; i < setsize; i++) eventLoop->events[i].mask = AE_NONE; // 返回事件循环 return eventLoop; }
2.5 register_handler/remove_handler 事件处理器注册与删除等的具体实现
(1)aeCreateFileEvent
该事件处理器注册函数主要涉及到变量eventLoop->events,eventLoop->apidata
其中,eventLoop->events数组主要用于存储aeFileEvent,包括回调函数,感兴趣的事件掩码mask,clientData等,fd对应的aeFileEvent存储于eventLoop->events[fd]处。(通过aeFileEvent和events数组,便将fd:mask和相关回调函数proc对应起来)
在调用aeApiAddEvent时,会将fd的指定事件加入底层的IO复用函数中;
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; }
(2)void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask); //删除文件事件
与aeCreateFileEvent相反,将在fd对应的aeFileEvent中,取消对事件mask的关注;并通过aeApiDelEvent在底层取消对fd相关事件mask的监听。具体代码如下:
/* * 将 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); }
3 Redis中事件监听和处理的流程图
Redis中事件监听和处理的流程如下:
(1) 通过aeApiPoll监听用户感兴趣的事件;
(2) 当有文件事件发生时返回(此处不考虑时间事件),就绪事件将存储于底层的就绪数组aeApiState.events;
(3) 将就绪数组拷贝到aeEventLoop的就绪数组aeEventLoop.fired中;
(4)通过fd,在aeEventLoop的注册文件事件数组中找到aeFileEvent -- eventLoop->events[fd],最后调用相关回调函数,完成事件处理。
参考: