redis事件机制 redis事件机制

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);

参数port指绑定socket的具体端口

fds用于存储所有绑定的socket, 例如:"假设服务端有两个ip:192.168.100.10 , 192.168.100.11,如果对这两个ip都进行绑定,则fds中将存储绑定后的两个套接字fd"

count 用于存储具体有多少个套接字进行了绑定

          创建并绑定socket后、服务端可以在这些socket上监听来自客户端的连接请求。

二、创建事件管理器

redis服务端在初始化函数initServer中,会创建一个事件管理器对象,用于管理命令事件、时间事件。函数aeCreateEventLoop将创建一个事件管理器,setsize参数指的是socket描述符的个数,服务端初始化时将该参数设为最大socket个数。函数将返回一个事件管理器对象。

[cpp]   view plain  copy
  1. aeEventLoop *aeCreateEventLoop(int setsize)   
  2. {  
  3.     aeEventLoop *eventLoop;  
  4.     int i;  
  5.     // 创建事件状态结构  
  6.     if ((eventLoop = zmalloc(sizeof(*eventLoop))) == NULL)   
  7.     goto err;  
  8.   
  9.     <span style="color:#ff0000;">// 创建未就绪事件表、就绪事件表</span>  
  10.     eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);  
  11.     eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);  
  12.     if (eventLoop->events == NULL || eventLoop->fired == NULL)   
  13.     goto err;  
  14.       
  15.     // 设置数组大小  
  16.     eventLoop->setsize = setsize;  
  17.     // 初始化执行最近一次执行时间  
  18.     eventLoop->lastTime = time(NULL);  
  19.   
  20.     // 初始化时间事件结构  
  21.     eventLoop->timeEventHead = NULL;  
  22.     eventLoop->timeEventNextId = 0;  
  23. <span style="color:#ff0000;">    //将多路复用io与事件管理器关联起来</span>  
  24.     if (aeApiCreate(eventLoop) == -1)  
  25.         goto err;  
  26.   
  27.     // 初始化监听事件  
  28.     for (i = 0; i < setsize; i++)  
  29.         eventLoop->events[i].mask = AE_NONE;  
  30.   
  31.     // 返回事件循环  
  32.     return eventLoop;  
  33. err:  
  34.    ...  
  35. }  

1、首先创建一个aeEventLoop对象

2、创建一个未就绪事件表、就绪事件表。events指针指向未就绪事件表、fired指针指向就绪事件表。表的内容在后面添加具体事件时进行初始化。

3、调用aeApiCreate创建一个epoll实例

[cpp]   view plain  copy
  1. static int aeApiCreate(aeEventLoop *eventLoop)   
  2. {  
  3.     aeApiState *state = zmalloc(sizeof(aeApiState));  
  4.   
  5.     // 初始化epoll就绪事件表  
  6.     state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);  
  7.   
  8.     // 创建 epoll 实例  
  9.     state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */  
  10.   
  11.     // 事件管理器与epoll关联  
  12.     eventLoop->apidata = state;  
  13.     return 0;  
  14. }  
[cpp]   view plain  copy
  1. typedef struct aeApiState   
  2. {  
  3.     // epoll_event 实例描述符  
  4.     int epfd;  
  5.     <span style="color:#ff0000;">// 存储epoll就绪事件表</span>  
  6.     struct epoll_event *events;  
  7. } aeApiState;  
aeApiCreate内部将创建一个aeApiState对象,并将对象存储在apidata中,这样aeApiCreate与aeApiState就关联起来了。

对象中epfd存储epoll的标识,events是一个就绪事件数组,当有事件发生时,所有发生的事件都将存储在这个数组中。这个就绪事件数组由应用层开辟空间、内核负责把所有发生的事件填充到该数组。

三、创建命令事件(也就是文件事件)

创建来事件管理器后、接下来就可以把具体的事件插入到事件管理器中。

[cpp]   view plain  copy
  1. typedef struct aeFileEvent   
  2. {  
  3.     // 监听事件类型掩码,// 值可以是 AE_READABLE 或 AE_WRITABLE ,  
  4.     // 或者 AE_READABLE | AE_WRITABLE  
  5.     int mask;   
  6.     // 读事件处理器  
  7.     aeFileProc *rfileProc;  
  8.     // 写事件处理器  
  9.     aeFileProc *wfileProc;  
  10.     // 多路复用库的私有数据  
  11.     void *clientData;  
  12. } aeFileEvent;  
aeFileEvent是命令事件结构,对于每一个具体的事件,都有读处理函数、写处理函数等。
[cpp]   view plain  copy
  1. int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,aeFileProc *proc, void *clientData)  
  2. {  
  3.     // 取出文件事件结构  
  4.     aeFileEvent *fe = &eventLoop->events[fd];  
  5.   
  6.     // 监听指定 fd 的指定事件  
  7.     if (aeApiAddEvent(eventLoop, fd, mask) == -1)  
  8.         return AE_ERR;  
  9.   
  10.     // 设置文件事件类型,以及事件的处理器  
  11.     fe->mask |= mask;  
  12.     if (mask & AE_READABLE)   
  13.     fe->rfileProc = proc;  
  14.     if (mask & AE_WRITABLE)   
  15.     fe->wfileProc = proc;  
  16.   
  17.     // 私有数据  
  18.     fe->clientData = clientData;  
  19.     return AE_OK;  
  20. }  
创建一个具体命令事件时,参数fd指的是具体的soket套接字,proc指fd产生事件时,具体的处理过程。
根据fd为索引在未就绪表中找到相应的元素,该数组元素就被fd占用。这个过程就相当于把fd插入到未就绪事件表中。接下来填充事件的回调、参数、事件类型等参数。

除了将事件插入到未就绪表中外,还需要把fd对应的事件插入到具体的io复用中,本例为epoll。

[cpp]   view plain  copy
  1. static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask)   
  2. {  
  3.     aeApiState *state = eventLoop->apidata;  
  4.     struct epoll_event ee;  
  5.   
  6.     //如果 fd 没有关联任何事件,那么这是一个 ADD 操作。如果已经关联了某个/某些事件,那么这是一个 MOD 操作。  
  7.     int op = eventLoop->events[fd].mask == AE_NONE ?  
  8.            EPOLL_CTL_ADD : EPOLL_CTL_MOD;  
  9.   
  10.     // 注册事件到 epoll  
  11.     ee.events = 0;  
  12.     mask |= eventLoop->events[fd].mask; /* Merge old events */  
  13.     if (mask & AE_READABLE)   
  14.     ee.events |= EPOLLIN;  
  15.     if (mask & AE_WRITABLE)   
  16.     ee.events |= EPOLLOUT;  
  17.   
  18.     ee.data.u64 = 0; /* avoid valgrind warning */  
  19.     ee.data.fd = fd;  
  20.     //将事件加入epoll中  
  21.     if (epoll_ctl(state->epfd,op,fd,&ee) == -1)  
  22.         return -1;  
  23.   
  24.     return 0;  
  25. }  
epoll内部使用红黑树维护每一个事件。红黑树中的每一个节点都代表一个fd。当应用层注册一个命令事件时,会将事件fd插入到红黑树中。应用层删除一个事件时,也相应会从红黑树中删除一个事件节点。

因此创建一个事件时、将会发生下面3个操作

1、创建事件并给事件赋值

2、将事件插入到未就绪事件表

3、将事件插入到epoll维护的红黑树中。

四、事件循环

程序将使用一个while死循环,一直维持着服务端的运转。

[cpp]   view plain  copy
  1. //事件处理器的主循环  
  2. void aeMain(aeEventLoop *eventLoop)   
  3. {  
  4.     eventLoop->stop = 0;  
  5.   
  6.     while (!eventLoop->stop)   
  7.     {  
  8.   
  9.         // 如果有需要在事件处理前执行的函数,那么运行它  
  10.         if (eventLoop->beforesleep != NULL)  
  11.             eventLoop->beforesleep(eventLoop);  
  12.   
  13.         // 开始处理事件  
  14.         aeProcessEvents(eventLoop, AE_ALL_EVENTS);  
  15.     }  
  16. }  
aeProcessEvents将开始进行事件处理。即处理定时事件也处理命令事件。定时事件与命令事件可以同时发生。定时事件将在下一节讲述,本节只讲述命令事件。
[cpp]   view plain  copy
  1. // 处理文件事件,阻塞时间由 tvp 决定  
  2. numevents = aeApiPoll(eventLoop, tvp);  
  3. for (j = 0; j < numevents; j++)   
  4. {  
  5.     // 从已就绪数组中获取事件  
  6. <span style="white-space:pre">    </span>aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];  
  7.     int mask = eventLoop->fired[j].mask;  
  8.     int fd = eventLoop->fired[j].fd;  
  9.     int rfired = 0;  
  10.   
  11.     // 读事件  
  12.     if (fe->mask & mask & AE_READABLE)   
  13.     {  
  14.           // rfired 确保读/写事件只能执行其中一个  
  15.           rfired = 1;  
  16.           //调用读处理函数  
  17.           fe->rfileProc(eventLoop,fd,fe->clientData,mask);  
  18.     }  
  19.   
  20.     // 写事件  
  21.     if (fe->mask & mask & AE_WRITABLE)   
  22.     {  
  23.         //调用写处理函数  
  24.          if (!rfired || fe->wfileProc != fe->rfileProc)  
  25.           fe->wfileProc(eventLoop,fd,fe->clientData,mask);  
  26.     }  
  27. }  
aeApiPoll调用时将阻塞,直到有事件发生,或者超时,该函数才返回。函数返回时,已就绪表中存储了所有就绪事件。

遍历所有已经发生的事件,根据fd在未就绪表中找到相应的事件,然后调用事件处理函数。

当一轮事件执行完后,程序又进入最外层的事件循环中,接着处理剩于的事件。

aeApiPoll内部调用epoll系统调用,等待指定事件发生或者超时。如果有事件发生则eploo_wait返回非0,如果为超时,则返回0。

当有事件发生时,内核会把所有发生的事件由内核层拷贝到应用层。eploo_wait返回时state->events就是保存是由内核返回的就绪表。


[cpp]   view plain  copy
  1. static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp)   
  2. {  
  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.     // 有至少一个事件就绪?  
  11.     if (retval > 0)   
  12.     {  
  13.         int j;  
  14.   
  15.         // 为已就绪事件设置相应的模式  
  16.         // 并加入到 eventLoop 的 fired 数组中  
  17.         numevents = retval;  
  18.         for (j = 0; j < numevents; j++)   
  19.     {  
  20.             int mask = 0;  
  21.             struct epoll_event *e = <span style="color:#ff0000;">state->events</span>+j;  
  22.   
  23.             if (e->events & EPOLLIN)  
  24.         mask |= AE_READABLE;  
  25.             if (e->events & EPOLLOUT)  
  26.         mask |= AE_WRITABLE;  
  27.             if (e->events & EPOLLERR)   
  28.         mask |= AE_WRITABLE;  
  29.             if (e->events & EPOLLHUP)  
  30.         mask |= AE_WRITABLE;  
  31.   
  32.             eventLoop->fired[j].fd = e->data.fd;  
  33.             eventLoop->fired[j].mask = mask;  
  34.         }  
  35.     }  
  36.       
  37.     // 返回已就绪事件个数  
  38.     return numevents;  
  39. }  
内核已经把就绪事件从内核拷贝到了应用层的epoll就绪表 state->events中。之后aeApiPoll会把epoll就绪表 state->events中的就绪事件拷贝到fired就绪表中

五、删除事件

当不在需要某个事件时,需要把事件删除掉。例如: 如果fd同时监听读事件、写事件。当不在需要监听写事件时,可以把该fd的写事件删除。

[cpp]   view plain  copy
  1. void aeDeleteFileEvent(aeEventLoop *eventLoop, int fd, int mask)  
  2. {  
  3.   
  4.     // 取出文件事件结构  
  5.     aeFileEvent *fe = &eventLoop->events[fd];  
  6.   
  7.     // 未设置监听的事件类型,直接返回  
  8.     if (fe->mask == AE_NONE)  
  9.     return;  
  10.   
  11.     // 计算新掩码  
  12.     fe->mask = fe->mask & (~mask);  
  13.   
  14.     // 取消对给定 fd 的给定事件的监视  
  15.     aeApiDelEvent(eventLoop, fd, mask);  
  16. }  
[cpp]   view plain  copy
  1. static void aeApiDelEvent(aeEventLoop *eventLoop, int fd, int delmask)   
  2. {  
  3.     aeApiState *state = eventLoop->apidata;  
  4.     struct epoll_event ee;  
  5.     int mask = eventLoop->events[fd].mask & (~delmask);  
  6.   
  7.     ee.events = 0;  
  8.     if (mask & AE_READABLE)   
  9.     ee.events |= EPOLLIN;  
  10.     if (mask & AE_WRITABLE)   
  11.     ee.events |= EPOLLOUT;  
  12.       
  13.     ee.data.u64 = 0; /* avoid valgrind warning */  
  14.     ee.data.fd = fd;  
  15.     if (mask != AE_NONE)   
  16.     {  
  17.         epoll_ctl(state->epfd,EPOLL_CTL_MOD,fd,&ee);  
  18.     }   
  19.     else   
  20.     {  
  21.         epoll_ctl(state->epfd,EPOLL_CTL_DEL,fd,&ee);  
  22.     }  
  23. }  
删除的过程可以总结为以下几个步骤

1、根据fd在未就绪表中查找到事件

2、取消该fd对应的相应事件标识符

3、通知内核,内核会将红黑树上的相应事件也给取消。

猜你喜欢

转载自blog.csdn.net/tuxedolinux/article/details/80560915