文前导读
skynet 是一个由云风所写的轻量级在线游戏服务器框架。本文为 skynet 框架源码剖析系列的第四篇文章,探讨了 skynet 的定时器机制,主要包含了以下内容:
- 定时器的基本数据结构
- starttime、current 以及 current_point 的意义,包括了 CLOCK_REALTIME 以及 CLOCK_MONOTONIC 等内容
- time、near 数组以及 t 数组的意义
- time、near 数组以及 t 数组三者之间的联系
基本数据结构
要了解 skynet 的定时器机制,需要先了解 skynet 中的 timer
的数据结构及初始化代码(skynet 中所有 timer 相关的代码都存放于 skynet_timer.c 文件中):
1 |
|
从上述数据结构的定义中可以知道,skynet 采用 timer_event
来表示超时事件,其中 handle
代表了该超时事件属于哪个服务,而 session
则代表向对应服务所发送的超时消息的 session。skynet 采用了带头节点的单链表来存储多个定时器。
starttime、current 以及 current_point 的意义
要想了解 上述三个字段的具体意义,我们需要先了解 timer 是如何被初始化,以及节点是如何添加到 timer 当中的。在说明 skynet_timer_init
之前,需要花点时间说明 clock_gettime
中不同的时间类别,也就是所谓的 clock_id. clock_gettime
支持多种不同的 clk_id
, 其中包括但不限于:CLOCK_REALTIME
、CLOCK_MONOTONIC
、CLOCK_PROCESS_CPUTIMEID
和 CLOCK_THREAD_CPUTIME_ID
- CLOCK_REALTIME:墙上时间(wall time),也就是我们现实生活中所用的时间,由变量xtime来记录。系统每次启动时将CMOS上的RTC时间读入xtime,这个值是”自1970-01-01起经历的秒数、本秒中经历的纳秒数”,每来一个timer interrupt,也需要去更新xtime。其值为从 1970-01-01:00:00:00 至今所流逝的时间。
- CLOCK_MONOTONIC:单调时间(monotonic time),代表的从系统启动至今所流逝的时间,由变量jiffies来记录。系统每次启动时jiffies初始化为0,每来一个timer interrupt,jiffies加1,也就是说它代表系统启动后流逝的tick数。jiffies一定是单调递增的
- CLOCK_PROCESS_CPUTIMEID:进程专属的 CPU 时钟,代表从进程启动后至今所流逝的时间
- CLOCK_THREAD_CPUTIME_ID:线程专属单 CPU 时钟,代表从线程启动后至今所流逝的时间
其中,CLOCK_REALTIME
和 CLOCK_MONOTONIC
的区别在于 CLOCK_REALTIME
的值可以受到系统时间跳变或 NTP 的影响, 而CLOCK_MONOTONIC
不会受到影响,因此常用 CLOCK_MONOTONIC
来计算系统启动后两个先后发生的事件之间的时间差
1 | //创建一个 timer 结构,并将其中 near 以及 t 链表数组清空 |
从上述代码中,我们可以知道 starttime
代表的是 timer 初始化的墙上时间,精确到秒,而 current
则相当于 timer 启动后至今的时间差(也就是 timer 的运行时间),精度为 10 ms,而 current_point
则相当于从系统开机至今经过的时间,精度同样为 10ms
time、near 数组以及 t 数组的意义
timer 一旦完成初始化后,就会交给 timer 线程去使用,为了了解上述三个字段的含义以及定时器背后的流程,我们需要先阅读 timer 线程的线程函数
1 | //skynet_timer.c |
从上述代码可以看出,timer 的调用主要通过 skynet_updatetime
函数来实现(skynet_socket_updatetime
函数的部分会放到网络当中讲)。继续追踪相应的函数:
1 | static void add_node(struct timer *T,struct timer_node *node) { |
在上述代码中,可以看到每调用一次 timer_shift
, time
就会自增 1,而 skynet_updatetime
中一共执行了 diff 次 timer_shift
。因此 time
代表了**从 timer 启动后至今一共经历了多少次 tick(一次 tick 的长度为 10ms)**。而且从 timer_shift
函数我们可以看出time
和near
数组以及t
数组关系:
如上图所示,skynet 按照超时时间的紧迫程度为 timer 划分出 5 个槽,其中紧急程度为 near > level0 > level1 > level2 > level3。其中,near
中的定时器节点超时时间相差最大不超过 2^8 = 256 次 tick,而对于同一个 level 而言,t[level] 中的定时器超时时间间隔不超过 2^6 = 64 次 tick。 time
中不同的位域代表了不同的紧急程度。timer_execute
每次只对 near
中的定时器执行超时操作。
了解了上述内容,我们就能够明白 skynet 是怎么样运转定时器的:skynet 的 timer 线程会不断触发 skynet_update
函数,在该函数中会不断执行 timer_execute
对 near
中的定时器执行超时操作。执行完毕后,调用timer_shift
从 t[0]
~t[3]
中选择合适的定时器节点加入到 near
中,这一过程就相当于提高了定时器节点的紧急程度(因为随着时间的流逝,定时器节点的紧急程度会越来越向 near 逼近)。
讲完了 skynet 定时器的运转流程,最后来看看为什么在函数 move_list
中,当 time
发生回绕时,为什么直接将 t[3]
放到 near
当中?这主要是因为添加节点采用的是位运算的方式,因此当发生 time 发生回绕时,低位会全部变为0,因此 t[0]
~ t[3]
都会被接连移动到 near
当中, 所以出于效率的考虑,直接将 t[3]
移入 near
即可
- 本文作者: Phoenix
- 本文链接: http://hacker-cube.com/2020/11/04/skynet-源码阅读笔记-——-skynet中的定时器机制/
- 版权声明: 本博客所有文章除特别声明外,均采用 MIT 许可协议。转载请注明出处!