redis源码分析(2)——事件循环

151 篇文章 2 订阅

redis源码分析(2)——事件循环

http://blog.csdn.net/chosen0ne/article/details/42717571

redis作为服务器程序,网络IO处理是关键。redis不像memcached使用libevent,它实现了自己的IO事件框架,并且很简单、小巧。可以选择select、epoll、kqueue等实现。
     作为 IO事件框架,需要抽象多种IO模型的共性,将整个过程主要抽象为:
           1)初始化
         2)添加、删除事件
           3)等待事件发生
      下面也按照这个步骤分析代码。

(1)初始化

回忆一下redis的初始化过程中,initServer函数会调用aeCreateEventLoop创建event loop对象,对事件循环进行初始化。下面看一下aeEventLoop结构,存储事件循环相关的属性。
  1. typedef struct aeEventLoop {  
  2.     int maxfd;   /* highest file descriptor currently registered */  
  3.     int setsize; /* max number of file descriptors tracked */  
  4.     long long timeEventNextId;  
  5.     // <MM>  
  6.     // 存放的是上次触发定时器事件的时间  
  7.     // </MM>  
  8.     time_t lastTime;     /* Used to detect system clock skew */  
  9.     aeFileEvent *events; /* Registered events */  
  10.     aeFiredEvent *fired; /* Fired events */  
  11.     // <MM>  
  12.     // 所有定时器事件组织成链表  
  13.     // </MM>  
  14.     aeTimeEvent *timeEventHead;  
  15.     // <MM>  
  16.     // 是否停止eventLoop  
  17.     // </MM>  
  18.     int stop;  
  19.     void *apidata; /* This is used for polling API specific data */  
  20.     // <MM>  
  21.     // 事件循环每一次迭代都会调用beforesleep  
  22.     // </MM>  
  23.     aeBeforeSleepProc *beforesleep;  
  24. } aeEventLoop;  
setsize:指定事件循环要监听的文件描述符集合的大小。这个值与配置文件中得maxclients有关。
      events:存放所有注册的读写事件,是大小为setsize的数组。内核会保证新建连接的fd是当前可用描述符的最小值,所以最多监听setsize个描述符,那么最大的fd就是setsize - 1。这种组织方式的好处是,可以以fd为下标,索引到对应的事件,在事件触发后根据fd快速查找到对应的事件。
     fired:存放触发的读写事件。同样是setsize大小的数组。
      timeEventHead:redis将定时器事件组织成链表,这个属性指向表头。
      apidata:存放epoll、select等实现相关的数据。
      beforesleep:事件循环在每次迭代前会调用beforesleep执行一些异步处理。

io模型初始化的抽象函数为aeApiCreate。aeCreateEventLoop函数创建并初始化全局事件循环结构,并调用aeApiCreate初始化具体实现依赖的数据结构。
  1. aeEventLoop *aeCreateEventLoop(int setsize) {  
  2.     aeEventLoop *eventLoop;  
  3.     int i;  
  4.   
  5.     // <MM>  
  6.     // setsize指定事件循环监听的fd的数目  
  7.     // 由于内核保证新创建的fd是最小的正整数,所以直接创建setsize大小  
  8.     // 的数组,存放对应的event  
  9.     // </MM>  
  10.     if ((eventLoop = zmalloc(sizeof(*eventLoop))) == NULL) goto err;  
  11.     eventLoop->events = zmalloc(sizeof(aeFileEvent)*setsize);  
  12.     eventLoop->fired = zmalloc(sizeof(aeFiredEvent)*setsize);  
  13.     if (eventLoop->events == NULL || eventLoop->fired == NULL) goto err;  
  14.     eventLoop->setsize = setsize;  
  15.     eventLoop->lastTime = time(NULL);  
  16.     eventLoop->timeEventHead = NULL;  
  17.     eventLoop->timeEventNextId = 0;  
  18.     eventLoop->stop = 0;  
  19.     eventLoop->maxfd = -1;  
  20.     eventLoop->beforesleep = NULL;  
  21.     if (aeApiCreate(eventLoop) == -1) goto err;  
  22.     /* Events with mask == AE_NONE are not set. So let's initialize the 
  23.      * vector with it. */  
  24.     for (i = 0; i < setsize; i++)  
  25.         eventLoop->events[i].mask = AE_NONE;  
  26.     return eventLoop;  
  27.   
  28. err:  
  29.     if (eventLoop) {  
  30.         zfree(eventLoop->events);  
  31.         zfree(eventLoop->fired);  
  32.         zfree(eventLoop);  
  33.     }  
  34.     return NULL;  
  35. }  
以epoll为例,aeApiCreate主要是创建epoll的fd,以及要监听的epoll_event,这些数据定义在:
  1. typedef struct aeApiState {  
  2.     int epfd;  
  3.     struct epoll_event *events;  
  4. } aeApiState;  
这里,监听到的事件组织方式与event_loop中监听事件一样,同样是setsize大小的数据,以fd为下标。
aeApiCreate会初始化这些属性,并将aeApiState结构存放到eventLoop->apidata。
  1. static int aeApiCreate(aeEventLoop *eventLoop) {  
  2.     aeApiState *state = zmalloc(sizeof(aeApiState));  
  3.   
  4.     if (!state) return -1;  
  5.     state->events = zmalloc(sizeof(struct epoll_event)*eventLoop->setsize);  
  6.     if (!state->events) {  
  7.         zfree(state);  
  8.         return -1;  
  9.     }  
  10.     state->epfd = epoll_create(1024); /* 1024 is just a hint for the kernel */  
  11.     if (state->epfd == -1) {  
  12.         zfree(state->events);  
  13.         zfree(state);  
  14.         return -1;  
  15.     }  
  16.     eventLoop->apidata = state;  
  17.     return 0;  
  18. }  

(2)添加、删除事件

redis支持两类事件,网络io事件和定时器事件。定时器事件的添加、删除相对简单些,主要是维护定时器事件列表。首先看一下表示定时器事件的结构:
  1. /* Time event structure */  
  2. typedef struct aeTimeEvent {  
  3.     long long id; /* time event identifier. */  
  4.     long when_sec; /* seconds */  
  5.     long when_ms; /* milliseconds */  
  6.     aeTimeProc *timeProc;  
  7.     aeEventFinalizerProc *finalizerProc;  
  8.     void *clientData;  
  9.     struct aeTimeEvent *next;  
  10. } aeTimeEvent;  
when_sec和when_ms:表示定时器触发的事件戳,在事件循环迭代返回后,如果当前时间戳大于这个值就会回调事件处理函数。
      timeProc:事件处理函数。
      finalizerProc:清理函数,在删除定时器时调用。
      clientData:需要传入事件处理函数的参数。
      next:定时器事件组织成链表,next指向下一个事件。

aeCreateTimeEvent函数用于添加定时器事件,逻辑很简单,根据当前时间计算下一次触发的事件,对事件属性赋值,并插入到定时器链表表头之前。删除通过aeDeleteTimeEvent函数,根据id找到事件并从链表删除该节点,回调清理函数。具体定时器事件的处理见后文,下面看一下io事件。
io事件的添加通过aeCreateFileEvent,逻辑很简单,根据要注册的fd,获取其event,设置属性,会调用aeApiAddEvent函数添加到底层的io模型。
  1. int aeCreateFileEvent(aeEventLoop *eventLoop, int fd, int mask,  
  2.         aeFileProc *proc, void *clientData)  
  3. {  
  4.     if (fd >= eventLoop->setsize) {  
  5.         errno = ERANGE;  
  6.         return AE_ERR;  
  7.     }  
  8.     aeFileEvent *fe = &eventLoop->events[fd];  
  9.   
  10.     if (aeApiAddEvent(eventLoop, fd, mask) == -1)  
  11.         return AE_ERR;  
  12.     fe->mask |= mask;  
  13.     if (mask & AE_READABLE) fe->rfileProc = proc;  
  14.     if (mask & AE_WRITABLE) fe->wfileProc = proc;  
  15.     fe->clientData = clientData;  
  16.     if (fd > eventLoop->maxfd)  
  17.         eventLoop->maxfd = fd;  
  18.     return AE_OK;  
  19. }  
mask:指定注册的事件类型,可以是读或写。
      proc:事件处理函数。

下面是io事件的结构,包括注册的事件类型mask,读写事件处理函数,以及对应的参数。
  1. /* File event structure */  
  2. typedef struct aeFileEvent {  
  3.     int mask; /* one of AE_(READABLE|WRITABLE) */  
  4.     aeFileProc *rfileProc;  
  5.     aeFileProc *wfileProc;  
  6.     void *clientData;  
  7. } aeFileEvent;  
下面看一下epoll添加事件的实现,主要是调用epoll_ctl
  1. static int aeApiAddEvent(aeEventLoop *eventLoop, int fd, int mask) {  
  2.     aeApiState *state = eventLoop->apidata;  
  3.     struct epoll_event ee;  
  4.     /* If the fd was already monitored for some event, we need a MOD 
  5.      * operation. Otherwise we need an ADD operation. */  
  6.     int op = eventLoop->events[fd].mask == AE_NONE ?  
  7.             EPOLL_CTL_ADD : EPOLL_CTL_MOD;  
  8.   
  9.     ee.events = 0;  
  10.     mask |= eventLoop->events[fd].mask; /* Merge old events */  
  11.     if (mask & AE_READABLE) ee.events |= EPOLLIN;  
  12.     if (mask & AE_WRITABLE) ee.events |= EPOLLOUT;  
  13.     ee.data.u64 = 0; /* avoid valgrind warning */  
  14.     ee.data.fd = fd;  
  15.     if (epoll_ctl(state->epfd,op,fd,&ee) == -1) return -1;  
  16.     return 0;  
  17. }  
struct epll_event用于指定要监听的事件,以及该文件描述符绑定的data,在事件触发时可以返回。这里将data直接存为fd,通过这个数据,便可以找到对应的事件,然后调用其处理函数。
      epoll的删除与添加类似,不再赘述。

(3)等待事件触发

通过调用aeMain函数进入事件循环:
  1. void aeMain(aeEventLoop *eventLoop) {  
  2.     eventLoop->stop = 0;  
  3.     while (!eventLoop->stop) {  
  4.         if (eventLoop->beforesleep != NULL)  
  5.             eventLoop->beforesleep(eventLoop);  
  6.         aeProcessEvents(eventLoop, AE_ALL_EVENTS);  
  7.     }  
  8. }  
函数内部就是一个while循环,不断的调用aeProcessEvents函数,等待事件发生。在每次迭代前会调用会调用beforesleep函数,处理异步任务,后续会和serverCron一起介绍。
      aeProcessEvents函数首先会处理定时器事件,然后是io事件,下面介绍这个函数的实现。
      首先,声明变量记录处理的事件个数,以及触发的事件。flags表示此轮需要处理的事件类型,如果不需要处理定时器事件和io事件直接返回。
  1. int processed = 0, numevents;  
  2.   
  3. /* Nothing to do? return ASAP */  
  4. if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;  
redis中的定时器事件是通过epoll实现的。大体思路是,在每次事件迭代调用epoll_wait时需要指定此轮sleep的时间。如果没有io事件发生,则在sleep时间到了之后会返回。通过算出下一次最先发生的事件,到当前时间的间隔,用这个值设为sleep,这样就可以保证在事件到达后回调其处理函数。但是,由于每次返回后,还有处理io事件,所以定时器的触发事件是不精确的,一定是比预定的触发时间晚的。下面看下具体实现。
      首先是,查找下一次最先发生的定时器事件,以确定sleep的事件。如果没有定时器事件,则根据传入的flags,选择是一直阻塞指导io事件发生,或者是不阻塞,检查完立即返回。通过调用aeSearchNearestTimer函数查找最先发生的事件,采用的是线性查找的方式,复杂度是O(n),可以将定时器事件组织成堆,加快查找。不过,redis中只有一个serverCron定时器事件,所以暂时不需优化。
  1. /* Note that we want call select() even if there are no 
  2.  * file events to process as long as we want to process time 
  3.  * events, in order to sleep until the next time event is ready 
  4.  * to fire. */  
  5. // <MM>  
  6. // 在两种情况下进入poll,阻塞等待事件发生:  
  7. // 1)在有需要监听的描述符时(maxfd != -1)  
  8. // 2)需要处理定时器事件,并且DONT_WAIT开关关闭的情况下  
  9. // </MM>  
  10. if (eventLoop->maxfd != -1 ||  
  11.     ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {  
  12.     int j;  
  13.     aeTimeEvent *shortest = NULL;  
  14.     struct timeval tv, *tvp;  
  15.   
  16.     // <MM>  
  17.     // 根据最快发生的定时器事件的发生时间,确定此次poll阻塞的时间  
  18.     // </MM>  
  19.     if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))  
  20.         // <MM>  
  21.         // 线性查找最快发生的定时器事件  
  22.         // </MM>  
  23.         shortest = aeSearchNearestTimer(eventLoop);  
  24.     if (shortest) {  
  25.         // <MM>  
  26.         // 如果有定时器事件,则根据它触发的时间,计算sleep的时间(ms单位)  
  27.         // </MM>  
  28.         long now_sec, now_ms;  
  29.   
  30.         /* Calculate the time missing for the nearest 
  31.          * timer to fire. */  
  32.         aeGetTime(&now_sec, &now_ms);  
  33.         tvp = &tv;  
  34.         tvp->tv_sec = shortest->when_sec - now_sec;  
  35.         if (shortest->when_ms < now_ms) {  
  36.             tvp->tv_usec = ((shortest->when_ms+1000) - now_ms)*1000;  
  37.             tvp->tv_sec --;  
  38.         } else {  
  39.             tvp->tv_usec = (shortest->when_ms - now_ms)*1000;  
  40.         }  
  41.         if (tvp->tv_sec < 0) tvp->tv_sec = 0;  
  42.         if (tvp->tv_usec < 0) tvp->tv_usec = 0;  
  43.     } else {  
  44.         // <MM>  
  45.         // 如果没有定时器事件,则根据情况是立即返回,或者永远阻塞  
  46.         // </MM>  
  47.         /* If we have to check for events but need to return 
  48.          * ASAP because of AE_DONT_WAIT we need to set the timeout 
  49.          * to zero */  
  50.         if (flags & AE_DONT_WAIT) {  
  51.             tv.tv_sec = tv.tv_usec = 0;  
  52.             tvp = &tv;  
  53.         } else {  
  54.             /* Otherwise we can block */  
  55.             tvp = NULL; /* wait forever */  
  56.         }  
  57.     }  
接着,调用aeApiPoll函数,传入前面计算的sleep时间,等待io事件放生。在函数返回后,触发的事件已经填充到eventLoop的fired数组中。epoll的实现如下,就是调用epoll_wait,函数返回后,会将触发的事件存放到state->events数组中的前numevents个元素。接下来,填充fired数组,设置每个触发事件的fd,以及事件类型。
  1. static int aeApiPoll(aeEventLoop *eventLoop, struct timeval *tvp) {  
  2.     aeApiState *state = eventLoop->apidata;  
  3.     int retval, numevents = 0;  
  4.   
  5.     // <MM>  
  6.     // 调用epoll_wait,state->events存放返回的发生事件的fd  
  7.     // </MM>  
  8.     retval = epoll_wait(state->epfd,state->events,eventLoop->setsize,  
  9.             tvp ? (tvp->tv_sec*1000 + tvp->tv_usec/1000) : -1);  
  10.     if (retval > 0) {  
  11.         int j;  
  12.   
  13.         numevents = retval;  
  14.         // <MM>  
  15.         // 有事件发生,将发生的事件存放于fired数组  
  16.         // </MM>  
  17.         for (j = 0; j < numevents; j++) {  
  18.             int mask = 0;  
  19.             struct epoll_event *e = state->events+j;  
  20.   
  21.             if (e->events & EPOLLIN) mask |= AE_READABLE;  
  22.             if (e->events & EPOLLOUT) mask |= AE_WRITABLE;  
  23.             if (e->events & EPOLLERR) mask |= AE_WRITABLE;  
  24.             if (e->events & EPOLLHUP) mask |= AE_WRITABLE;  
  25.             eventLoop->fired[j].fd = e->data.fd;  
  26.             eventLoop->fired[j].mask = mask;  
  27.         }  
  28.     }  
  29.     return numevents;  
  30. }  
在事件返回后,需要处理事件。遍历fired数组,取得fd对应的事件,并根据触发的事件类型,回调其处理函数。
  1. for (j = 0; j < numevents; j++) {  
  2.     // <MM>  
  3.     // poll返回后,会将所有触发的时间存放于fired数组  
  4.     // </MM>  
  5.     aeFileEvent *fe = &eventLoop->events[eventLoop->fired[j].fd];  
  6.     int mask = eventLoop->fired[j].mask;  
  7.     int fd = eventLoop->fired[j].fd;  
  8.     int rfired = 0;  
  9.   
  10.     /* note the fe->mask & mask & ... code: maybe an already processed 
  11.      * event removed an element that fired and we still didn't 
  12.      * processed, so we check if the event is still valid. */  
  13.     // <MM>  
  14.     // 回调发生事件的fd,注册的事件处理函数  
  15.     // </MM>  
  16.     if (fe->mask & mask & AE_READABLE) {  
  17.         rfired = 1;  
  18.         fe->rfileProc(eventLoop,fd,fe->clientData,mask);  
  19.     }  
  20.     if (fe->mask & mask & AE_WRITABLE) {  
  21.         if (!rfired || fe->wfileProc != fe->rfileProc)  
  22.             fe->wfileProc(eventLoop,fd,fe->clientData,mask);  
  23.     }  
  24.     processed++;  
  25. }  
以上便是,io事件的处理,下面看一下定时器事件的处理。会调用processTimeEvents函数处理定时器事件。
      首先会校验是否发生系统时钟偏差(system clock skew,修改系统事件会发生?把事件调到过去),如果发生就将所有事件的发生时间置为0,立即触发。
  1. /* If the system clock is moved to the future, and then set back to the 
  2.  * right value, time events may be delayed in a random way. Often this 
  3.  * means that scheduled operations will not be performed soon enough. 
  4.  * 
  5.  * Here we try to detect system clock skews, and force all the time 
  6.  * events to be processed ASAP when this happens: the idea is that 
  7.  * processing events earlier is less dangerous than delaying them 
  8.  * indefinitely, and practice suggests it is. */  
  9. if (now < eventLoop->lastTime) {  
  10.     te = eventLoop->timeEventHead;  
  11.     while(te) {  
  12.         te->when_sec = 0;  
  13.         te = te->next;  
  14.     }  
  15. }  
  16. eventLoop->lastTime = now;  
接下来遍历所有定时器事件,查找触发的事件,然后回调处理函数。定时器事件处理函数的返回值,决定这个事件是一次性的,还是周期性的。如果返回AE_NOMORE,则是一次性事件,在调用完后会删除该事件。否则的话,返回值指定的是下一次触发的时间。
  1. te = eventLoop->timeEventHead;  
  2. maxId = eventLoop->timeEventNextId-1;  
  3. while(te) {  
  4.     long now_sec, now_ms;  
  5.     long long id;  
  6.   
  7.     if (te->id > maxId) {  
  8.         te = te->next;  
  9.         continue;  
  10.     }  
  11.     aeGetTime(&now_sec, &now_ms);  
  12.     if (now_sec > te->when_sec ||  
  13.         (now_sec == te->when_sec && now_ms >= te->when_ms))  
  14.     {  
  15.         // <MM>  
  16.         // 定时器事件的触发时间已过,则回调注册的事件处理函数  
  17.         // </MM>  
  18.         int retval;  
  19.   
  20.         id = te->id;  
  21.         retval = te->timeProc(eventLoop, id, te->clientData);  
  22.         processed++;  
  23.         /* After an event is processed our time event list may 
  24.          * no longer be the same, so we restart from head. 
  25.          * Still we make sure to don't process events registered 
  26.          * by event handlers itself in order to don't loop forever. 
  27.          * To do so we saved the max ID we want to handle. 
  28.          * 
  29.          * FUTURE OPTIMIZATIONS: 
  30.          * Note that this is NOT great algorithmically. Redis uses 
  31.          * a single time event so it's not a problem but the right 
  32.          * way to do this is to add the new elements on head, and 
  33.          * to flag deleted elements in a special way for later 
  34.          * deletion (putting references to the nodes to delete into 
  35.          * another linked list). */  
  36.         // <MM>  
  37.         // 根据定时器事件处理函数的返回值,决定是否将该定时器删除。  
  38.         // 如果retval不等于-1(AE_NOMORE),则更改定时器的触发时间为  
  39.         // now + retval(ms)  
  40.         // </MM>  
  41.         if (retval != AE_NOMORE) {  
  42.             aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);  
  43.         } else {  
  44.             // <MM>  
  45.             // 如果返回AE_NOMORE,则删除该定时器  
  46.             // </MM>  
  47.             aeDeleteTimeEvent(eventLoop, id);  
  48.         }  
  49.         te = eventLoop->timeEventHead;  
  50.     } else {  
  51.         te = te->next;  
  52.     }  
  53. }  
在回调处理函数时,有可能会添加新的定时器事件,如果不断加入,存在无限循环的风险,所以需要避免这种情况,每次循环不处理新添加的事件,这是通过下面的代码实现的。
  1. if (te->id > maxId) {  
  2.     te = te->next;  
  3.     continue;  
  4. }  

事件循环部分分析到此结束,感觉比较直观、清晰,完全可以抽出来,作为一个独立的库使用。下面一节,会介绍请求的处理。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值