参考资料: RTT官网文档
关键字:分析RT-Thread源码、stm32、RTOS、定时器timer。
问题及总结
一、为什么定时器定时不支持超过RT_TICK_MAX / 2(RT_ASSERT(timer->init_tick < RT_TICK_MAX / 2);)?
这个问题其实就跟我们现实中的时钟一样。这个问题分为过了12点(归0)和没过12点。
1)没过12点:
已知 a + b + c = max;因为定时不超过RT_TICK_MAX / 2,即b < RT_TICK_MAX / 2。
所以a + c 就会大于RT_TICK_MAX / 2,即cur_tick - timeout_tick 在cur_tick小于timeout时,cur_tick - timeout_tick = a + c > RT_TICK_MAX / 2。
2)超过12点:
因为定时init_tick不超过RT_TICK_MAX / 2,所以a + c < RT_TICK_MAX / 2,
所以b > RT_TICK_MAX / 2,即cur_tick - timeout_tick = b > RT_TICK_MAX / 2。
所以我们才可以用(t->timeout_tick - timer->timeout_tick) < RT_TICK_MAX / 2来判断是否到达超时时间。
二、定时器跳表总结
加入插入一个400tick的定时器时start的流程。这里就没有话引索的建立了(额,灵魂画手。。)
1:start的第一个for循环,用来遍历各级引索
2:第二个for循环,遍历该级引索链表
345:这行代码的意思:row_head[row_lvl + 1] = row_head[row_lvl] + 1;
总结:这种方法实现跳表比较简洁易懂,不过多少级引索每个定时器就要包含多少个节点,比较浪费空间。目前还不清楚这个建立引索的算法是否高效。
跳表
背景介绍(可以忽略)
有了时钟节拍之后,我们就可以利用它来完成一些和时间相关功能,如定时器。我们只要记录下调用定时时的时钟节拍tick_start,和需要定时多久的时钟节拍tick_count,然后我们在在时钟节拍的处理里面比较当前tick_current, 只要检测到tick_cuurent >= tick_start +tick_count,就可以调用超时处理函数了。操作系统中会广泛的使用定时器,所以我们不可能只维护一个,这个时候就需要用链表把它们串起来,只要我们按超时时间顺序链起来,从头遍历比较就可以实现管理多个定时任务了。
好了,既然链表得顺序排序,那我们插入链表的时候就得顺序插入。即便对于已经按顺序排序的链表,我们也得从头开始一个个遍历,如果链表比较长,而我们要插入的节点在比较靠后的位置上,那么我们就要花费很大时间才能插入这个节点了,有什么办法可以解决这个问题吗?
二分法?可惜链表不能跟数组一样可以随机访问。哈希/HASH?消耗较大。看过RTT文档你应该知道是跳表了。跳表。也许你可能没听说过,大部分数据结构和算法的书籍好像也都不怎么讲关于跳表。但它是个好东西。
跳表
跳表在原有的有序链表上面增加了多级索引,通过索引来实现快速查找。首先在最高级索引上查找最后一个小于当前查找元素的位置,然后再跳到次高级索引继续查找,直到跳到最底层为止,这时候以及十分接近要查找的元素的位置了(如果查找元素存在的话)。由于根据索引可以一次跳过多个元素,所以跳查找的查找速度也就变快了。
例如在原有链表上,我们要找39,需要从3一个个遍历到39,需要3->18->39就可以找到39了,
跳表就是利用引索来快速遍历的,时间复杂度为O(log n),相当于二分查找。可以看出跳表是用空间来换时间的。
跳表的难点在于其是动态的,需要动态的建立多级引索,每次插入一个节点都要考虑是否需要重新建立引索。如何建立引索又是一个难题,建立不好,不仅浪费空间,也会降低搜索的时间,极端情况还可能退化到单链表,所以需要考虑如何才能适当的建立引索,才有高效的搜索。
这里推荐一下王争老师的数据结构与算法之美,讲的不错,也不太枯燥。
跳表一节可以试读https://time.geekbang.org/column/article/42896
源码分析
先来看一下定时器的结构体成员组成。
/**
* timer structure
*/
struct rt_timer
{
struct rt_object parent; /**< inherit from rt_object */
rt_list_t row[RT_TIMER_SKIP_LIST_LEVEL];
void (*timeout_func)(void *parameter); /**< timeout function */
void *parameter; /**< timeout function's parameter */
rt_tick_t init_tick; /**< timer timeout tick */
rt_tick_t timeout_tick; /**< timeout tick */
};
成员: struct rt_object parent
parent是用来“继承”基本的内核对象的,加入对象容器队列中,形成一条定时器链表。
parent.flag这里是用来记录这个定时器的状态。flag的状态有以下几种:
RT_TIMER_FLAG_DEACTIVATED(无效的),RT_TIMER_FLAG_ACTIVATED(激活的),
RT_TIMER_FLAG_ONE_SHOT(单次的),RT_TIMER_FLAG_PERIODIC(周期性的),
RT_TIMER_FLAG_HARD_TIMER(硬定时), RT_TIMER_FLAG_SOFT_TIMER(软定时)
成员: rt_list_t row[RT_TIMER_SKIP_LIST_LEVEL];
这是一个链表数组,用来记录跳表的引索,下标是引索的级数。默认是1。
成员: void (*timeout_func)(void *parameter);
超时回调函数,当定时时间到了的时候,将会调用这个超时函数。
成员:void *parameter;
超时函数的参数,类型时void *。
成员:init_tick
是用来记录超时间隔的,在启动定时器后的init_tick时间后,将触发超时。
成员:timeou_tick
用来记录超时时的tick。
了解了定时器的结构体后,现在我们来看一下定时器相关的API。以介绍硬定时器为例。
void rt_system_timer_init(void)
{
int i;
for (i = 0; i < sizeof(rt_timer_list) / sizeof(rt_timer_list[0]); i++)
{
rt_list_init(rt_timer_list + i);
}
}
在rt_system_timer_init中初始化了rt_timer_list,默认只有一个。
接着看一下初始化定时器:
void rt_timer_init(rt_timer_t timer,
const char *name,
void (*timeout)(void *parameter),
void *parameter,
rt_tick_t time,
rt_uint8_t flag)
{
/* timer check */
RT_ASSERT(timer != RT_NULL);
/* timer object initialization */
rt_object_init((rt_object_t)timer, RT_Object_Class_Timer, name);
_rt_timer_init(timer, timeout, parameter, time, flag);
}
第2行代码rt_object_init将timer初始化为内核的定时器对象,并插入定时器对象链表中。
第3行,真正初始化定时器的地方。
static void _rt_timer_init(rt_timer_t timer,
void (*timeout)(void *parameter),
void *parameter,
rt_tick_t time,
rt_uint8_t flag)
{
int i;
/* set flag */
timer->parent.flag = flag; //设置标识位,单次或周期性,硬定时或软定时
/* set deactivated */
timer->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED; //初始化时先设置为无效定时器。
timer->timeout_func = timeout; //设置超时函数
timer->parameter = parameter; //设置超时函数参数
timer->timeout_tick = 0; //超时tick初始值为0
timer->init_tick = time; //超时间隔
/* initialize timer list */
for (i = 0; i < RT_TIMER_SKIP_LIST_LEVEL; i++)
{
rt_list_init(&(timer->row[i])); //初始化引索链表,i为引索级别
}
}
初始化定时器后,就可以启动启动器了。
下面就是重点了,在启动中插入链表和建立引索。
这个start函数有点长,这里以硬定时为例。
rt_err_t rt_timer_start(rt_timer_t timer)
{
unsigned int row_lvl;
rt_list_t *timer_list;
register rt_base_t level;
rt_list_t *row_head[RT_TIMER_SKIP_LIST_LEVEL];
unsigned int tst_nr;
static unsigned int random_nr;
/* remove timer from list */
_rt_timer_remove(timer); //防止重复插入链表
timer->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED; //将定时器标识为无效
/*
* get timeout tick,
* the max timeout tick shall not great than RT_TICK_MAX/2
*/
RT_ASSERT(timer->init_tick < RT_TICK_MAX / 2); //init_tick必须小于RT_TICK_MAX / 2。
timer->timeout_tick = rt_tick_get() + timer->init_tick; //计算超时时间
{
/* insert timer to system timer list */
timer_list = rt_timer_list; //将timer_list指向rt_timer_list
}
row_head[0] = &timer_list[0]; //将row_head[0]指向rt_timer_list
//遍历各级跳表,这里的level默认为1,这个for就相当于没有。
for (row_lvl = 0; row_lvl < RT_TIMER_SKIP_LIST_LEVEL; row_lvl++)
{
//遍历当前级别(row_lvl)跳表,第一次没有定时器,不会进入。
//注意这里for的语句1为空,row_head是从上一级跳下来的,而不从第一个节点开始遍历
//如果不是空或者最后一个的则遍历,即当找到不到比他大的时候已指向最后一个
for (; row_head[row_lvl] != timer_list[row_lvl].prev;
row_head[row_lvl] = row_head[row_lvl]->next)
{
struct rt_timer *t;
rt_list_t *p = row_head[row_lvl]->next; //这里指向的是下一个定时器的row而不是当前的。
/* fix up the entry pointer */
//通过成员地址获取结构体地址,rt_list_entry比较简单,就不介绍了
t = rt_list_entry(p, struct rt_timer, row[row_lvl]); //t指向了p的定时器地址
/* If we have two timers that timeout at the same time, it's
* preferred that the timer inserted early get called early.
* So insert the new timer to the end the the some-timeout timer
* list.
*/
if ((t->timeout_tick - timer->timeout_tick) == 0) //如果超时时间一样。下一轮则插入其后面
{
continue;
}
//如果要插入的超时时间小于遍历的这个定时器的超时时间,即找到位置,则break。
else if ((t->timeout_tick - timer->timeout_tick) < RT_TICK_MAX / 2)
{
break;
}
}
//把row_head[row_lvl + 1]指向了上一级找到的位置的下一个row
//如果不是后面一级,则进入下一级引索继续遍历,这里的+1等于sizeof(row_head[0]),即将row_head[row_lvl +1]指向了该定时器的row[row_lvl+1]。
if (row_lvl != RT_TIMER_SKIP_LIST_LEVEL - 1)
row_head[row_lvl + 1] = row_head[row_lvl] + 1;
}
/* Interestingly, this super simple timer insert counter works very very
* well on distributing the list height uniformly. By means of "very very
* well", I mean it beats the randomness of timer->timeout_tick very easily
* (actually, the timeout_tick is not random and easy to be attacked). */
random_nr++; //这里将插入的定时器次数作为随机值,为了让跳表引索分布均匀
tst_nr = random_nr;
//将定时器插入到最后一级的跳表链表中,即RT_TIMER_SKIP_LIST_LEVEL - 1级,所有的都会插入最后一级链表中
rt_list_insert_after(row_head[RT_TIMER_SKIP_LIST_LEVEL - 1],
&(timer->row[RT_TIMER_SKIP_LIST_LEVEL - 1]));
//建立跳表引索,RT_TIMER_SKIP_LIST_LEVEL应该要大于2的
for (row_lvl = 2; row_lvl <= RT_TIMER_SKIP_LIST_LEVEL; row_lvl++)
{
//判断低两位是否为0,是则插入当前引索链表中,越上层的引索应该插入的越少。
if (!(tst_nr & RT_TIMER_SKIP_LIST_MASK))
rt_list_insert_after(row_head[RT_TIMER_SKIP_LIST_LEVEL - row_lvl],
&(timer->row[RT_TIMER_SKIP_LIST_LEVEL - row_lvl]));
else
break;
/* Shift over the bits we have tested. Works well with 1 bit and 2
* bits. */
//将tst_nr左移2位
tst_nr >>= (RT_TIMER_SKIP_LIST_MASK + 1) >> 1;
}
//置为活跃状态
timer->parent.flag |= RT_TIMER_FLAG_ACTIVATED;
return -RT_EOK;
}
接着看一下stop函数。
rt_err_t rt_timer_stop(rt_timer_t timer)
{
register rt_base_t level;
/* timer check */
RT_ASSERT(timer != RT_NULL);
//如果不是活跃状态
if (!(timer->parent.flag & RT_TIMER_FLAG_ACTIVATED))
return -RT_ERROR;
RT_OBJECT_HOOK_CALL(rt_object_put_hook, (&(timer->parent)));
/* disable interrupt */
level = rt_hw_interrupt_disable();
//从链表中移除
_rt_timer_remove(timer);
/* enable interrupt */
rt_hw_interrupt_enable(level);
//修改状态为无效
/* change stat */
timer->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;
return RT_EOK;
}
stop中将定时器从链表中移除,并将状态设置为无效状态。
现在看一下是怎么检测定时器是否到达定时时间的。rt_timer_check放在系统节拍的中断上,也就是每个系统节拍都会检查一下是否有定时器到达超时时间。
void rt_timer_check(void)
{
struct rt_timer *t;
rt_tick_t current_tick;
register rt_base_t level;
RT_DEBUG_LOG(RT_DEBUG_TIMER, ("timer check enter\n"));
//获取当前tick
current_tick = rt_tick_get();
/* disable interrupt */
level = rt_hw_interrupt_disable();
//判断定时器最低层(已排序好)的第一个定时器是否超时。
while (!rt_list_isempty(&rt_timer_list[RT_TIMER_SKIP_LIST_LEVEL - 1]))
{
t = rt_list_entry(rt_timer_list[RT_TIMER_SKIP_LIST_LEVEL - 1].next,
struct rt_timer, row[RT_TIMER_SKIP_LIST_LEVEL - 1]);
/*
* It supposes that the new tick shall less than the half duration of
* tick max.
*/
//如果到达超时时间
if ((current_tick - t->timeout_tick) < RT_TICK_MAX / 2)
{
RT_OBJECT_HOOK_CALL(rt_timer_timeout_hook, (t));
//从链表中移除该定时器
/* remove timer from timer list firstly */
_rt_timer_remove(t);
//调用超时函数,并把parameter当做参数传入
/* call timeout function */
t->timeout_func(t->parameter);
/* re-get tick */
current_tick = rt_tick_get();
RT_DEBUG_LOG(RT_DEBUG_TIMER, ("current tick: %d\n", current_tick));
//判断是否是周期性的以及是否还是活跃状态
if ((t->parent.flag & RT_TIMER_FLAG_PERIODIC) &&
(t->parent.flag & RT_TIMER_FLAG_ACTIVATED))
{
/* start it */
t->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;
rt_timer_start(t); //周期性的话就重新插入跳表中
}
else
{
/* stop timer */
t->parent.flag &= ~RT_TIMER_FLAG_ACTIVATED;
}
}
else
break;
}
/* enable interrupt */
rt_hw_interrupt_enable(level);
RT_DEBUG_LOG(RT_DEBUG_TIMER, ("timer check leave\n"));
}
rt_timer_check中找到超时的定时器,并调用超时函数。
有了定时器,现在实现线程的延时/睡眠就轻松多了。线程管理那一篇已经介绍过rt_thread_delay了,
现在不用看代码我们也能知道是怎么实现delay的了。首先delay的时候需要将线程挂起suspend,然后根据要delay的时间,start启动定时器,然后timeout超时函数里则是将线程唤醒resume就绪态。
关于rtt的定时器就分析到这里,总结及问题就放在最前面了。
测试
下面是RTT-MINI的定时器测试,这里没有用跳表,代码见链接。
发现了两个bug,thread_exit里面没有调用schedule,导致推出时跑死。schedule_ready_del_thread里面没有判断同优先级下是否还有其它线程。
测试代码链接:https://download.csdn.net/download/u012220052/11236208