Redis event loop 相关源码阅读笔记,源码文件 ae.h
& ae.c
& ae_epoll.c
& ae_evport.c
& ae_kqueue.c
& ae_select.c
。
1. 小结
- Redis 会按序处理各个已触发的事件,无抢占
- 因为 Redis 在处理时间事件之前,会先处理文件事件,所以时间事件的处理时间通常比设定值晚一些
- 文件事件的
AE_BARRIER
和时间事件的refcount
这两种特性,在 Redis 目前的实现中尚未使用
Tips
- 循环遍历链表时,如果需要删除链表节点,可以使用标记删除。
2. 数据结构
2.1. 文件事件
Redis 的文件事件的数据结构如下:
|
|
-
mask
: 文件事件的类型,可为以下几种叠加AE_NONE
: 初始值,未注册读写事件AE_READABLE
: 读事件,监听 fd 是否可读AE_WRITABLE
: 写事件,监听 fd 是否可写AE_BARRIER
: 用于 fd 同时可读写时,控制读写处理的顺序- 同一个 fd 可同时读写时,一般我们会先处理读事件,这样利于我们在处理完一个查询操作后立马返回查询结果
- 当
AE_BARRIER
被设置时,我们会先处理写事件,再处理读事件
按照 issues/7098 所述,且查看代码后,发现目前 Redis 好像没用到
AE_BARRIER
特性
-
rfileProc
: 读事件处理函数 -
wfileProc
: 写事件处理函数 -
clientData
: 在rfileProc
/wfileProc
中可能会用到的数据
Fired file event 结构如下:
|
|
2.2. 时间事件
Redis 的时间事件的数据结构如下:
|
|
id
: 时间事件的 ID,单调递增,即新产生的时间事件 ID 大于已有时间事件的 IDwhen_sec
+when_ms
:时间点,记录时间事件触发的时间戳timeProc
:时间事件处理函数,当前时间不小于 <when_sec
,when_ms
> pair 时需调用timeProc
finalizerProc
: 时间事件终结处理函数,删除时间事件时调用clientData
: 在timeProc
/finalizerProc
中可能会用到的数据prev
/next
: 分别指向前/后一个时间事件,在 event loop 中时间事件为双向链表refcount
: 引用计数,防止递归调用processTimeEvents
时,时间事件被释放根据 pull/7253,Redis 现有实现无递归调用
processTimeEvents
的 case
2.3. Event loop
Redis 的 event loop 的数据结构如下:
|
|
-
maxfd
: 注册至 event loop 中的文件事件的最大 fd,其值不超过setsize
-
setsize
: event loop 中可容纳的文件事件数量上限 -
event
:event loop 中文件事件按照数组形式保存,其长度为setsize
- 未注册部分
aeFileEvent->mask
为AE_NONE
- 未注册部分
-
fired
: Fired file event 数组,长度同样为setsize
-
apidata
: used for polling API specific data,记录 polling API 的状态-
Redis 的 ae 包装了不同的 I/O 多路复用函数库,在编译时会按照选择系统中性能最佳的函数库,顺序为
ae_evport
->ae_epoll
->ae_kqueue.c
->ae_select.c
-
当使用 Linux 的
epoll
时,apidata
存储的内容为:1 2 3 4
typedef struct aeApiState { int epfd; struct epoll_event *events; } aeApiState;
-
-
timeEventHead
: event loop 中时间事件头节点 -
timeEventNextId
:已注册的时间事件的 ID 最大值- 每次调用
aeCreateTimeEvent
注册时间事件时加 1 - Use: Make sure we don’t process time events created by time events in this iteration
不过在目前的实现中这个 check 是多余的,因为新增的时间事件添加在时间事件链表头部
- 每次调用
-
lastTime
:记录 event loop 上次调用processTimeEvents
的时间戳- 当发生时钟跳变时,为了不推迟时间事件的触发执行,将所有时间事件的
when_sec
设置为 0,即立马处理该事件
因为在 Redis 中其实只会注册一个时间事件
serverCron
,所以问题不大 - 当发生时钟跳变时,为了不推迟时间事件的触发执行,将所有时间事件的
-
stop
: 值不为 0 时,停止轮询处理事件 -
beforesleep
: 调用epoll_wait
前可能会调用的函数 -
aftersleep
: 调用epoll_wait
后可能会调用的函数 -
flags
: 值可以设置为AE_DONT_WAIT
,表示调用epoll_wait
时 timeout 为 0 ,即不等待
3. API
3.1. 文件事件
|
|
当然,在添加/删除文件事件时,不仅会修改文件事件数组 aeEventLoop->events
,而且也会修改 aeEventLoop->apidata
,即 polling api state.
3.2. 时间事件
|
|
- 在添加时间事件时,
milliseconds
指的是时间间隔 - 在删除时间事件时,并不会真的从时间事件链表中删除该节点,而是将其
id
标记为AE_DELETED_EVENT_ID
- 真正的删除操作在
processTimeEvents
中实现
- 真正的删除操作在
3.3. Event loop
|
|
- 首先依据
aeEventLoop->lastTime
值判断是否发生了时钟跳变,若为真,则将所有时间事件设置为立马触发执行 - 从前往后遍历时间事件链表,执行已触发的时间事件
- 首先判断时间事件是否已被标记为删除,若为真,则从链表中删除该时间事件并释放内存空间
- 在执行完时间事件处理函数后,依据返回值决定是否将该时间事件标记为删除
- 返回值为
AE_NOMORE
(-1),表示该时间事件是个定时事件,执行一次后可删除,故标记为AE_DELETED_EVENT_ID
- 返回值为其他值时,表示该时间事件为定期事件,返回值为执行时间间隔,故利用返回值更新
when_sec
&when_ms
,即下次触发事件
- 返回值为
- 返回处理的时间事件数量
Tips:循环遍历链表时,如果需要删除链表节点,可以使用标记删除。
|
|
形参 flags
的取值可为以下几种叠加:
Flags | meaning |
---|---|
$0$ | 啥都不干,立马返回 |
AE_ALL_EVENTS |
执行已触发的文件事件和时间事件 |
AE_FILE_EVENTS |
执行已触发的文件事件 |
AE_TIME_EVENTS |
执行已触发的时间事件 |
AE_DONT_WAIT |
调用 epoll_wait 时,timeout 设置为 $0$,不等待 |
AE_CALL_BEFORE_SLEEP |
调用 epoll_wait 前调用 beforesleep 函数 |
AE_CALL_AFTER_SLEEP |
调用 epoll_wait 返回后调用 aftersleep 函数 |
函数执行流程大致如下:
- 根据
flags
的设置决定调用epoll_wait
的 timeout 值- timeout 为 $0$:最近将触发的时间事件时间戳不大于当前时间戳或
AE_DONT_WAIT
被设置 - timeout 大于 $0$:最近将触发的时间事件时间戳大于当前时间戳且未设置
AE_DONT_WAIT
,则 timeout 为两者差值 - timeout 为 $-1$:不处理时间事件或时间事件链表为空且未设置
AE_DONT_WAIT
,则 timeout 为 $-1$,即一直等待
- timeout 为 $0$:最近将触发的时间事件时间戳不大于当前时间戳或
- 调用
aeEventLoop->beforesleep
- 调用
epoll_wait
检查已触发的文件事件 - 调用
aeEventLoop->aftersleep
- 处理已触发的文件事件
- 同一个 fd 可同时读写时,一般先处理读事件,当
AE_BARRIER
被设置时,会先处理写事件,再处理读事件
- 同一个 fd 可同时读写时,一般先处理读事件,当
- 调用
processTimeEvents
处理时间事件 - 返回处理的文件事件和时间事件数量之和
|
|
事件循环主函数,会尝试同时处理文件事件和时间事件,且在调用 epoll_wait
前后会分别调用已设置的 aeEventLoop->beforesleep
和 aeEventLoop->aftersleep
。
|
|