开始进入ae事件库的实现,这部分代码位于ae.c


image.png
1553518087124571.png

同样,没有任何外部库的依赖,其中config.h配置头文件会决定IO多路复用的选取,如下

image.png
1553518189574268.png

此处按照性能降序排列,这四种实现都是对系统IO多路复用库API进行一定程度的封装,其中kqueue和evport用于FreeBSD和Solaris,由于二者不如linux流行,因此平时见的会比较少,大部分人都没有用过kqueue与evport,因此我们后面封装部分会去剖析epoll api的封装。这四个实现实现封装成同一套API,把IO多路复用与事件库本身的解耦,其封装都是标准使用。

ps:下面我们统一使用实现为epoll


image.png
1553519026593187.png

创建一个时间循环,如果有任何步骤不成功则全部rollback,对于文件事件需要把每个事件的mask标志置为AE_NONE

events数组里面全部放的是文件事件,setsize实际上是events数组(文件事件)的size,maxfd记录的是当前events数组中句柄值最大的文件描述符,这样处理的原因是为了可以在O(1)时间内对任意文件事件进行操作。例如,对于文件描述符fd=11的上的文件事件,他的FileEvent就被放在events[11],即使他可能是第一个被加入进events数组的,这样也算是空间换时间。

再有一点就是fired数组是用于记录epoll原生接口中的事件数组中每个fd是被何种它关注的事件所激活,这个可能现在这样看会觉得比较绕,慢慢看,后面就明白了,总之fired数组记录了每次就绪的fd与fd上对应的时间类型。


image.png
1553520143775263.png

获取setsize大小,不多说


image.png
1553520449139477.png

重置setsize大小,即对events数组的size进行操作。

如果setsize<=maxfd,那么返回失败,显然如果setsize<=maxfd,那么包括maxfd对应的events[maxfd]在内,一定会有一部分事件丢失,这是不允许的;

其中aeApiResize调用的那个四个实现里的封号好的API,重置IO多路复用监听对象的数量,很简单,例如在epoll中就会调整struct epoll_event数组的的大小;

接下来调整events数组与fired数组的长度,setsize的大小,最要把events中文件事件的mask再次初始化为AE_NONE


image.png
1553521169813727.png

删除整个事件循环,不多说。


image.png
1553521722749457.png

在事件循环中创建一个文件事件,首先当然是检查新创建时间的fd是否放得下,若是放不下须返回失败。

接下来找到fd在events数组中对应“槽位”(个人认为很形象)的地址,在aeApiAddEvent中真正要增加该文件描述符上的事件,同样也很简单,在epoll中就是对应EPOLLIN与EPOLLOUT,最后调用epoll_ctl。

此后对该文件事件的mask位进行赋值,同时注册读处理函数与写处理函数到该事件上,最后检查是否需要更新maxfd。

这样下来,就把文件描述符fd与事件循环中的events[fd]进行了绑定,并注册了相应的处理函数,在epoll(或者其他)中也完成了底层真正的注册。


image.png
1553522161677003.png

删除事件循环中的某个文件事件,注意可能是仅删除读事件或写事件中的一个还有一个保留

首先检查fd是否在范围内,检查该事件是否需要删除,接下来在底层真正删除它,然后把fe的mask标志位进行相应的置位。

最后若完全删除(即读写都不留)了maxfd对应的事件,还需要从后往前遍历找到当前最大fd对应的事件以更新maxfd。


image.png
1553523027989213.png

获取指定fd上的事件类型,注意一个细节,一般人可能注意不到,在高并发的系统中,是获取值还是指针不是一个随意的事,比如此处为何非要获取该事件的指针再返回对应mask,因为如果获取的是值的话,很有可能你获取的值下一时刻就已经被更新,因此需要获取指针。


image.png
1553523296211094.png

aeGetTime获取此刻的Unix事件,精确到毫秒,aeAddMilliSeconds在入参sec与ms对应的时间上增加入参milliseconds毫秒,更新sec与ms。与刚才说的对应,这些操作一定都会用指针,而不会去用值,很多人也许只会觉得传指针方便,事实上真正的好处还是最大程度的保证了实时性。


image.png
1553523733638838.png

创建一个时间事件,时间事件即达到对应时间点时才能被触发的事件,亦可被称为定时时间。这里需要注意时间事件是以单向链表进行直接串联,每次新增加的一个时间事件会放在链表头。id是时间事件的唯一标识,就像文件描述符唯一标识文件事件。


image.png
1553523891740374.png

删除指定id的时间事件,实际上这个删除动作是给这个时间事件加入删除标志位,后面在处理时间事件的函数的中回去回收有这个标志位的时间事件。


image.png
1553524048592033.png

搜索离此刻最近的一个时间事件,在此认为时间事件链表中的时间事件的触发时间都是被设定在此刻之前的,因此要遍历链表找出距离现在最近的一个时间事件,这个过程的复杂度为O(N)。的确可以优化,但是目前redis没有这个需求,如果对于时间事件有优化需求的话,有两种方案,官方提出两种方案:

  • 有序的插入,按离此刻时间的时间差对时间事件升序排列,这样链表头永远离现在最近,可在O(1)时间内取到离现在最近的时间事件,但是这样会使插入的复杂度变为O(N)。

  • 使用跳表,O(1)时间内取到离现在最近的时间事件,并且插入时间复杂度为O(logN)。

![image.png][1]

处理时间事件,首先注释中的话,直白的翻译一下:

(要处理的场景)如果系统时钟在某个时刻被调到了未来,接着又被调回到了正确的时间,那么时间事件可能被以一种随机的方式推迟执行。通常这意味着计划好的操作不会尽快被执行。

(处理方法)在这里我们会尽力去监测系统时间的畸变,并且当时间发生畸变时会强制尽快执行所有的时间事件:这样做的原因就在于,尽早去处理事件总是比遥遥无期(是在不知道用什么词才合适了)的去推迟它要好,并且实践也验证了这一点。

这一点就是通过lasttime与now来进行比较来判断系统时钟是否发生畸变,正常情况下now时间必定会大于等于lasttime,lasttime记录的是上一次处理时间事件的时刻或者是刚初始化eventsloop时的时刻(见aeCreateEventLoop),因此当now小于lasttime时,会把eventsloop中的所有事件事件的执行时刻改为0,这就保证了这些时间接下来会全部被执行,这个过程只需要遍历一遍整个时间事件链表就能完成。

最后无论时间是否发生了畸变都要更新lasttime


image.png
1553597764806211.png

接下来在一个循环中开始处理时间事件,首先要做的是判断是否该时间事件是否被标记为删除,如果是则去释放该节点,并注意对是否需要更新链表头。在释放过程中,如果发现注册了finalizerProc清理函数则会使用其对clientData数据进行相应的清理工作,接下来释放整个时间事件节点,做完这些后遍历指针te往后走进入下次循环。

接下来注意一个可能不会比较奇怪的地方,我们看到有这样一个判断:

image.png
1553598505934382.png

是否不太明白?这部分是这样:因为一个时间事件本身的动作也可以是去创建一个新的时间事件在时间循环中,因为,我们在时间事件处理过中应该跳过由时间本身新创建的时间,这是符合逻辑的,同时也不会形成潜在死循环,不然还可以永远创建一个时间事件让他去执行创建一个时间事件的动作,这个地方的逻辑大部分人可能注意不到。

接下来便是获取当前的时间,然后判断该时间事件是否已达到触发点,若达到则通过其执行其timeProc,接下来retval会接收timeProc返回值,这个返回值的意义是下次执行本时间事件到现在的间隔,如下

image.png
1553599189967214.png

显然,时间事件明显就被分为了周期性时间事件与非周期性时间事件两种,非周期性时间在执行完后便会被置删除标志,在下次进入processTimeEvents时会被删除、清理。

其中processed变量作为返回值,它会记录本次时间事件处理中真正处理时间事件的次数。


image.png
1553599882751963.png

处理所有事件的主干函数,首先对于flags入参如下

image.png
1553599950498541.png

flags可以标识处理的何种事件:文件事件、时间事件、二者都处理。同时AE_DONT_WAIT标志会让整个处理过程尽快结束尽量避免等待。

在对flags做一个基本的检查后,进入下面

image.png
1553600153351974.png

接下来的判断只会让两种情况进入:

1、有文件事件需要处理

2、flags被置时间事件标志位,并且是阻塞的方式

若二者都不满足但是flags仍然被置时间事件标志位,则应该考虑不阻塞方式执行时间事件,如下

image.png
1553600398516226.png

假设刚才的判断中满足一种情况,如下

image.png
1553600510404971.png

如果flags被置位时间事件且是阻塞方式,那么就得先找出离现在最近将要被触发的一个时间时间,只要时间事件链表不为空,那么必定shortest必定不为空,那么接下来会获取此刻的时间,与shortest时间事件的触发时间点做差,得到一个后面需要阻塞等待的时间。在这个判断的else中,说明当前必定不是时间事件的处理(因为shortest为NULL可以说明,可以自己琢磨下),此时如果flags被置非阻塞标志位,那么把阻塞时间置为0,否则直接把tvp赋为NULL设定为阻塞模式。对,熟悉epoll的就能想到这必定是给epoll_wait中的时间参数用的。

想一个问题,这个shortest对应的被阻塞的时间事件什么时候执行呢?当然是等下一以非阻塞方式处理时间事件进来的时候才能,因为只要是阻塞方式处理时间事件那么都没有机会去调用ProcessTimeEvents。

image.png
1553601164447769.png

接下来调用对应的底层epoll被封装的api(或者其他),获取返回的就绪时间,fe中的fired数组在aeApiPoll中被修改了,记录了每个就绪的文件描述符及其对应的就绪时间。接下来便是进行相应时间的处理,初始有个值得注意的是rfired,假设读事件当前为就绪,那么rfired是0,那么只要写事件就绪,即使fe的写事件处理方法与读事件处理方法一样,写事件仍会被执行。相反,若读事件先被执行过,此时即使写事件可执行也要保证写事件处理方法与读事件处理方法必须不同。

此处为何用fe->mask而不把fe->mask取出来放到某个变量,注释里说了,这是为了避免某些读事件或写事件在执行过程中修改当前了fd在eventloop中的mask而后面却仍然执行,这显然是不符合预期的。


image.png
1553602624650589.png

wait某个fd被唤醒,不多说


image.png
1553604285903757.png

主循环,可被stop的设定停止,如下

image.png
1553604359955216.png


image.png
1553604421499260.png

获取底层实现api名,“epoll”、“select”、“kqueue”、“evport”


image.png
1553604499357284.png

设置BeforeSleep方法,给外部调用


完结

[1]: /wp-content/uploads/image/20190326/1553595541885473.png “1553595541885473.png”