簡化版任務調度器的實現和應用

背景

有別於“裸奔”的程序,類似於FreeRTOS或者Uc/OS II之類的實時系統都必備一個強大的任務調度器,基於此用戶可以實現各種“亂七八糟”或者“豐富多彩”的功能。而“裸奔”的用戶似乎與只能在main函數中,或者中斷函數中苦苦掙扎求生存。當項目小的時候,我相信程序員有能力能夠hold住。一旦項目變得複雜或者成熟後,有時候一點點需求的變動都會讓整個項目都變得傷痕累累-各種補丁和註釋。

是不是可以既得實時系統的優點-任務調度器,又可以兼備裸奔的“簡潔”呢?我們想要的無外非一個可以把我們所有的任務都管理的僅僅有條而且有條不紊的管家而已,那些妖豔的隊列,互斥信號量或者郵箱我們其實可以不用,畢竟有時候殺雞也用不了牛刀。對此類實時系統的源碼或者書籍做了一番解讀以後,那我們是不是可以模仿它的行爲來構建一個簡化版的任務調度器?
我們迫切想要實現一個功能羅列如下:

  1. 隨時可以註冊任務,即將一個任務加入到list裏面
  2. 可以刪除任務,即將一個任務從list中移除
  3. 任務可以delay,即暫時的掛起。被掛起的任務將會自動被調度器忽略,當delay時間到來後,自動取消掛起。
  4. 任務可以sleep,即被永久的掛起。被掛起的任務將會自動被調度器忽略,直到被手動解除休眠,即取消掛起。
  5. 把一個或者多個註冊過的任務依次執行。
  6. 每一個任務不在delay或者sleep的任務都可以從任務調度器哪裏得到cpu的控制權後,在完成任務後自動返還cpu控制權,即任務調度器重新得到cpu的控制權,以便其繼續運行list中的下一個任務。
  7. 需要一箇中斷作爲調度器的節拍器,也就是心臟。

所以我們這裏涉及的簡化版任務調度器不具備:

  1. 搶佔優先級,所有註冊後的任務都是一個優先級,排隊依次被調用
  2. 基於堆棧的task進入/退出方法,這個將會和後面介紹的狀態機結合在一起,基於狀態機實現一個複雜的任務。
  3. 沒有隊列,信號量,郵箱等數據同步/衝突解決方案,簡單一點,別整的這麼複雜。

這樣最大的好處在於:

  1. 任務調度器是自行開發的,不存在維護困難問題。
  2. 基於任務調度器的應用程序二次開發將會具備最大化的靈活性。
  3. 支持多個task“同時”運行,想想都要流口水。

下面開始吹牛,歡迎拍磚

正文

下面先聲明一個任務的狀態的各種可能,這裏用枚舉類型

typedef enum 
{
	Task_Ready 			= 2,
	Task_ReadyWithTimer	= 3,
	Task_Error 			= 4,
	Task_Delay 			= 5,
	Task_Sleep 			= 6
}TASK_StatusEnum;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8

然後再定義一個結構體,正如其名任務統計。這個變量將可以被所有註冊過的任務訪問。

/*
	the follow struct was shared by all task in the tlt
*/

typedef struct
{
uint32_t active_signal_num;
uint32_t active_task_num;
}TaskStatisticsStr;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9

接下來在定義一個結構體,統稱爲任務句柄。通過這個句柄,就可以訪問該任務的狀態等所有的相關的變量。

typedef struct BSPTaskHandleStr
{
/*
	high level status of the task, the task should be execute or not was depend on the high level task status
	only in Task_Ready or Task_ReadyWithTimer will make task_handle active. otherwise will execute the special
	function like enqueue or dequeue or just pass it to next tle.
	delay_us was only active when task_status == Task_Delay. it was differ to timer_us
*/
	TASK_StatusEnum	task_status;
	int32_t 		delay_us;
/*
	low level stask of the task, it was the detail about how the task run and transmit to other sub-status
	the timer_us was used for sub_status, in some situation there need a timeout alarm to push the sub-status
	back to idle or other.
*/
	uint32_t		subtask_status;
	int32_t 		timer_us;
/*
	define a para to delivery into task_process
*/
	void *			para;
	int32_t			(*task_process)(struct BSPTaskHandleStr*,void *);
TaskStatisticsStr <span class="token operator">*</span>tss<span class="token punctuation">;</span>

}BSPTaskHandleStr;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25

然後最關鍵的一步,定義一個結構體來表示鏈表元素tle,該結構體具備三個指針,前兩個用來形成鏈表,後面一個指針作爲具體的任務句柄的入口。

typedef struct TaskListElementStr
{
	struct TaskListElementStr * Next;
	struct TaskListElementStr * Prev;
BSPTaskHandleStr 			<span class="token operator">*</span>task_handle<span class="token punctuation">;</span>

}TaskListElementStr;

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

這裏小小的介紹一下鏈表:
鏈表用於實現將不連續(也可以連續分佈)分佈的數據串聯在一起,形成兩種(目前我僅知道兩種)鏈表:

  1. 環形鏈表
  2. 單向鏈表(都是隨便取得名字,呵呵)

下面只介紹環形鏈表,因爲我們這裏將會應用這個鏈表結構

20191201194649.png

當鏈表中僅包含一個元素的時候:

20191201195302.png

當鏈表中包含兩個或以上元素的時候:

可以看到,但一個鏈表中包含一個以上元素(這裏我們稱之爲tle:tast list element)的時候,每一個tle裏面的prev指向前一個tle,每一個next指向後一個tle,同時task handle也指向了各自的任務句柄實體。通過這個句柄就可以通過函數指針(task_process)(struct BSPTaskHandleStr,void *)訪問具體的任務函數實體。

下面通過圖片演示一下鏈表中元素入列和出列

首先是入列,如下圖所示將tle2放置在原來的tle1和tle3中,上圖是未插入之前的狀態,後者是插入後的狀態。
下圖中綠色加粗的虛線就是入列後,對整個鏈表的影響。
20191201200416.png
20191201200214.png

下面是入列實現的源碼,注意這裏每一次入列將會將任務統計中的有效任務數量+1,用來顯示當前鏈表中有幾個tle是有效的

// to insert the tle before the target. this function only work when there are at least one tle in the list
void task_enqueue_insert_before(TaskListElementStr * insert_tle, TaskListElementStr * target_tle)
{
	assert_param(__Check_Point_Is_Valid(target_tle));
	assert_param(__Check_Point_Is_Valid(insert_tle));
insert_tle<span class="token operator">-&gt;</span>Prev <span class="token operator">=</span> target_tle<span class="token operator">-&gt;</span>Prev<span class="token punctuation">;</span>
insert_tle<span class="token operator">-&gt;</span>Next <span class="token operator">=</span> target_tle<span class="token punctuation">;</span>

target_tle<span class="token operator">-&gt;</span>Prev<span class="token operator">-&gt;</span>Next <span class="token operator">=</span> insert_tle<span class="token punctuation">;</span>
target_tle<span class="token operator">-&gt;</span>Prev <span class="token operator">=</span> insert_tle<span class="token punctuation">;</span>
<span class="token keyword">if</span><span class="token punctuation">(</span>target_tle <span class="token operator">==</span> head_tle<span class="token punctuation">)</span>
	head_tle <span class="token operator">=</span> insert_tle<span class="token punctuation">;</span>
insert_tle<span class="token operator">-&gt;</span>task_handle<span class="token operator">-&gt;</span>tss<span class="token operator">-&gt;</span>active_task_num<span class="token operator">++</span><span class="token punctuation">;</span>

}

// to insert the tle after the target. this function only work when there are at least one tle in the list
void task_enqueue_insert_after(TaskListElementStr insert_tle, TaskListElementStr target_tle)
{
assert_param(__Check_Point_Is_Valid(target_tle));
assert_param(__Check_Point_Is_Valid(insert_tle));

insert_tle<span class="token operator">-&gt;</span>Prev <span class="token operator">=</span> target_tle<span class="token punctuation">;</span>
insert_tle<span class="token operator">-&gt;</span>Next <span class="token operator">=</span> target_tle<span class="token operator">-&gt;</span>Next<span class="token punctuation">;</span>

target_tle<span class="token operator">-&gt;</span>Next<span class="token operator">-&gt;</span>Prev <span class="token operator">=</span> insert_tle<span class="token punctuation">;</span>
target_tle<span class="token operator">-&gt;</span>Next <span class="token operator">=</span> insert_tle<span class="token punctuation">;</span>
<span class="token keyword">if</span><span class="token punctuation">(</span>target_tle <span class="token operator">==</span> tail_tle<span class="token punctuation">)</span>
	tail_tle <span class="token operator">=</span> insert_tle<span class="token punctuation">;</span>
insert_tle<span class="token operator">-&gt;</span>task_handle<span class="token operator">-&gt;</span>tss<span class="token operator">-&gt;</span>active_task_num<span class="token operator">++</span><span class="token punctuation">;</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32

下面在介紹出列,如下圖所示將tle2刪除,上圖是未刪入之前的狀態,後者是刪後的狀態。
下圖中綠色加粗的虛線就是入列後,對整個鏈表的影響。
20191201200956.png
20191201201013.png

到目前爲止,大家應該對整個任務調度器的原理有了最基本的認識:
原來就是利用鏈表,通過輪詢鏈表中的tle的狀態,來執行tle中的任務句柄。

下面再看一眼,任務調度器的心臟,中斷函數的實現,其實僅僅實現了:

  1. 輪訓一遍,從head tle開始
  2. 檢查每一個tle的狀態,如果是delay,那麼就將其句柄中的delay計數器減去相應的值。如果delay數小於零,將狀態重新置爲Ready
  3. 如果是Task_ReadyWithTimer狀態,那麼就將句柄中的定時器減去相應的值,這個定時器將會用來提示任務是否超時
  4. 最後將active_task_num++,該變量保存在任務統計裏,將會用來指導任務調度器的工作是否開始
  5. 同時在整個執行的過程中暫時禁用了中斷,避免衝突
void SysTick_Handler(void)
{
	TaskListElementStr * lookup_tle;
<span class="token comment">//make sure that before handle run, there are at least one or more tle in the list</span>
<span class="token function">assert_param</span><span class="token punctuation">(</span><span class="token function">__Check_Point_Is_Valid</span><span class="token punctuation">(</span>head_tle<span class="token punctuation">)</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">// enter critical code	</span>
<span class="token function">__disable_irq</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>


<span class="token comment">// initial the lookup_tle</span>
lookup_tle <span class="token operator">=</span> head_tle<span class="token punctuation">;</span>

/*
1. traversal all tle in the list one by one to find which tle was in Task_Delay
2. if it was Task_Delay then sub the delay_us with US_PER_TICK so does Task_ReadyWithTimer
3. after subtraction then check the delay_us agian. to pull it to ready again if necessary
*/

do
{

	<span class="token keyword">if</span><span class="token punctuation">(</span>lookup_tle<span class="token operator">-&gt;</span>task_handle<span class="token operator">-&gt;</span>task_status <span class="token operator">==</span> Task_Delay<span class="token punctuation">)</span>
	<span class="token punctuation">{</span>
		<span class="token keyword">if</span><span class="token punctuation">(</span>lookup_tle<span class="token operator">-&gt;</span>task_handle<span class="token operator">-&gt;</span>delay_us <span class="token operator">&gt;</span> <span class="token number">0</span><span class="token punctuation">)</span>
			lookup_tle<span class="token operator">-&gt;</span>task_handle<span class="token operator">-&gt;</span>delay_us <span class="token operator">-</span><span class="token operator">=</span> US_PER_TICK<span class="token punctuation">;</span>

		<span class="token keyword">if</span><span class="token punctuation">(</span>lookup_tle<span class="token operator">-&gt;</span>task_handle<span class="token operator">-&gt;</span>delay_us <span class="token operator">&lt;=</span> <span class="token number">0</span><span class="token punctuation">)</span>
			lookup_tle<span class="token operator">-&gt;</span>task_handle<span class="token operator">-&gt;</span>task_status <span class="token operator">=</span> Task_Ready<span class="token punctuation">;</span>
	<span class="token punctuation">}</span>
	<span class="token keyword">else</span> <span class="token keyword">if</span><span class="token punctuation">(</span>lookup_tle<span class="token operator">-&gt;</span>task_handle<span class="token operator">-&gt;</span>task_status <span class="token operator">==</span> Task_ReadyWithTimer<span class="token punctuation">)</span>
		lookup_tle<span class="token operator">-&gt;</span>task_handle<span class="token operator">-&gt;</span>timer_us <span class="token operator">-</span><span class="token operator">=</span> US_PER_TICK<span class="token punctuation">;</span>
	<span class="token keyword">else</span>
	<span class="token punctuation">{</span>
		lookup_tle<span class="token operator">-&gt;</span>task_handle<span class="token operator">-&gt;</span>delay_us <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
		lookup_tle<span class="token operator">-&gt;</span>task_handle<span class="token operator">-&gt;</span>timer_us <span class="token operator">=</span> <span class="token number">0</span><span class="token punctuation">;</span>
	<span class="token punctuation">}</span>
	lookup_tle <span class="token operator">=</span> lookup_tle<span class="token operator">-&gt;</span>Next<span class="token punctuation">;</span>
<span class="token punctuation">}</span><span class="token keyword">while</span><span class="token punctuation">(</span>lookup_tle <span class="token operator">!=</span> head_tle<span class="token punctuation">)</span><span class="token punctuation">;</span>

<span class="token comment">//this signal will be used for </span>
lookup_tle<span class="token operator">-&gt;</span>task_handle<span class="token operator">-&gt;</span>tss<span class="token operator">-&gt;</span>active_signal_num<span class="token operator">++</span><span class="token punctuation">;</span>
<span class="token comment">// leave critical code	</span>
<span class="token function">__enable_irq</span><span class="token punctuation">(</span><span class="token punctuation">)</span><span class="token punctuation">;</span>

}

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
  • 31
  • 32
  • 33
  • 34
  • 35
  • 36
  • 37
  • 38
  • 39
  • 40
  • 41
  • 42
  • 43
  • 44
  • 45

到這裏基本上已經完成介紹,接下來開始寫任務調度器本身,源碼如下,是不是很簡單,其實就是一個while(1):

  1. 當active_signal_num不爲0後,調度器就正式開始工作
  2. 檢查當前tle是否是ready,如果不是直接pass
  3. 如果是就利用函數指針進入並工作,此時就要離開任務調度器去具體的實現函數裏
  4. 當完成當前任務後,回到任務調度器,將當前tle指向下一個tle
  5. 如果當前tle是header,意味着已經從頭到尾完成了一輪,等待下一個active_signal_num不爲0.
void	task_scheduler()
{
	while(1)
	{
		if((realtime_tle->task_handle->tss->active_signal_num >= 1)&&(__Check_Num_Is_Positive(realtime_tle->task_handle->tss->active_task_num)))
		{
			realtime_tle->task_handle->tss->active_signal_num--;
			while(1)
			{
				__TASK_RECORD_ENTRY_TIME(realtime_tle->task_handle)
				switch(realtime_tle->task_handle->task_status)
			   	{
					case Task_Ready:
					{
						realtime_tle->task_handle->delay_us = 0;
						realtime_tle->task_handle->timer_us = 0;
						realtime_tle->task_handle->task_process(realtime_tle->task_handle,realtime_tle->task_handle->para2);
						break;
					}						
					case Task_ReadyWithTimer:
					{
						realtime_tle->task_handle->delay_us = 0;						
						realtime_tle->task_handle->task_process(realtime_tle->task_handle,realtime_tle->task_handle->para2);
						break;
					}
					case Task_Error:
					{
						// should not run to here
						assert_param(1==0);
						break;
					}
					case Task_Delay:
					{
						realtime_tle->task_handle->timer_us = 0;
						break;
					}
					case Task_Sleep:
					{
						break;
					}
					default:
					{
						// should not run to here
						assert_param(1==0);
						break;
					}					
				}
/*
				after currenttask has been conplete, the next task should be call
*/
				__TASK_RECORD_EXIT_TIME(realtime_tle->task_handle)
				realtime_tle = realtime_tle->Next;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章