redis并没有采用libevent库作为事件机制的底层实现,而是自己对io多路复用进行了封装,即可以采用select、epoll、evport、kqueue作为底层的实现。redis客户端与服务端进行通信时,redis提供了命令事件(也就是文件事件)。另外redis还提供了定时事件,用于对系统实时性要求进行处理,以及处理用户的业务需求。
先来看下redis事件机制的整体结构, 接下来的每个小结都围绕这个结构进行分析。
一、创建/监听socket
redis服务端在初始化函数initServer中会进行创建socket、绑定socket的操作。将服务端的指定ip或者所有ip都设置为可以监听来自客户端的连接请求。
//创建套接字、绑定套接字
int listenToPort(int port, int *fds, int *count);
fds用于存储所有绑定的socket, 例如:"假设服务端有两个ip:192.168.100.10 , 192.168.100.11,如果对这两个ip都进行绑定,则fds中将存储绑定后的两个套接字fd"
count 用于存储具体有多少个套接字进行了绑定
创建并绑定socket后、服务端可以在这些socket上监听来自客户端的连接请求。
二、创建事件管理器
redis服务端在初始化函数initServer中,会创建一个事件管理器对象,用于管理命令事件、时间事件。函数aeCreateEventLoop将创建一个事件管理器,setsize参数指的是socket描述符的个数,服务端初始化时将该参数设为最大socket个数。函数将返回一个事件管理器对象。
aeEventLoop *aeCreateEventLoop(int setsize)
{
aeEventLoop *eventLoop;
int i;
// 创建事件状态结构
if ((eventLoop = zmalloc(sizeof(*eventLoop))) == NULL)
goto err;
// 创建未就绪事件表、就绪事件表
eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);
eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);
if (eventLoop->events == NULL || eventLoop->fired == NULL)
goto err;
// 设置数组大小
eventLoop->setsize = setsize;
// 初始化执行最近一次执行时间
eventLoop->lastTime = time(NULL);
// 初始化时间事件结构
eventLoop->timeEventHead = NULL;
eventLoop->timeEventNextId = 0;
//将多路复用io与事件管理器关联起来
if (aeApiCreate(eventLoop) == -1)
goto err;
// 初始化监听事件
for (i = 0; i < setsize; i++)
eventLoop->events[i].mask = AE_NONE;
// 返回事件循环
return eventLoop;
err:
...
}
1、首先创建一个aeEventLoop对象
2、创建一个未就绪事件表、就绪事件表。events指针指向未就绪事件表、fired指针指向就绪事件表。表的内容在后面添加具体事件时进行初始化。
3、调用aeApiCreate创建一个epoll实例
static int aeApiCreate(aeEventLoop *eventLoop)
{
aeApiState *state = zmalloc(sizeof(aeApiState));
// 初始化epoll就绪事件表
state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);
// 创建 epoll 实例
state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */
// 事件管理器与epoll关联
eventLoop->apidata = state;
return 0;
}
typedef struct aeApiState
{
// epoll_event 实例描述符
int epfd;
// 存储epoll就绪事件表
struct epoll_event *events;
} aeApiState;
aeApiCreate内部将创建一个aeApiState对象,并将对象存储在apidata中,这样aeApiCreate与aeApiState就关联起来了。
对象中epfd存储epoll的标识,events是一个就绪事件数组,当有事件发生时,所有发生的事件都将存储在这个数组中。这个就绪事件数组由应用层开辟空间、内核负责把所有发生的事件填充到该数组。
三、创建命令事件(也就是文件事件)
创建来事件管理器后、接下来就可以把具体的事件插入到事件管理器中。
typedef struct aeFileEvent
{
// 监听事件类型掩码,// 值可以是 AE_READABLE 或 AE_WRITABLE ,
// 或者 AE_READABLE | AE_WRITABLE
int mask;
// 读事件处理器
aeFileProc *rfileProc;
// 写事件处理器
aeFileProc *wfileProc;
// 多路复用库的私有数据
void *clientData;
} aeFileEvent;
aeFileEvent是命令事件结构,对于每一个具体的事件,都有读处理函数、写处理函数等。
int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,aeFileProc *proc, void *clientData)
{
// 取出文件事件结构
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;
return AE_OK;
}
创建一个具体命令事件时,参数fd指的是具体的soket套接字,proc指fd产生事件时,具体的处理过程。
除了将事件插入到未就绪表中外,还需要把fd对应的事件插入到具体的io复用中,本例为epoll。
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;
//将事件加入epoll中
if (epoll_ctl(state->epfd,op,fd,&ee) == -1)
return -1;
return 0;
}
epoll内部使用红黑树维护每一个事件。红黑树中的每一个节点都代表一个fd。当应用层注册一个命令事件时,会将事件fd插入到红黑树中。应用层删除一个事件时,也相应会从红黑树中删除一个事件节点。
因此创建一个事件时、将会发生下面3个操作
1、创建事件并给事件赋值
2、将事件插入到未就绪事件表
3、将事件插入到epoll维护的红黑树中。
四、事件循环
程序将使用一个while死循环,一直维持着服务端的运转。
//事件处理器的主循环
void aeMain(aeEventLoop *eventLoop)
{
eventLoop->stop = 0;
while (!eventLoop->stop)
{
// 如果有需要在事件处理前执行的函数,那么运行它
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
// 开始处理事件
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
aeProcessEvents将开始进行事件处理。即处理定时事件也处理命令事件。定时事件与命令事件可以同时发生。定时事件将在下一节讲述,本节只讲述命令事件。
// 处理文件事件,阻塞时间由 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);
}
}
aeApiPoll调用时将阻塞,直到有事件发生,或者超时,该函数才返回。函数返回时,已就绪表中存储了所有就绪事件。
遍历所有已经发生的事件,根据fd在未就绪表中找到相应的事件,然后调用事件处理函数。
当一轮事件执行完后,程序又进入最外层的事件循环中,接着处理剩于的事件。
aeApiPoll内部调用epoll系统调用,等待指定事件发生或者超时。如果有事件发生则eploo_wait返回非0,如果为超时,则返回0。
当有事件发生时,内核会把所有发生的事件由内核层拷贝到应用层。eploo_wait返回时state->events就是保存是由内核返回的就绪表。
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就绪表
state->events中。之后aeApiPoll会把epoll就绪表
state->events中的就绪事件拷贝到fired就绪表中
五、删除事件
当不在需要某个事件时,需要把事件删除掉。例如: 如果fd同时监听读事件、写事件。当不在需要监听写事件时,可以把该fd的写事件删除。
void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask)
{
// 取出文件事件结构
aeFileEvent *fe = &eventLoop->events[fd];
// 未设置监听的事件类型,直接返回
if (fe->mask == AE_NONE)
return;
// 计算新掩码
fe->mask = fe->mask & (~mask);
// 取消对给定 fd 的给定事件的监视
aeApiDelEvent(eventLoop, fd, mask);
}
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);
}
}
删除的过程可以总结为以下几个步骤
1、根据fd在未就绪表中查找到事件
2、取消该fd对应的相应事件标识符
3、通知内核,内核会将红黑树上的相应事件也给取消。