redis源码--IO多路复用的封装
一,
Redis客户端对服务端的每次调用都经历了发送命令,执行命令,返回结果三个过程。其中执行命令阶段,由于Redis是单线程来处理命令的,所有每一条到达服务端的命令不会立刻执行,所有的命令都会进入一个队列中,然后逐个被执行。并且多个客户端发送的命令的执行顺序是不确定的。但是可以确定的是不会有两条命令被同时执行,不会产生并发问题,这就是Redis的单线程基本模型。
redis的多路复用选择器
Redis 基于 Reactor 模式开发了自己的网络事件处理器: 这个处理器被称为文件事件处理器(file event handler):
文件事件处理器使用 I/O 多路复用(multiplexing)程序来同时监听多个套接字, 并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
当被监听的套接字准备好执行连接应答(accept)、读取(read)、写入(write)、关闭(close)等操作时, 与操作相对应的文件事件就会产生, 这时文件事件处理器就会调用套接字之前关联好的事件处理器来处理这些事件。
文件事件处理器的构成
文件事件处理器的四个组成部分, 它们分别是套接字、 I/O 多路复用程序、 文件事件分派器(dispatcher)、 以及事件处理器。
文件事件是对套接字操作的抽象, 每当一个套接字准备好执行连接应答(accept)、写入、读取、关闭等操作时, 就会产生一个文件事件。 因为一个服务器通常会连接多个套接字, 所以多个文件事件有可能会并发地出现。
I/O 多路复用程序负责监听多个套接字, 并向文件事件分派器传送那些产生了事件的套接字。
尽管多个文件事件可能会并发地出现, 但 I/O 多路复用程序总是会将所有产生事件的套接字都入队到一个队列里面, 然后通过这个队列, 以有序(sequentially)、同步(synchronously)、每次一个套接字的方式向文件事件分派器传送套接字: 当上一个套接字产生的事件被处理完毕之后(该套接字为事件所关联的事件处理器执行完毕), I/O 多路复用程序才会继续向文件事件分派器传送下一个套接字。
二, Redis 的线程模型
- 文件事件处理器
- 如果是客户端要连接 redis,那么会为socket关联连接应答处理器
- 如果是客户端要写入数据到 redis,那么会为 scoket关联命令请求处理器
- 如果是客户端要从 redis读数据,那么会为socket关联命令回复处理器
三, IO复用的封装实现
Redis 服务采用 Reactor 的方式来实现文件事件处理器(每一个网络连接其实都对应一个文件描述符)
文件事件处理器使用 I/O 多路复用模块同时监听多个 FD,当 accept、read、write 和 close 文件事件产生时,文件事件处理器就会回调 FD 绑定的事件处理器。
可以参考 https://blog.csdn.net/asdfsadfasdfsa/article/details/87914459
虽然整个文件事件处理器是在单线程上运行的,但是通过 I/O 多路复用模块的引入,实现了同时对多个 FD 读写的监控,提高了网络通信模型的性能,同时也可以保证整个 Redis 服务实现的简单
为了将所有io复用统一,Redis为所有io复用统一了类型名aeApiState,对于epoll而言,类型成员就是调用epoll_wait所需要的参数
//ae_epoll.c
typedef struct aeApiState {
int epfd; //epollfd,文件描述符
struct epoll_event *events; //保存激活的事件(epoll_event)
} aeApiState;
/* State of an event based program */
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;
} aeEventLoop; //事件轮询的状态结构
为什么保存两个就够了呢,epoll_wait明明需要4个参数。原因是在Redis初始化时,已经将保存激活事件的数组(events)的容量调至最大,所以maxevents只需要设置成最大即可,无需保存。对于超时时间,Redis的策略是在时间事件中找到最早超时的那个,计算还有多久到达超时时间,将这个时间差(相对时间)作为io复用的超时时间
这么设计的原因是如果Redis中没有时间事件,那么io复用函数可以一直阻塞在那里直到有事件被激活,如果有时间事件,为了不影响超时事件的回调,需要在事件超时时从io复用中返回,那么设置成超时时间是最合适的(这一点和libevent的策略相同)
接下来就是一些对epoll接口的封装了,包括创建epoll(epoll_create),注册事件(epoll_ctl),删除事件(epoll_ctl),阻塞监听(epoll_wait)等
创建epoll就是简单的为aeApiState申请内存空间,然后将返回的指针保存在事件驱动循环中
//ae_epoll.c
/* 创建epollfd,即调用::epoll_create */
static int aeApiCreate(aeEventLoop *eventLoop) {
/* 申请内存 */
aeApiState *state = zmalloc(sizeof(aeApiState));
if (!state) return -1;
/* events用于保存激活的事件,需要足够大的空间(不小于epoll_create时传入的参数) */
/* eventLoop->setsize是初始化时设置的最大文件描述符个数 */
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;
}
/* 保存io复用数据成员到事件驱动中 */
eventLoop->apidata = state;
return 0;
}
注册事件和删除事件就是对epoll_ctl的封装,根据操作不同选择不同的参数,以注册事件为例
//ae_epoll.c
/*
* 将文件描述符和对应事件注册到io多路复用中
* 即调用::epoll_ctl(epollfd, EPOLL_CTL_ADD, fd, &event)
*/
static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {
/* 从事件驱动中获取io复用 */
aeApiState *state = eventLoop->apidata;
/* 用于传给epoll_ctl的参数 */
struct epoll_event ee = {0}; /* avoid valgrind warning */
/* If the fd was already monitored for some event, we need a MOD
* operation. Otherwise we need an ADD operation. */
/* 判断是否是第一次注册,如果是则添加否则是修改 */
int op = eventLoop->events[fd].mask == AE_NONE ?
EPOLL_CTL_ADD : EPOLL_CTL_MOD;
ee.events = 0;
/* 合并以前的监听事件,因为不一定是首次添加 */
mask |= eventLoop->events[fd].mask; /* Merge old events */
/* 根据监听事件的不同设置struct epoll_event中的events字段 */
if (mask & AE_READABLE) ee.events |= EPOLLIN;
if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;
/* 保存监听的文件描述符 */
ee.data.fd = fd;
/* 调用接口 */
if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;
return 0;
}
阻塞监听是对epoll_wait的封装,在返回后将激活的事件保存在事件驱动中
//ae_epoll.c
/* 阻塞监听,即调用::epoll_wait(epollfd, struct epoll_event*, int, struct timeval*); */
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;
numevents = retval;
/* 保存所有激活的事件,将其文件描述符和激活原因保存在fired数组中 */
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;
/* fired数组中只保存文件描述符和激活原因
* 当需要获取激活事件时,根据文件描述符从eventLoop->events数组中查找 */
eventLoop->fired[j].fd = e->data.fd;
eventLoop->fired[j].mask = mask;
}
}
/* 返回激活事件的个数 */
return numevents;
}
事件驱动循环流程
io复用的封装实现完成,那么Redis是何时调用io复用函数的呢,这就需要从server.c/main函数入手,可以猜测到当main函数初始化工作完成后,就需要进行事件驱动循环,而在循环中,会调用io复用函数进行监听
在初始化完成后,main函数调用了aeMain函数,传入的参数就是服务器的事件驱动
//server.c
int main(int argc, char **argv) {
/* 一系列的初始化工作 */
...
aeMain(server.el);
...
}
在ae_epoll.c中可以找到aeMain函数,这个函数便是一直在循环,每次循环会调用aeProcessEvents函数
//ae_epoll.c
void aeMain(aeEventLoop *eventLoop) {
eventLoop->stop = 0;
/* 一直循环监听 */
while (!eventLoop->stop) {
if (eventLoop->beforesleep != NULL)
eventLoop->beforesleep(eventLoop);
aeProcessEvents(eventLoop, AE_ALL_EVENTS);
}
}
可以猜测,aeProcessEvents函数中一定调用io复用函数进行监听,当io复用返回后,执行每个激活事件的回调函数,这个函数比较长,但是还是蛮好理解的
/* 每次事件循环都会调用一次该函数 */
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;
/* 为io复用函数寻找超时时间(通常是最先超时的时间事件的时间(相对时间)) */
/* redis中有时间事件 */
if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
shortest = aeSearchNearestTimer(eventLoop);
/* 根据最早超时的那个时间事件获取超时的相对时间 */
if (shortest) {
long now_sec, now_ms;
/* 获取当前时间 */
aeGetTime(&now_sec, &now_ms);
tvp = &tv;
/* 计算时间差(相对时间) */
long long ms =
(shortest->when_sec - now_sec)*1000 +
shortest->when_ms - now_ms;
if (ms > 0) {
tvp->tv_sec = ms/1000;
tvp->tv_usec = (ms % 1000)*1000;
} else {
tvp->tv_sec = 0;
tvp->tv_usec = 0;
}
} else {
/* 如果没有时间事件,那么io复用要么一直等,要么不等,取决于flags的设置 */
/* 传入的struct timeval*是NULL表示一直等直到有事件被激活
* 传入的timeval->tv_src = timeval->tv_usec = 0表示不等,直接返回 */
if (flags & AE_DONT_WAIT) {
tv.tv_sec = tv.tv_usec = 0;
tvp = &tv;
} else {
/* Otherwise we can block */
tvp = NULL; /* wait forever */
}
}
/* 调用io复用函数,返回被激活事件的个数,所有被激活的事件保存在epollLoop->fired数组中 */
numevents = aeApiPoll(eventLoop, tvp);
for (j = 0; j < numevents; j++) {
/* fired只保存的文件描述符和激活原因,实际的文件事件仍需要从events数组中取出 */
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 */
}
至此一次事件驱动循环就执行完毕,里面的细节比较多,比如如何为io复用函数寻找超时时间,如果从激活事件调用回调函数,如果处理已超时事件等