整个分析的代码多数都在ae.c、ae.h、ae_epoll.c、ae_evport.c、ae_kqueue.c、ae_select.c,某些不在这些文件的函数会特别指出位置
一、Redis的事件模型库
Redis服务器是一个事件驱动程序。下面先来简单介绍什么是事件驱动。
所谓事件驱动,就是当你输入一条命令并且按下回车,然后消息被组装成Redis协议(RESP协议)的格式发送给Redis服务器,这就会产生一个事件,Redis服务器会接收该命令,处理该命令和发送回复,而当你没有与服务器进行交互时,那么服务器就会处于阻塞等待状态,会让出CPU从而进入睡眠状态,当事件触发时,就会被操作系统唤醒。事件驱动使CPU更高效的利用。
事件驱动是一种概括和抽象,也可以称为I/O多路复用(I/O multiplexing),它的实现方式各个系统都不同。
大家到网上Google“Redis libevent”就可以搜到Redis为什么没有选择libevent以及libev为其事件模型库,而是自己写了一个事件模型。 从代码中可以看到它主要支持了epoll、select、kqueue、以及基于Solaris的event ports。主要提供了对两种类型的事件驱动:
- 1.IO事件(文件事件),包括有IO的读事件和写事件。
- 2.定时器事件,包括有一次性定时器和循环定时器。
二、事件的抽象
2.1 文件事件结构(包括IO读事件和写事件)
1 /* File event structure 文件事件结构*/
2 typedef struct aeFileEvent {
3 // 文件时间类型:AE_NONE,AE_READABLE,AE_WRITABLE
4 int mask; /* one of AE_(READABLE|WRITABLE|BARRIER) */
5 // 可读处理函数
6 aeFileProc *rfileProc;
7 // 可写处理函数
8 aeFileProc *wfileProc;
9 // 客户端传入的数据
10 void *clientData;
11 } aeFileEvent;
其中rfileProc
和wfileProc
成员分别为两个函数指针,他们的原型为
1 //IO读写事件回调函数
2 typedef void aeFileProc(struct aeEventLoop *eventLoop, int fd, void *clientData, int mask);
这个函数是回调函数
,如果当前文件事件所指定的事件类型发生时,则会调用对应的回调函数
处理该事件。
当事件就绪时,我们需要知道文件事件的文件描述符还有事件类型才能对于锁定该事件,因此定义了aeFiredEvent
结构统一管理:
1 /* A fired event 就绪事件*/
2 typedef struct aeFiredEvent {
3 // 就绪事件的文件描述符
4 int fd;
5 // 就绪事件类型:AE_NONE,AE_READABLE,AE_WRITABLE
6 int mask;
7 } aeFiredEvent;
2.2 时间事件结构
1 /* Time event structure 定时事件结构体,是一个双向链表*/
2 typedef struct aeTimeEvent {
3 // 时间事件的id
4 long long id; /* time event identifier. */
5 // 时间事件到达的时间的秒数
6 long when_sec; /* seconds */
7 // 时间事件到达的时间的毫秒数
8 long when_ms; /* milliseconds */
9 // 时间事件处理的回调函数
10 aeTimeProc *timeProc;
11 // 时间事件删除时的回调函数
12 aeEventFinalizerProc *finalizerProc;
13 // 客户端传入的数据
14 void *clientData;
15 // 指向前一个时间事件
16 struct aeTimeEvent *prev;
17 // 指向下一个时间事件
18 struct aeTimeEvent *next;
19 } aeTimeEvent;
从这个结构中可以看出,时间事件表是一个链表,因为它有一个next
指针域,指向下一个时间事件。
和文件事件一样,当时间事件所指定的事件发生时,也会调用对应的回调函数,结构成员timeProc和finalizerProc都是回调函数,函数原型如下:
1 //定时器事件回调函数
2 typedef int aeTimeProc(struct aeEventLoop *eventLoop, long long id, void *clientData);
3 //删除定时事件的回调函数
4 typedef void aeEventFinalizerProc(struct aeEventLoop *eventLoop, void *clientData);
虽然对文件事件和时间事件都做了抽象,Redis
仍然需要对事件做整体抽象,于是定义了aeEventLoop
结构。
2.3 事件状态结构
2.3.1 aeEventLoop结构
aeEventLoop保存了文件事件、定时事件、底层IO多复用模型等诸多信息
1 /* State of an event based program 事件状态结构*/
2 typedef struct aeEventLoop {
3 //当前注册的最大文件描述符
4 int maxfd; /* highest file descriptor currently registered */
5 /**
6 * 指定事件循环要监听的文件描述符集合的大小。这个值与配置文件中得maxclients有关。
7 *
8 * setsize参数表示了eventloop可以监听的网络事件fd的个数(不包含超时事件),
9 * 如果当前监听的fd个数超过了setsize,eventloop将不能继续注册。
10 */
11 int setsize; /* max number of file descriptors tracked */
12 // 下一个时间事件的ID
13 long long timeEventNextId;
14 // 最后一次执行事件的时间
15 time_t lastTime; /* Used to detect system clock skew */
16 /**
17 * 存放所有注册的读写事件,是大小为setsize的数组。内核会保证新建连接的fd是当前可用描述符的最小值,
18 * 所以最多监听setsize个描述符,那么最大的fd就是setsize - 1。这种组织方式的好处是,可以以fd为下标,
19 * 索引到对应的事件,在事件触发后根据fd快速查找到对应的事件。
20 */
21 aeFileEvent *events; /* Registered events */
22 //存放触发的读写事件。同样是setsize大小的数组。
23 aeFiredEvent *fired; /* Fired events */
24 //redis将定时器事件组织成链表,这个属性指向表头。
25 aeTimeEvent *timeEventHead;
26 // 事件处理开关
27 int stop;
28 //存放epoll、select等实现相关的数据。
29 void *apidata; /* This is used for polling API specific data */
30 //事件循环在每次迭代前会调用beforesleep执行一些异步处理。
31 aeBeforeSleepProc *beforesleep;
32 // 执行处理事件之后的回调函数
33 aeBeforeSleepProc *aftersleep;
34 } aeEventLoop;
2.3.2 底层IO多路复用模式的选择
aeEventLoop结构保存了一个void *类型的万能指针apidata,是用来保存轮询事件的状态的,也就是保存底层调用的多路复用库的事件状态,关于Redis的多路复用库的选择,Redis包装了常见的select epoll evport kqueue,他们在编译阶段,根据不同的系统选择性能最高的一个多路复用库作为Redis的多路复用程序的实现,而且所有库实现的接口名称都是相同的,因此Redis多路复用程序底层实现是可以互换的。具体选择库的源码为
1 /**
2 * Include the best multiplexing layer supported by this system.
3 * The following should be ordered by performances, descending.
4 * 包括该系统支持的最佳复用层。性能依次下降,IO复用的选择,性能依次下降,Linux支持 "ae_epoll.c" 和 "ae_select.c"
5 */
6 #ifdef HAVE_EVPORT
7 #include "ae_evport.c"
8 #else
9 #ifdef HAVE_EPOLL
10 #include "ae_epoll.c"
11 #else
12 #ifdef HAVE_KQUEUE
13 #include "ae_kqueue.c"
14 #else
15 #include "ae_select.c"
16 #endif
17 #endif
18 #endif
2.3.3 查看当前系统所使用的多路复用库
也可以通过Redis客户端的命令来查看当前选择的多路复用库,info server。
1 127.0.0.1:6379> INFO server
2 # Server
3 ……
4 multiplexing_api:epoll
5 ……
2.3.4 epoll多路复用库
2.3.4.1 aeApiState结构
既然知道了多路复用库的选择,那么我们来查看一下apidata
保存的epoll
模型的事件状态结构,实际上就是将需要监听的事件和epoll_create创建的文件描述符epfd封装到了一起,位于ae_epoll.c文件中
1 typedef struct aeApiState {
2 // epoll事件的文件描述符
3 int epfd;
4 // 事件表
5 struct epoll_event *events;
6 } aeApiState;
2.3.4.2 用户层到底层事件类型的转换
epoll模型的struct epoll_event的结构中定义这自己的事件类型,例如EPOLLIN POLLOUT等等,但是Redis的文件事件结构aeFileEvent中也在mask中定义了自己的事件类型,例如:AE_READABLE AE_WRITABLE等,于是,就需要实现一个中间层将两者的事件类型相联系起来,这也就是之前提到的ae_epoll.c文件中实现的相同的API,我们列出来:
1 // 创建一个epoll实例,保存到eventLoop中
2 static int aeApiCreate(aeEventLoop *eventLoop)
3 // 调整事件表的大小
4 static int aeApiResize(aeEventLoop *eventLoop, int setsize)
5 // 释放epoll实例和事件表空间
6 static void aeApiFree(aeEventLoop *eventLoop)
7 // 在epfd标识的事件表上注册fd的事件
8 static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask)
9 // 在epfd标识的事件表上注删除fd的事件
10 static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask)
11 // 等待所监听文件描述符上有事件发生
12 static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)
13 // 返回正在使用的IO多路复用库的名字
14 static char *aeApiName(void)
这些API都是调用相应的底层多路复用库来将Redis事件状态结构aeEventLoop所关联,就是将epoll
的底层函数封装起来,Redis实现事件时,只需调用这些接口即可。我们查看两个重要的函数的源码,看看是如何实现的
2.3.4.2.1 aeApiAddEvent函数
-
向Redis事件状态结构
aeEventLoop
的事件表event
注册一个事件,对应的是epoll_ctl
函数1 // 在epfd标识的事件表上注册fd的事件 2 static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) { 3 aeApiState *state = eventLoop->apidata; 4 struct epoll_event ee = {0}; /* avoid valgrind warning */ 5 /** 6 * If the fd was already monitored for some event, we need a MOD 7 * operation. Otherwise we need an ADD operation. 8 * EPOLL_CTL_ADD,向epfd注册fd的上的event 9 * EPOLL_CTL_MOD,修改fd已注册的event 10 * #define AE_NONE 0 //未设置 11 * #define AE_READABLE 1 //事件可读 12 * #define AE_WRITABLE 2 //事件可写 13 * 判断fd事件的操作,如果没有设置事件,则进行关联mask类型事件,否则进行修改 14 */ 15 int op = eventLoop->events[fd].mask == AE_NONE ? 16 EPOLL_CTL_ADD : EPOLL_CTL_MOD; 17 18 19 // struct epoll_event { 20 // uint32_t events; /* Epoll events */ 21 // epoll_data_t data; /* User data variable */ 22 // }; 23 ee.events = 0; 24 // 如果是修改事件,合并之前的事件类型 25 mask |= eventLoop->events[fd].mask; /* Merge old events */ 26 // 根据mask映射epoll的事件类型 27 if (mask & AE_READABLE) ee.events |= EPOLLIN;//读事件 28 if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;//写事件 29 //设置事件所从属的目标文件描述符 30 ee.data.fd = fd; 31 // 将ee事件注册到epoll中 32 if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1; 33 return 0; 34 }
2.3.4.2.1 aeApiPoll函数
- 等待所监听文件描述符上有事件发生,对应着底层
epoll_wait
函数1 // 等待所监听文件描述符上有事件发生 2 static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) { 3 aeApiState *state = eventLoop->apidata; 4 int retval, numevents = 0; 5 6 // 监听事件表上是否有事件发生 7 retval = epoll_wait(state->epfd,state->events,eventLoop->setsize, 8 tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1); 9 // 至少有一个就绪的事件 10 if (retval > 0) { 11 int j; 12 13 numevents = retval; 14 // 遍历就绪的事件表,将其加入到eventLoop的就绪事件表中 15 for (j = 0; j < numevents; j++) { 16 int mask = 0; 17 struct epoll_event *e = state->events+j; 18 19 // 根据就绪的事件类型,设置mask 20 if (e->events & EPOLLIN) mask |= AE_READABLE; 21 if (e->events & EPOLLOUT) mask |= AE_WRITABLE; 22 if (e->events & EPOLLERR) mask |= AE_WRITABLE; 23 if (e->events & EPOLLHUP) mask |= AE_WRITABLE; 24 // 添加到就绪事件表中 25 eventLoop->fired[j].fd = e->data.fd; 26 eventLoop->fired[j].mask = mask; 27 } 28 } 29 // 返回就绪的事件个数 30 return numevents; 31 }
三、使用实例
3.1 底层多路IO模型实现
这里写了一个由标准输入的读事件驱动的echo服务例子,同时用一个5秒的循环定时器每个5秒打印一次服务器状态。这里用了epoll为底层事件接口。具体的代码抽取可以
1 /**
2 * Include the best multiplexing layer supported by this system.
3 * The following should be ordered by performances, descending.
4 * 包括该系统支持的最佳复用层。性能依次下降
5 */
6 #ifdef HAVE_EVPORT
7 #include "ae_evport.c"
8 #else
9 #ifdef HAVE_EPOLL
10 #include "ae_epoll.c"
11 #else
12 #ifdef HAVE_KQUEUE
13 #include "ae_kqueue.c"
14 #else
15 #include "ae_select.c"
16 #endif
17 #endif
18 #endif
3.2 使用实例
这里主要是分析Redis的事件模型的封装,因此对于其对socket的包装以及内存管理都不做分析。故采用标准输入,同时需要将这些文件中 的内存管理接口"zmalloc()"以及"zfree()"替换成C库中的“malloc()”还有"free()"。可以使用sed或者vim的%s做替换操作。代码如下:
1 #include "ae.h" 2 3 #include <stdio.h> 4 #include <assert.h> 5 #include <unistd.h> 6 #include <sys/time.h> 7 8 9 #define MAXFD 5 10 11 12 void loop_init(struct aeEventLoop *l) 13 { 14 puts("I'm loop_init!!! "); 15 } 16 17 void file_cb(struct aeEventLoop *l,int fd,void *data,int mask) 18 { 19 char buf[51] ={0}; 20 read(fd,buf,51); 21 printf("I'm file_cb ,here [EventLoop: %p],[fd : %d],[data: %p],[mask: %d] ",l,fd,data,mask); 22 printf("get %s",buf); 23 } 24 25 int time_cb(struct aeEventLoop *l,long long id,void *data) 26 { 27 printf("now is %ld ",time(NULL)); 28 printf("I'm time_cb,here [EventLoop: %p],[id : %lld],[data: %p] ",l,id,data); 29 return 5*1000; 30 31 } 32 33 void fin_cb(struct aeEventLoop *l,void *data) 34 { 35 puts("call the unknow final function "); 36 } 37 38 int main(int argc,char *argv[]) 39 { 40 aeEventLoop *l; 41 char *msg = "Here std say:"; 42 char *user_data = malloc(50*sizeof(char)); 43 if(! user_data) 44 assert( ("user_data malloc error",user_data) ); 45 memset(user_data,'