软件定时器在协议栈等很多场景都有广泛的应用,有时候会有大量的定时器同时处于工作状态,它们的超时时间各异,要高效的保证每个定时器都能够较为准确的超时并执行到其回调函数并不是一件易事。本文分析嵌入式实时操作系统Nucleus的定时器方案,它巧妙的管理了一条按照相对时间来排序的双向链表,避免每次tick中断都要遍历链表检查超时和更新剩余时间,实现了一种相当高效的软件定时器。
结构体TM_TCB来表示动态创建的定时器,其定义如下
typedef struct TM_TCB_STRUCT
{
/*Nucleus的定时器分为应用定时器和task内置两种type */
INT tm_timer_type;
/* 剩余时间,可动态改变 */
UNSIGNED tm_remaining_time;
/*tm_information指向TM_APP_TCB,其保存了超时函数等定时器参数 */
VOID *tm_information;
struct TM_TCB_STRUCT
*tm_next_timer, /* Next timer in list */
*tm_previous_timer; /* Previous timer in list*/
} TM_TCB;
每个字段都有注释,和本文相关的字段已经标红。
定时器结构体TM_TCB之间使用双向循环链表的方式组织在一起,即tm_next_timer和tm_previous_timer指针。Nucleus里有个TM_TCB *TMD_Active_Timers_List全局指针,其指向该链表的头部,管理这所有处于激活状态的定时器。
假设当前系统里没有定时器,我们的应用在初始化时要连续创建和激活5个周期性的定时器,其周期超时时间为:5,8,8,12,20(单位都是tick).
假设是按照顺序创建,那么链表的形成过程如下所示(方框里的数字就是TM_TCB的tm_remaining_time):
//第1个定时器会在5个tick之后超时
// 第2个定时器会在8个tick之后超时,相对于前一个,它的超时时间多3
// 第3个定时器会在8个tick之后超时,相对于前一个,它的超时时间多0
// 第4个定时器会在12个tick之后超时,相对于前一个,它的超时时间多4
// 第5个定时器会在20个tick之后超时,相对于前一个,它的超时时间多8
看起来很容易。
现在打乱其创建的顺序,例如12,8,20,5,8,那么链表的形成过程如下所示(蓝色表示新插入的节点,棕色表示受影响的节点):
//第1个定时器会在12个tick之后超时
// 第1个定时器会在8个tick之后超时,第2个相对于前一个,它的超时时间多4
// 第3个定时器会在20个tick之后超时,相对于前一个,它的超时时间多8
//链表重新排序,并更新当前插入的定时器的下一个元素的相对时间
//链表重新排序,并更新当前插入的定时器的下一个元素的相对时间
总之,无论创建的顺序如何,结果链表组织方式和顺序都是一样的
有了这个排序的记录有相对时间的双向链表后,下一个问题就是时间的递减怎么记录?
需要引入一个重要的全局变量
UNSIGNED TMD_Timer
它记录了当前距离超时最近的定时器的剩余时间。例如在上面的例子里,当前TMD_Timer值为5。Nucleus的底层实现了一个中断处理函数INT_Timer_Interrupt,它是由硬件定时器触发的,当有处于激活状态的timer时,每个tick触发一次,在这个函数里,会对TMD_Timer减1并判断是否为0,如果为0,说明有定时器超时,开始激活TMD_HISR,其回调函数就是用于处理所有定时器超时的入口。
那么,有定时器超时了以后会发生什么?
假设现在已经过了5个tick(这个5个tick里,不需要对定时器链表有任何的操作),那么首节点的超时函数会被执行,首节点被从链表头部摘除,TMD_Timer会被改写为下一个首节点的剩余时间重新计数,即3.且链表会更新成(蓝色表示新插入的节点,棕色表示受影响的节点):
原首节点定时器是周期性的,所以需要重新寻找合适的位置插入链表,由于它的下次超时时间是5个tick后.所以应该放在3之后,且相对超时时间为2。同时受到影响的还有它的下一个节点,原来的相对剩余时间是4,由于加入了一个新的节点,相对的剩余时间变成了2。
假设又继续过了3个tick,现在的第一、二个节点会超时(在一次激活HISR的函数里执行两个定时器的超时函数),TMD_Timer会被改写为下一个首节点的剩余时间重新计数,即2,链表会被更新为
再过2个tick,下个节点超时,以此类推。
还有一个问题,前面创建几个定时器的时候都是同时创建的,如果不是同时创建会怎么样?
如上面的分析,回到整个用例开始的时候,在5个定时器组成的链表建立好之后,在接下来的5个tick内,没有定时器会超时,同时链表里的数据也不会有任何更新。
假设在3个tick之后,需要加入一个新的定时器,超时时间为10,应该怎么办?
如果继续按照上面组织链表的办法,10 = 5 + 3 + 2,应该放在第4位,且相对前一个定时器的超时时间加2,如下所示:
但是请注意,当前已经距离开始时间已经过去了3个tick,第一个定时器会在2个tick后超时,第二和第三个会在5秒后超时,所以如果按照这种方式,新加入的这个定时器在7个tick后就超时了,这显然是不正确的。
这个地方还是需要TMD_Timer,它记录了当前距离超时最近的定时器的剩余时间,也就是等于当前正确的链表首节点的剩余时间(开始是5,过了3个tick之后,变成了2)
正确的做法是先更新头节点的剩余时间之后,再插入新的节点,更新完之后的链表如下所示
这样,链表里的每个定时器节点都会有正确的超时时间了
有了前面的分析,停止定时器时的链表操作就很简单了,紧接着上面的例子,假设1个tick后,应用程序停止了第5个timer(图中的剩余时间为1),链表更新如下:
停止定时器最重要的工作就是将其从TMD_Active_Timers_List链表上摘下,摘节点时不需要更新头节点的剩余时间,但需要更新要摘除的节点的下一个节点的剩余时间8 = 7 + 1
最后还有两个问题:
1 这个定时器的设计把耗时的操作都放到了start 和stop timer的时刻。当timer个数很多时,这个新timer节点的插入以及整个链表的会不会比较费时?
答:1 前面的这些讨论都是关于在激活状态的定时器,实际使用中,在同一时间里,会有相当部分的定时器是处于非激活状态,它们不需要用链表组织起来(应用程序拥有其指针可对其进行操作)。所以激活定时器组成的链表长度不会太长。
2将start timer的操作分解开来,就可以看出它只是干了几件事(stop也是类似的):
a)更新头节点的剩余超时时间为TMD_Time。固定时间,几条指令即可完成
b)遍历链表,找到合适的插入位置。这是一个相对耗时的操作,时间复杂度是O(n)
c)更新这个插入位置的下一个节点的剩余时间,也就是图里面的棕色节点, 固定时间,几条指令即可完成
2 这个定时器的设计还能再优化吗?
答:软件层面上,这是楼主见过的最优秀的定时器设计方案,硬件上还有一点优化空间。前面对于硬件定时计数的描述是这样的
每个tick触发一次,在这个函数里,会对TMD_Timer减1并判断是否为0,如果为0,说明有定时器超时,开始激活TMD_HISR
这是一个反复比较的过程,每个tick的中断里,会做不多的数字累加和比较操作,这个定时器中断的操作可以省下来。每次链表头节点更新的时候,可以启动一个硬件定时器,将其超时时间设置为TMD_Timer,时间一到,就代表着头节点已超时,而不用反复的在每个tick都做着重复的工作