服务器运行时主要关注两大类型事件:文件事件和时间事件。文件事件指的是socket文件描述符的读写就绪情况,时间事件分为一次性定时器和周期性定时器。
说明
网络事件的处理归档在event目录。
其中:
ae.c/ae.h:头文件里定义了描述文件事件和事件时间的结构体, 即aeFileEvent和aeTimeEvent;事件驱动状态结构体aeEventLoop, 这个结构体只有一个名为eventloop的全局变量在整个服务器进程中;事件就绪回调函数指针aeFileProc和aeTimeProc;以及操作事件驱动模型的各种API(aeCreateEventLoop以及之后全部的函数声明)。
ae_epoll.c/ae_select.c/ae_keque.c和ae_evport.c封装了select/epoll/kqueue等系统调用。
ae_epoll.c解析
我们以epoll为例,看看redis是怎么处理网络请求的。
封装的函数接口重点看这三个
创建: aeApiCreate
封装了创建epoll句柄的操作。给两个数据结构赋值,调用底层epoll_create
注册: aeApiAddEvent
注册需要监听的fd的epoll事件,如果fd已经有相关监听,则修改,否则是新增。调用底层epoll_ctl
收集就绪文件描述符: aeApiPoll
调用系统函数epoll_wait,获取已经就绪的时间数量。就绪的fd的数据都存储在事件表中。一个个的取出来,将其放在就绪事件表中。
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;
}
ae.c解析
event的主函数在ae.c中。
redis的封装event的数据结构在两个重要的结构体里面。各种函数的入参都是下面这个结构体的指针
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 */
// 已就绪的文件事件表
aeFiredEvent *fired; /* Fired events */
// 时间事件的头节点指针
aeTimeEvent *timeEventHead;
// 事件处理开关
int stop;
// 多路复用库的事件状态数据
void *apidata; /* This is used for polling API specific data */
// 执行处理事件之前的函数
aeBeforeSleepProc *beforesleep;
// 执行处理事件之后的函数
aeBeforeSleepProc *aftersleep;
} aeEventLoop;
而其中的apidata指针指向了下面这个ApiState结构体,一个epoll的fd和一个epoll_event的数组指针存储事件表。
typedef struct aeApiState {
// epoll事件的文件描述符
int epfd;
// 事件表
struct epoll_event *events;
} aeApiState;
Linux下当然不支持kqueue和evport。至于究竟选择哪一种I/O多路复用技术,在ae.c里有预处理控制,也就是说,这些源文件只有一个能最后被编译。优先选择epoll或者kqueue(FREEBSD和Mac OSX可用),其次是select。依次选择性能由高到低的多路复用处理处理方式。
#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
事件轮询主函数入口在aeMain(aeEventLoop *eventLoop)
中
主要逻辑方法aeProcessEvents(aeEventLoop *eventLoop, int flags)
里面
//#啥都没有就返回
if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
//#获取最早到时的时间事件,就是个单链表循环遍历,返回时间最小aeTimeEvent指针,计算距离下一个最近的时间事件还要等多久,这个时间就是后续调用epoll_wait的第四个参数
struct timeval tv, *tvp;
...
shortest = aeSearchNearestTimer(eventLoop);
...
//#获取就绪的文件事件,为什么这里的tvp是上面时间事件的最近的timer,下面解释!!
numevents = aeApiPoll(eventLoop, tvp);
//#依次处理文件事件
fe->rfileProc(eventLoop,fd,fe->clientData,mask);
fe->wfileProc(eventLoop,fd,fe->clientData,mask);
//#最后处理时间事件
if (flags & AE_TIME_EVENTS)
processed += processTimeEvents(eventLoop);
瞅瞅processTimeEvents
怎么处理时间事件,还是简单的单链表遍历。
// #如果节点被标记为删除,单链表删除节点操作,pre节点的next指向te的next。常规操作
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 (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;
上面的epoll_wait为啥等待tvp的时间,
Redis服务器在没有被事件触发时,就会阻塞等待,因为没有设置AE_DONT_WAIT标识。但是他不会一直的死等待,等待文件事件的到来,因为他还要处理时间时间,因此,在调用aeApiPoll进行监听之前,先从时间事件表中获取一个最近到达的时间时间,根据要等待的时间构建一个struct timeval tv, *tvp结构的变量,这个变量保存着服务器阻塞等待文件事件的最长时间,一旦时间到达而没有触发文件事件,aeApiPoll函数就会停止阻塞,如果时间事件列表中为空,则wait forever(基本不可能)。否则到期后调用processTimeEvents处理时间事件,因为Redis服务器设定一个对自身资源和状态进行检查的周期性检查的时间事件,而该函数就是timeProc所指向的回调函数。
时间事件与文件事件回调函数
被监听的文件描述符有变更或者时间事件被触发(上一节说的那样),会调用回调函数处理数据。
时间事件
通过调用aeCreateTimeEvent()
来创建时间事件,在单链表头部插入新的时间事件。在initServer
初始化的时候创建,传进来的回调就是serverCron
,时间事件都干点啥。看看这个serverCron的注释咋说的
// Active expired keys collection (it is also performed in a lazy way on lookup).
// Software watchdog.
// Update some statistic.
// Incremental rehashing of the DBs hash tables.
// Triggering BGSAVE / AOF rewrite, and handling of terminated children.
// Clients timeout of different kinds.
// Replication reconnection.
// Many more…文件事件
通过调用aeCreateFileEvent
来创建文件事件,很多地方调用,看几个比较重要的。等看到具体调用链的时候再回头补上这里的。- 文件事件之一
acceptTcpHandler - 文件事件之二
acceptUnixHandler - 文件事件之三
readQueryFromClient - 文件事件之四
sendReplyToClient
- 文件事件之一