參考資料: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