redis事件机制
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;
- <span style="color:#ff0000;">// 创建未就绪事件表、就绪事件表</span>
- 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;
- <span style="color:#ff0000;"> //将多路复用io与事件管理器关联起来</span>
- 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;
- <span style="color:#ff0000;">// 存储epoll就绪事件表</span>
- struct epoll_event *events;
- } aeApiState;
对象中epfd存储epoll的标识,events是一个就绪事件数组,当有事件发生时,所有发生的事件都将存储在这个数组中。这个就绪事件数组由应用层开辟空间、内核负责把所有发生的事件填充到该数组。
三、创建命令事件(也就是文件事件)
创建来事件管理器后、接下来就可以把具体的事件插入到事件管理器中。
- typedef struct aeFileEvent
- {
- // 监听事件类型掩码,// 值可以是 AE_READABLE 或 AE_WRITABLE ,
- // 或者 AE_READABLE | AE_WRITABLE
- int mask;
- // 读事件处理器
- aeFileProc *rfileProc;
- // 写事件处理器
- aeFileProc *wfileProc;
- // 多路复用库的私有数据
- void *clientData;
- } 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为索引在未就绪表中找到相应的元素,该数组元素就被fd占用。这个过程就相当于把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;
- }
因此创建一个事件时、将会发生下面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);
- }
- }
- // 处理文件事件,阻塞时间由 tvp 决定
- numevents = aeApiPoll(eventLoop, tvp);
- for (j = 0; j < numevents; j++)
- {
- // 从已就绪数组中获取事件
- <span style="white-space:pre"> </span>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);
- }
- }
遍历所有已经发生的事件,根据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 = <span style="color:#ff0000;">state->events</span>+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;
- }
五、删除事件
当不在需要某个事件时,需要把事件删除掉。例如: 如果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、通知内核,内核会将红黑树上的相应事件也给取消。
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;
- <span style="color:#ff0000;">// 创建未就绪事件表、就绪事件表</span>
- 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;
- <span style="color:#ff0000;"> //将多路复用io与事件管理器关联起来</span>
- 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;
- <span style="color:#ff0000;">// 存储epoll就绪事件表</span>
- struct epoll_event *events;
- } aeApiState;
对象中epfd存储epoll的标识,events是一个就绪事件数组,当有事件发生时,所有发生的事件都将存储在这个数组中。这个就绪事件数组由应用层开辟空间、内核负责把所有发生的事件填充到该数组。
三、创建命令事件(也就是文件事件)
创建来事件管理器后、接下来就可以把具体的事件插入到事件管理器中。
- typedef struct aeFileEvent
- {
- // 监听事件类型掩码,// 值可以是 AE_READABLE 或 AE_WRITABLE ,
- // 或者 AE_READABLE | AE_WRITABLE
- int mask;
- // 读事件处理器
- aeFileProc *rfileProc;
- // 写事件处理器
- aeFileProc *wfileProc;
- // 多路复用库的私有数据
- void *clientData;
- } 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为索引在未就绪表中找到相应的元素,该数组元素就被fd占用。这个过程就相当于把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;
- }
因此创建一个事件时、将会发生下面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);
- }
- }
- // 处理文件事件,阻塞时间由 tvp 决定
- numevents = aeApiPoll(eventLoop, tvp);
- for (j = 0; j < numevents; j++)
- {
- // 从已就绪数组中获取事件
- <span style="white-space:pre"> </span>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);
- }
- }
遍历所有已经发生的事件,根据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 = <span style="color:#ff0000;">state->events</span>+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;
- }
五、删除事件
当不在需要某个事件时,需要把事件删除掉。例如: 如果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、通知内核,内核会将红黑树上的相应事件也给取消。