中国领先的IT技术网站
|
|

Redis源码学习之事件驱动

Redis基于多路复用技术实现了一套简单的事件驱动库,代码在ae.h、ae.c以及ae_epoll.c、ae_evport.c和ae_kqueue.c、ae_select.c这几个文件中。其中ae表示的是antirez eventloop的意思。

作者:佚名来源:数据库开发|2017-06-12 10:31

沙龙活动 | 去哪儿、陌陌、ThoughtWorks在自动化运维中的实践!10.28不见不散!


Redis基于多路复用技术实现了一套简单的事件驱动库,代码在ae.h、ae.c以及ae_epoll.c、ae_evport.c和ae_kqueue.c、ae_select.c这几个文件中。其中ae表示的是antirez eventloop的意思。

Redis里面包含两种事件类型:FileEvent和TimeEvent。

Redis采用IO多路复用技术,所有的事件都是在一个线程中进行处理。Redis的事件驱动模型可以以以下为代码进行表示:

  1. int main(int argc,char **argv) 
  2.  
  3.  
  4.     while(true) { 
  5.  
  6.         // 等待事件到来:wait4Event(); 
  7.  
  8.         // 处理事件:processEvent() 
  9.  
  10.     } 
  11.  
  12.  

在一个死循环中等待事件的到来,然后对事件进行处理,以此往复。这就是一个最经典的网络编程模型。

1.基本数据结构

aeEventLoop

aeEventLoop是Redis中事件驱动模型的核心,封装了整个事件循环,其中每个字段解释如下:

  • maxfd:已经接受的最大的文件描述符。
  • setsize:当前循环中所能容纳的文件描述符的数量。
  • timeEventNextId:下一个时间事件的ID.
  • lastTime:上一次被访问的时间,用来检测系统时钟是否被修改。
  • events:指针,指向保存所有注册的事件的数组首地址。
  • fired:指针,保存所有已经买被触发的事件的数组首地址。
  • timeEventHead:Redis用一个链表来存储所有的时间事件,timeEventHead是指向这个链表的首节点指针。
  • stop:停止整个事件循环。
  • apiData:指针,指向epoll结构。
  • beforeSleep:函数指针。每次实现循环的时候,在阻塞直到时间到来之前,会先调用这个函数。

aeFileEvent和aeTimeEvent

这两个结构分别表示文件事件和时间事件,定义如下

  1. typedef struct aeFileEvent { 
  2.  
  3.     int mask; /* one of AE_(READABLE|WRITABLE) */ 
  4.  
  5.     aeFileProc *rfileProc; // 函数指针,写事件处理 
  6.  
  7.     aeFileProc *wfileProc; // 函数指针,读事件处理 
  8.  
  9.     void *clientData; // 具体的数据 
  10.  
  11. } aeFileEvent;  

其中mask表示文件事件类型掩码,可以是AE_READABLE表示是可读事件,AE_WRITABLE为可写事件。aeFileProc是函数指针。

  1. /* Time event structure */ 
  2.  
  3. typedef struct aeTimeEvent { 
  4.  
  5.     long long id; // 事件ID 
  6.  
  7.     long when_sec; // 事件触发的时间:s 
  8.  
  9.     long when_ms; // 事件触发的时间:ms 
  10.  
  11.     aeTimeProc *timeProc; // 函数指针 
  12.  
  13.     aeEventFinalizerProc *finalizerProc; // 函数指针:在对应的aeTieEvent节点被删除前调用,可以理解为aeTimeEvent的析构函数 
  14.  
  15.     void *clientData; // 指针,指向具体的数据 
  16.  
  17.     struct aeTimeEvent *next; // 指向下一个时间事件指针 
  18.  
  19. } aeTimeEvent;  

aeFiredEvent

aeFiredEvent结构表示一个已经被触发的事件,结果如下:

  1. /* A fired event */ 
  2.  
  3. typedef struct aeFiredEvent { 
  4.  
  5.     int fd; // 事件被触发的文件描述符 
  6.  
  7.     int mask; // 被触发事件的掩码,表示被触发事件的类型 
  8.  
  9. } aeFiredEvent;  

fd表示事件发生在哪个文件描述符上面,mask用来表示具体事件的类型。

aeApiState

Redis底层采用IO多路复用技术实现高并发,具体实现可以采用kqueue、select、epoll等技术。对于Linux来说,epoll的性能要优于select,所以以epoll为例来进行分析。

  1. typedef struct aeApiState { 
  2.  
  3.     int epfd; 
  4.  
  5.     struct epoll_event *events; 
  6.  
  7. } aeApiState;  

aeApiState封装了跟epoll相关的数据,epfd保存epoll_create()返回的文件描述符。

具体实现细节

事件循环启动:aeMain()

事件驱动的启动代码位于ae.c的aeMain()函数中,代码如下:

从aeMain()方法中可以看到,整个事件驱动是在一个while()循环中不停地执行aeProcessEvents()方法,在这个方法中执行从客户端发送过来的请求。

初始化:aeCreateEventLoop()

aeEventLoop的初始化是在aeCreateEventLoop()方法中进行的,这个方法是在server.c中的initServer()中调用的。实现如下:

在这个方法中主要就是给aeEventLoop对象分配内存然后并进行初始化。其中关键的地方有:

1、调用aeApiCreate()初始化epoll相关的数据。aeApiCreate()实现如下:

在aeApiCreate()方法中主要完成以下三件事:

  1. 分配aeApiState结构需要的内存。
  2. 调用epoll_create()方法生成epoll的文件描述符,并保存在aeApiState.epfd字段中。
  3. 把第一步分配的aeApiState的内存地址保存在EventLoop->apidata字段中。

2、初始化events中的mask字段为为AE_NONE。

生成fileEvent:aeCreateFileEvent()

Redis使用aeCreateFileEvent()来生成fileEvent,代码如下:

aeCreateFileEvent()方法主要做了以下三件事:

  1. 检查新增的fd是否超过所能容纳最大值。
  2. 调用aeApiAddEvent()方法把对应的fd以mask模式添加到epoll监听器中。
  3. 设置相应的字段值。

其中最关键的步骤是第二步,aeApiAddEvent()方法如下:

生成timeEvent:aeCreateTimeEvent()

aeCreateTimeEvent()方法主要是用来生成timeEvent节点,其实现比较简单,代码如下所示:

处理timeEevnt:processTimeEvents()

Redis在processTimeEvents()方法中来处理所有的timeEvent,实现如下:

  1. static int processTimeEvents(aeEventLoop *eventLoop) { 
  2.  
  3.     int processed = 0; 
  4.  
  5.     aeTimeEvent *te, *prev; 
  6.  
  7.     long long maxId; 
  8.  
  9.     time_t now = time(NULL); 
  10.  
  11.     /** 
  12.  
  13.      * 如果系统时间被调整到将来某段时间然后又被设置回正确的时间, 
  14.  
  15.      * 这种情况下链表中的timeEvent有可能会被随机的延迟执行,因 
  16.  
  17.      * 此在这个情况下把所有的timeEvent的触发时间设置为0表示及执行 
  18.  
  19.      */ 
  20.  
  21.     if (now < eventLoop->lastTime) { 
  22.  
  23.         te = eventLoop->timeEventHead; 
  24.  
  25.         while(te) { 
  26.  
  27.             te->when_sec = 0; 
  28.  
  29.             te = te->next
  30.  
  31.         } 
  32.  
  33.     } 
  34.  
  35.     eventLoop->lastTime = now; // 设置上次运行时间为now 
  36.  
  37.   
  38.  
  39.     prev = NULL
  40.  
  41.     te = eventLoop->timeEventHead; 
  42.  
  43.     maxId = eventLoop->timeEventNextId-1; 
  44.  
  45.     while(te) { 
  46.  
  47.         long now_sec, now_ms; 
  48.  
  49.         long long id; 
  50.  
  51.         /** 
  52.  
  53.          * 删除已经被标志位 删除 的时间事件 
  54.  
  55.          */ 
  56.  
  57.         if (te->id == AE_DELETED_EVENT_ID) { 
  58.  
  59.             aeTimeEvent *next = te->next
  60.  
  61.             if (prev == NULL
  62.  
  63.                 eventLoop->timeEventHead = te->next
  64.  
  65.             else 
  66.  
  67.                 prev->next = te->next
  68.  
  69.             if (te->finalizerProc) 
  70.  
  71.                 // 在时间事件节点被删除前调用finlizerProce()方法 
  72.  
  73.                 te->finalizerProc(eventLoop, te->clientData); 
  74.  
  75.             zfree(te); 
  76.  
  77.             te = next
  78.  
  79.             continue
  80.  
  81.         } 
  82.  
  83.         if (te->id > maxId) { 
  84.  
  85.             /** 
  86.  
  87.              * te->id > maxId 表示当前te指向的timeEvent为当前循环中新添加的, 
  88.  
  89.              * 对于新添加的节点在本次循环中不作处理。 
  90.  
  91.              * PS:为什么会出现这种情况呢?有可能是在timeProc()里面会注册新的timeEvent节点? 
  92.  
  93.              * 对于当前的Redis版本中不会出现te->id > maxId这种情况 
  94.  
  95.              */ 
  96.  
  97.             te = te->next
  98.  
  99.             continue
  100.  
  101.         } 
  102.  
  103.         aeGetTime(&now_sec, &now_ms); 
  104.  
  105.         if (now_sec > te->when_sec || 
  106.  
  107.             (now_sec == te->when_sec && now_ms >= te->when_ms)) 
  108.  
  109.         { 
  110.  
  111.             // 如果当前时间已经超过了对应的timeEvent节点设置的触发时间, 
  112.  
  113.             // 则调用timeProc()方法执行对应的任务 
  114.  
  115.             int retval; 
  116.  
  117.   
  118.  
  119.             id = te->id; 
  120.  
  121.             retval = te->timeProc(eventLoop, id, te->clientData); 
  122.  
  123.             processed++; 
  124.  
  125.             if (retval != AE_NOMORE) { 
  126.  
  127.                 // 要执行多次,则计算下次执行时间 
  128.  
  129.                 aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms); 
  130.  
  131.             } else { 
  132.  
  133.                 // 如果只需要执行一次,则把id设置为-1,再下次循环中删除 
  134.  
  135.                 te->id = AE_DELETED_EVENT_ID; 
  136.  
  137.             } 
  138.  
  139.         } 
  140.  
  141.         prev = te; 
  142.  
  143.         te = te->next
  144.  
  145.     } 
  146.  
  147.     return processed; 
  148.  
  149.  

在这个方法中会

  1. 判断系统时间有没有调整过,如果调整过,则会把timeEvent链表中的所有的timeEvent的触发时间设置为0,表示立即执行。
  2. 对timeEvent链表进行遍历,对于每个timeEvent节点,如果有:
    • 返回为AE_NOMORE,表示当前timeEvent节点属于一次性事件,标记该节点ID为AE_DELETED_EVENT_ID,表示删除节点,该节点将会在下一轮的循环中被删除。
    • 返回不是AE_NOMORE,表示当前timeEvent节点属于周期性事件,需要多次执行,调用aeAddMillisecondsToNow()方法设置下次被执行时间。
    • 如果已经被标记为删除(AE_DELETED_EVENT_ID),则立即释放对应节点内存,遍历下个节点。
      • 如果id大于maxId,则表示当前节点为本次循环中新增节点,咋本次循环中不错处理,继续下个节点。
      • 如果当前节点的触发时间大于当前时间,则调用对应节点的timeProc()方法执行任务。根据timeProc()方法的返回,又分为两种情况:

处理所有事件:aeProcessEvents()

Redis中所有的事件,包括timeEvent和fileEvent都是在aeProcessEvents()方法中进行处理的,刚方法实现如下:

  1. /* Process every pending time event, then every pending file event 
  2.  
  3. * (that may be registered by time event callbacks just processed). 
  4.  
  5. * Without special flags the function sleeps until some file event 
  6.  
  7. * fires, or when the next time event occurs (if any). 
  8.  
  9.  
  10. * If flags is 0, the function does nothing and returns
  11.  
  12. * if flags has AE_ALL_EVENTS setall the kind of events are processed. 
  13.  
  14. * if flags has AE_FILE_EVENTS set, file events are processed. 
  15.  
  16. * if flags has AE_TIME_EVENTS settime events are processed. 
  17.  
  18. * if flags has AE_DONT_WAIT set the function returns ASAP until all 
  19.  
  20. * the events that's possible to process without to wait are processed. 
  21.  
  22.  
  23. * The function returns the number of events processed. */ 
  24.  
  25. int aeProcessEvents(aeEventLoop *eventLoop, int flags) 
  26.  
  27.  
  28.     int processed = 0, numevents; 
  29.  
  30.     /** 
  31.  
  32.      * 既没有时间事件也没有文件事件,则直接返回 
  33.  
  34.      */ 
  35.  
  36.     if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0; 
  37.  
  38.     /** 
  39.  
  40.      * -1 == eventloop->maxfd 表示还么有任何aeFileEvent被添加到epoll     
  41.  
  42.      * 事件循环中进行监听 
  43.  
  44.      */ 
  45.  
  46.     if (eventLoop->maxfd != -1 || 
  47.  
  48.         ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) { 
  49.  
  50.         int j; 
  51.  
  52.         aeTimeEvent *shortest = NULL
  53.  
  54.         struct timeval tv, *tvp; 
  55.  
  56.   
  57.  
  58.         /** 
  59.  
  60.          * 如果有aeFileEvent需要处理,就先要从所有待处理的 
  61.  
  62.          * aeTimeEvent事件中找到最近的将要被执行的aeTimeEvent节点 
  63.  
  64.          * 并结算该节点触发时间 
  65.  
  66.          */ 
  67.  
  68.         if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT)) 
  69.  
  70.             shortest = aeSearchNearestTimer(eventLoop); 
  71.  
  72.         if (shortest) { 
  73.  
  74.             long now_sec, now_ms; 
  75.  
  76.   
  77.  
  78.             aeGetTime(&now_sec, &now_ms); 
  79.  
  80.             tvp = &tv; 
  81.  
  82.   
  83.  
  84.             /* How many milliseconds we need to wait for the next 
  85.  
  86.              * time event to fire? */ 
  87.  
  88.             // 计算epoll_wait()需要等待的时间 
  89.  
  90.             long long ms = 
  91.  
  92.                 (shortest->when_sec - now_sec)*1000 + 
  93.  
  94.                 shortest->when_ms - now_ms; 
  95.  
  96.   
  97.  
  98.             if (ms > 0) { 
  99.  
  100.                 tvp->tv_sec = ms/1000; 
  101.  
  102.                 tvp->tv_usec = (ms % 1000)*1000; 
  103.  
  104.             } else { 
  105.  
  106.                 tvp->tv_sec = 0; 
  107.  
  108.                 tvp->tv_usec = 0; 
  109.  
  110.             } 
  111.  
  112.         } else { 
  113.  
  114.             // 如果flags设置了AE_DONT_WAIT,则设置epoll_wait()等待时间为0, 
  115.  
  116.             // 即立刻从epoll中返回 
  117.  
  118.             if (flags & AE_DONT_WAIT) { 
  119.  
  120.                 tv.tv_sec = tv.tv_usec = 0; 
  121.  
  122.                 tvp = &tv; 
  123.  
  124.             } else { 
  125.  
  126.                 /* Otherwise we can block */ 
  127.  
  128.                 tvp = NULL; /* wait forever */ 
  129.  
  130.             } 
  131.  
  132.         } 
  133.  
  134.   
  135.  
  136.         // 调用aeApiPoll()进行阻塞等待事件的到来,等待时间为tvp 
  137.  
  138.         numevents = aeApiPoll(eventLoop, tvp); 
  139.  
  140.         for (j = 0; j < numevents; j++) { 
  141.  
  142.             aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd]; 
  143.  
  144.             int mask = eventLoop->fired[j].mask; 
  145.  
  146.             int fd = eventLoop->fired[j].fd; 
  147.  
  148.             int rfired = 0; 
  149.  
  150.             /* note the fe->mask & mask & ... code: maybe an already processed 
  151.  
  152.              * event removed an element that fired and we still didn't 
  153.  
  154.              * processed, so we check if the event is still valid. */ 
  155.  
  156.             // fe->mask && mask 的目的是确保对应事件时候还有效 
  157.  
  158.             if (fe->mask & mask & AE_READABLE) { 
  159.  
  160.                 rfired = 1; 
  161.  
  162.                 fe->rfileProc(eventLoop,fd,fe->clientData,mask); 
  163.  
  164.             } 
  165.  
  166.             if (fe->mask & mask & AE_WRITABLE) { 
  167.  
  168.                 if (!rfired || fe->wfileProc != fe->rfileProc) 
  169.  
  170.                     fe->wfileProc(eventLoop,fd,fe->clientData,mask); 
  171.  
  172.             } 
  173.  
  174.             processed++; 
  175.  
  176.         } 
  177.  
  178.     } 
  179.  
  180.     /* Check time events */ 
  181.  
  182.     if (flags & AE_TIME_EVENTS) 
  183.  
  184.         // 处理aeTimeEvent 
  185.  
  186.         processed += processTimeEvents(eventLoop); 
  187.  
  188.   
  189.  
  190.     return processed; /* return the number of processed file/time events */ 
  191.  
  192.  

该方法的入参flag表示要处理哪些事件,可以取以下几个值 :

  • AE_ALL_EVENTS:timeEvent和fileEvent都会处理。
  • AE_FILE_EVENTS:只处理fileEvent。
  • AE_TIME_EVENTS:只处理timeEvent。
  • AE_DONT_WAIT:要么立马返回,要么处理完那些不需要等待的事件之后再立马返回。

aeProcessEvents()方法会做下面几件事:

  1. 判断传入的flag的值,如果既不包含AE_TIME_EVENTS也不包含AE_FILE_EVENTS则直接返回。
  2. 计算如果有aeFileEvent事件需要进行处理,则先计算epoll_wait()方法需要阻塞等待的时间,计算方式如下:
    • 先从aeTimeEvent事件链表中找到最近的需要被触发的aeTimeEvent节点并计算需要被触发的时间,该被触发时间则为epoll_wait()需要等待的时间。
    • 如果没有找到最近的aeTimeEvent节点,表示没有aeTimeEvent节点被加入链表,则判断传入的flags是否包含AE_DONT_WAIT选项,则设置epoll_wait()需要等待时间为0,表示立即返回。
    • 如果没有设置AE_DONT_WAIT,则设置需要等待时间为NULL,表示epoll_wait()一直阻塞等待知道有fileEvent事件到来。
  3. 调用aeApiPoll()方法阻塞等待事件的到来,阻塞时间为第二步中计算的时间。aeApiPoll()实现见文末:
    • aeApiPoll()会做下面几件事:
      • 根据传入的tvp计算需要阻塞的时间,然后调用epoll_wait()进行阻塞等待。
      • 有事件到来之后先计算对应事件的类型。
      • 把事件发生的fd以及对应的类型mask拷贝到fired数组中。
  4. 从aeApiPoll()方法返回之后,所有事件已经就绪了的fd以及对应事件的类型mask已经保存在eventLoop->fired[]数组中。依次遍历fired数组,根据mask类型,执行对应的frileProc()或者wfileProce()方法。
  5. 如果传入的flags中有AE_TIME_EVENTS,则调用processTimeEvents()执行所有已经到时间了的timeEvent。

本系列

【编辑推荐】

  1. 分布式中使用Redis实现Session共享
  2. .NET中Redis的使用
  3. Redis快速入门
  4. Redis的丰富扩展之高性能图数据库
  5. Redis源码学习之Redis事务
【责任编辑:枯木 TEL:(010)68476606】

点赞 0
分享:
大家都在看
猜你喜欢

读 书 +更多

游戏关卡设计

《半条命》作者倾心写就 暴雪总裁等业内专家强力推荐 盛大公司专业团队翻译 一起来创造引人入胜的游戏体验吧! 任何精彩游戏的核心部分...

订阅51CTO邮刊

点击这里查看样刊

订阅51CTO邮刊
× Python最火的编程语言