Redis实现了自己的事件库,代码在ae.c
中。想要理解Redis事件库的工作原理,最好的方法就是去理解Redis如何使用它。
事件循环初始化
redis.c
中的initServer
函数初始化了redisServer
结构体变量的众多成员,其中一个就是Redis事件循环(event loop)el
:
initServer
调用aeCreateEventLoop
(定义在ae.c
)初始化server.el
的成员。aeEventLoop
的定义如下:
01 |
typedef struct aeEventLoop
|
04 |
long long timeEventNextId;
|
05 |
aeFileEvent events[AE_SETSIZE];
|
06 |
aeFiredEvent fired[AE_SETSIZE];
|
07 |
aeTimeEvent *timeEventHead;
|
10 |
aeBeforeSleepProc *beforesleep;
|
aeCreateEventLoop
aeCreateEventLoop
首先为aeEventLoop
结构体分配内存,然后调用ae_epoll.c:aeApiCreate
。
aeApiCreate
分配aeApiState
的空间,它有两个成员:epfd
保存epoll_create
调用返回的epoll
文件描述符,events
是Linux epoll
库中定义的epoll_event
结构类型。后面会再介绍events
的使用。
接下来是ae.c:aeCreateTimeEvent
,但是在那之前,initServer
会先调用anet.c:anetTcpServer
创建一个监听描述符(listening descriptor),默认监听6379端口。返回的监听描述符保存在server.fd
。
aeCreateTimeEvent
aeCreateTimeEvent
接收如下参数:
-
eventLoop
:即redis.c
中的 server.el
。
- milliseconds:从当前时间开始距离定时器过期的毫秒数。
-
proc
:函数指针,保存了定时器过期后调用的函数地址。
-
clientData
: 通常是NULL
。
-
finalizerProc
:指向定时事件被移除前要调用的函数。
initServer
调用aeCreateTimeEvent
为server.el
中的timeEventHead
成员添加一个定时事件,timeEventHead
是指向定时事件链表的指针。如下是 redis.c:initServer
函数中调用aeCreateTimeEvent
的代码。
1 |
aeCreateTimeEvent(server.el , 1 , serverCron , NULL , NULL );
|
redis.c:serverCron
执行很多后台操作来保持Redis正常运转。
aeCreateFileEvent
aeCreateFileEvent
函数实质就是执行epoll_ctl
系统调用,以将anetTcpServer
创建的监听描述符增加到EPOLLIN
事件队列,并将它和aeCreateEventLoop
创建的epoll
描述符相关联。
下面解释了 redis.c:initServer
中调用aeCreateFileEvent
具体做的工作。
initServer
传递了如下参数给aeCreateFileEvent
:
-
server.el
:aeCreateEventLoop
创建的事件循环,epoll
描述符是从server.el
里获取的。
-
server.fd
:监听描述符,作为从eventLoop->events
中获取相关文件事件结构体的索引,结构体中存储了回调函数等信息。
-
AE_READABLE
:表示必须监视server.fd
的EPOLLIN
事件。
-
acceptHandler
:当被监听的事件就绪时执行的函数,函数指针存储在eventLoop->events[server.fd]->rfileProc
。
以上完成了Redis事件循环的初始化。
事件循环的处理
redis.c:main
通过调用ae.c:aeMain
来处理前一阶段初始化好的事件循环。
ae.c:aeMain
在一个while循环中调用ae.c:aeProcessEvents
来处理就绪的定时事件和文件事件。
aeProcessEvents
ae.c:aeProcessEvents
在事件循环上调用ae.c:aeSearchNearestTimer
寻找最先要过期的定时事件。我们的示例中,事件循环里只有ae.c:aeCreateTimeEvent
创建的一个定时事件(译者注:即前面调用ae.c:aeCreateTimeEvent
使用的回调redis.c:serverCron
)。
请记住,aeCreateTimeEvent
创建的定时事件很可能已经过期了,因为过期时间只有1毫秒。定时器过期后,timeval
结构体的tvp
变量会把成员变量秒和毫秒都重置为0。
tvp
结构体变量和事件循环变量作为参数传给了ae_epoll.c:aeApiPoll
。
aeApiPoll
函数在epoll
描述符上调用了epoll_wait
,然后用下面内容填充 eventLoop->fired
数组。
-
fd
:准备好做读/写操作的描述符,操作类型取决于mask值。
-
mask
:标识读/写事件可以在对应描述符上执行。(译者注:有读事件mask |= AE_READABLE
,有写事件mask |= AE_WRITABLE
)
aeApiPoll
返回已就绪事件的个数。来看个实际的例子,假设有客户端发起了连接请求,那么aeApiPoll
将会注意到,使用监听描述符填充eventLoop->fired
数组的描述符成员,把mask设为AE_READABLE
。
现在,aeProcessEvents
调用redis.c:acceptHandler
回调函数。acceptHandler
在监听描述符上执行accept,返回一个客户端连接描述符。redis.c:createClient
通过下面这样调用ae.c:aeCreateFileEvent
,向连接描述符添加一个文件事件。
1 |
if (aeCreateFileEvent(server.el, c->fd, AE_READABLE,
|
2 |
readQueryFromClient, c) == AE_ERR) {
|
c
是redisClient
结构体变量,c->fd
是连接描述符。
然后,ae.c:aeProcessEvent
调用ae.c:processTimeEvents
。
processTimeEvents
ae.processTimeEvents
从 eventLoop->timeEventHead
开始,依次遍历链表上的定时事件。
对每个过期的定时事件,processTimeEvents
调用相应的回调函数。这个示例只会调用唯一注册的定时事件回调函数redis.c:serverCron
,回调函数返回的时间表示多少毫秒后它将被再次调用。返回的时间被ae.c:aeAddMilliSeconds
记录下来,ae.c:aeMain
中的while循环会在下次迭代中继续处理定时事件。
就这些了。