話不多說,直接進入正題,今天要實現的便是RTOS任務相關的所有功能
1 臨界區保護
本節代碼在05_critical下
爲什麼需要臨界區保護呢,請看下圖:
當task1要對共享資源進行讀-改-寫操作時,在寫回之前被某一事件中斷打斷切換到task2,而此時task2恰巧也有修改共享資源x的代碼,此時task2將共享資源修改成了11,當完成這個操作後,task2交出cpu控制權,此時RTOS又切換到了task1運行,執行讀-改-寫的寫操作,將tmp值回寫到共享資源x中,此時task2對共享資源x操作會被覆蓋,等於沒有發生。這其中的共享資源x便稱爲臨界資源,當任務對臨界資源進行操作時,必須要有相應的臨界區保護方能逃過一劫。
對於臨界區保護有多種方式:
- 關中斷,此種方法最爲簡單粗暴,直接關閉中斷,再也不會有任何事件打斷,當前任務會獨佔CPU,等到執行完成臨界區代碼後再打開中斷。
- 調度鎖,在進入臨界區後不允許OS進行任務調度,此種方法只能用於任務之間,無法用於ISR和任務之間。
- 同步機制, 信號量,互斥鎖等,此種方法只能用於任務之間。
沒有臨界區保護
本小節主要實現關中斷來完成臨界區保護,調度鎖本文沒有完成,而同步機制會在後續的事件控制塊中講述。我們首先來看一下沒有臨界區保護的代碼的行爲。代碼主要是修改兩個應用任務,其描述與上圖相符合,task1每隔1s更新一次test_sync_val,task2每隔5s**讀-改-寫**一次test_sync_val。
main.c
18 uint32_t test_sync_val = 0;
19
20 void task1_entry(void *param)
21 {
22 uint32_t status = 0;
23 init_systick(1000);
24 for(;;) {
25 printk("%s\n", __func__);
26 //status = task_enter_critical();
27 task_delay(1);
28 test_sync_val++;
29 //task_exit_critical(status);
30 printk("task1:test_sync_val:%d\n", test_sync_val);
31 }
32 }
33
34 void task2_entry(void *param)
35 {
36 uint32_t counter = test_sync_val;
37 uint32_t status = 0;
38
39 for(;;) {
40 printk("%s\n", __func__);
41
42 //status = task_enter_critical();
43 counter = test_sync_val;
44 task_delay(5);
45 test_sync_val = counter +1;
46 //task_exit_critical(status);
47
48 printk("task2:test_sync_val:%d\n", test_sync_val);
49 }
50 }
運行結果如下圖,task1修改的結果會被task2的讀-改-寫,而這並不是我們想看到的結果。
增加臨界區保護
接口定義
臨界區保護需要定義兩個接口。task_enter_critical在進入臨界區調用,task_exit_critical在退出臨界區調用。
task.h
25 extern uint32_t task_enter_critical(void);
26 extern void task_exit_critical(uint32_t status);
接口實現
task_enter_critical很簡單,首先獲取primask的值,然後保存下來,因爲我們的OS允許進入多次臨界區,所以除了關閉CM3的IRQ以外,還需要把primask的值記錄下來,等到退出時恢復。這樣是防止嵌套情況下退出臨界區錯誤打開了中斷。
PRIMASK是個只有單一比特的寄存器。在它被置1後,就關掉所有可屏蔽的異常,只剩下NMI和硬fault可以響應。它的缺省值是0,表示沒有關中斷。
task_exit_critical與task_enter_critical正好是反操作,恢復primask的值並打開irq。 primask和irq中斷開關的函數必須用匯編來實現,實現在cm3_s.s中。
task.c
118 uint32_t task_enter_critical(void)
119 {
120 uint32_t ret = get_primask();
121 disable_irq();
122 return ret;
123 }
124
125 void task_exit_critical(uint32_t status)
126 {
127 set_primask(status);
128 enable_irq();
129 }
cm3_s.s
86 get_primask:
87 mrs r0, PRIMASK
88 blx lr
89
90 set_primask:
91 msr PRIMASK, r0
92 blx lr
93
94 disable_irq:
95 cpsid i
96 blx lr
97
98 enable_irq:
99 cpsie i
100 blx lr
應用測試
應用代碼和上一小節基本一樣,只是將註釋的臨界區保護打開。不多說,直接看運行結果,可以看到兩個任務能夠很好同步了共享資源的讀寫。
main.c
18 uint32_t test_sync_val = 0;
19
20 void task1_entry(void *param)
21 {
22 uint32_t status = 0;
23 init_systick(1000);
24 for(;;) {
25 printk("%s\n", __func__);
26 status = task_enter_critical();
27 task_delay(1);
28 test_sync_val++;
29 task_exit_critical(status);
30 printk("task1:test_sync_val:%d\n", test_sync_val);
31 }
32 }
33
34 void task2_entry(void *param)
35 {
36 uint32_t counter = test_sync_val;
37 uint32_t status = 0;
38
39 for(;;) {
40 printk("%s\n", __func__);
41
42 status = task_enter_critical();
43 counter = test_sync_val;
44 task_delay(5);
45 test_sync_val = counter +1;
46 task_exit_critical(status);
47
48 printk("task2:test_sync_val:%d\n", test_sync_val);
49 }
50 }
2 OS數據結構
本節代碼位於06_multi_prio
在將優先級,時間片調度及延時隊列等問題前,有必要把OS中的數據結構給捋一遍。其實就兩,位圖和雙向循環鏈表。
位圖
位圖法就是bitmap的縮寫。所謂bitmap,就是用每一位來存放某種狀態,適用於大規模數據,但數據狀態又不是很多的情況。通常是用來判斷某個數據狀態。
接口定義
本文中的bitmap數據結構很簡單,就包含了一個uint32_t的字段bitmap,原因是本文用到的位圖是用於任務優先級,而本設計中OS的任務優先級最多大就31,最小爲0,所以只需要一個字節的位圖即可。所以這裏的位圖結構並不算一個通用數據結構。
lib.h
6 /*Bitmap*/
7 typedef struct bitmap_tag {
8 uint32_t bitmap;
9 }bitmap_t;
10
11 extern void bitmap_init(bitmap_t *bitmap);
12 extern uint32_t bitmap_count(void);
13 extern void bitmap_set(bitmap_t *bitmap, uint32_t pos);
14 extern void bitmap_clear(bitmap_t *bitmap, uint32_t pos);
15 extern uint32_t bitmap_get_first_set(bitmap_t *bitmap);
實現
lib.c
bitmap_init初始化bitmap,對所有的位清零。
bitmap_count計算bitmap一共有多少個位,這裏返回固定的32。
bitmap_set設置bitmap某一位。
bitmap_clear清零bitmap某一位。
bitmap_get_first_set獲取bitmap第一個非0位。有些讀者可能會感到迷惑:獲取第一個非0位不就是一個簡單的循環查位嗎?爲什麼要搞得這麼複雜,確如讀者所想,如果只是爲了獲取第一個非0位,事情不會如此複雜,一個循環就能解決問題,但因爲這裏用於RTOS,所以時間上它的操作必須是常量,所以這裏採取了一個讀者不熟悉的做法,至於這個做法的原理,讀者只要模仿計算機執行這段程序即可。節選自《嵌入式實時操作系統uC/OS-II原理及應用(第四版)》
void bitmap_init(bitmap_t *bitmap)
{
bitmap->bitmap = 0;
}
uint32_t bitmap_count()
{
return 32;
}
void bitmap_set(bitmap_t *bitmap, uint32_t pos)
{
bitmap->bitmap |= 1 << pos;
}
void bitmap_clear(bitmap_t *bitmap, uint32_t pos)
{
bitmap->bitmap &= ~(1 << pos);
}
uint32_t bitmap_get_first_set(bitmap_t *bitmap)
{
uint32_t pos = 32;
static const uint8_t quick_table[] =
{
/* 00 */ 0xff, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 10 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 20 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 30 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 40 */ 6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 50 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 60 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 70 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 80 */ 7, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* 90 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* A0 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* B0 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* C0 */ 6, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* D0 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* E0 */ 5, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0,
/* F0 */ 4, 0, 1, 0, 2, 0, 1, 0, 3, 0, 1, 0, 2, 0, 1, 0
};
if (bitmap->bitmap & 0xff) {
pos = quick_table[bitmap->bitmap & 0xff];
} else if (bitmap->bitmap & 0xff00) {
pos = quick_table[(bitmap->bitmap >> 8) & 0xff] + 8;
} else if (bitmap->bitmap & 0xff0000) {
pos = quick_table[(bitmap->bitmap >> 16) & 0xff] + 16;
} else if (bitmap->bitmap & 0xFF000000) {
pos = quick_table[(bitmap->bitmap >> 24) & 0xFF] + 24;
} else {
pos = bitmap_count();
}
return pos;
}
雙向循環鏈表
雙向循環鏈表採用了類似linux雙向循環鏈表的實現,相比於linux的鏈表只是多了個count字段記錄鏈表中有多少個節點,原理網上有很多,就不講了,百度搜索linux雙向循環鏈表即可。這裏僅把接口定義描述一遍,代碼實現有興趣的朋友可以結合網上linux雙向循環鏈表來理解。
list_node_t定義了鏈表節點,prev指向前一個節點,next指向後一個節點
list_t定義了鏈表結構,其中包含一個鏈表頭和一個計數值count,該計數值代表鏈表中含有多少個鏈表節點。
container_of從鏈表節點元素獲取該其父結構。
list_init初始化鏈表。
list_count獲取鏈表含有多少個鏈表節點(除頭節點)。
list_head獲取鏈表頭節點。
list_tail獲取鏈表尾節點。
node_prev獲取該節點的前一個節點。
node_next獲取該節點的後一個節點。
list_remove_all刪除所有鏈表所有節點。
list_insert_head插入list_node到鏈表頭部。
list_append_last插入list_node到鏈表尾部。
list_remove_first刪除鏈表第一個元素並返回。
list_remove刪除鏈表節點node。
lib.h
/*Double Linked List*/
typedef struct list_node_tag {
struct list_node_tag *prev;
struct list_node_tag *next;
}list_node_t;
extern void list_node_init(list_node_t *node);
typedef struct list_tag {
list_node_t head;
uint32_t node_count;
}list_t;
#define container_of(node, parent, name) (parent *)((uint32_t)node - (uint32_t)&((parent *)0)->name)
extern void list_init(list_t *list);
extern uint32_t list_count(list_t *list);
extern list_node_t *list_head(list_t *list);
extern list_node_t *list_tail(list_t *list);
extern list_node_t *node_prev(list_node_t *list_node);
extern list_node_t *node_next(list_node_t *list_node);
extern void list_remove_all(list_t *list);
extern void list_insert_head(list_t *list, list_node_t *list_node);
extern void list_append_last(list_t *list, list_node_t *list_node);
extern list_node_t *list_remove_first(list_t *list);
extern void list_remove(list_t *list, list_node_t *node);
3 多優先級任務
RTOS與通用OS比較大的一個差異就是搶佔式調度。搶佔式調度一定是基於多優先級完成的,高優先級的任務能搶佔低優先級任務的CPU使用權。
回想現在我們的系統中關於任務的數據結構只有一個任務表g_task_table,而且此時的任務是沒有優先級之分的。本節完成一個優先級只對應於一個任務。UCOS2也是這種做法,系統簡單的情況下使用一個優先級對應一個任務已然足夠。在這種前提下,任務表g_task_table的索引對應於任務的優先級。只有一個任務表是不夠的,我們需要另外一個數據結構來表示該優先級有沒有處於可運行狀態。這時上一章節所說的bitmap就派上用場了。規定RTOS只有32個優先級,bitmap的每一位對應於一個優先級,當某一位設1,表示該優先級有任務需要運行,如果該位爲0,則表示該優先級沒有任務需要運行。大致情況如下圖:
如上所述,現在一共有兩個數據結構,一個是任務表g_task_table,另外一個乃優先級位圖g_task_prio_bitmap。初試狀態g_task_prio_bitmap的第0位爲0,表示最高優先級並沒有需要運行的任務,所以非0最低位爲第一位,因此此時RTOS執行任務表中task2(優先級爲1),隨後第0位被某個事件置1,表示此時優先級0有任務需要運行,當下一次任務調度的時候,就會將當前任務切換到task1(優先級爲0)來執行。
實現
- 在任務控制塊結構中添加表示優先級的字段prio
task.h
8 /*Task Control block*/
9 typedef struct task_tag {
10
11 task_stack_t *stack;
12 uint32_t delay_ticks;
13 uint32_t prio;
14 }task_t;
- 修改task_init接口,初始化時需要傳入prio
task.c
21 void task_init (task_t * task, void (*entry)(void *), void *param, uint32_t prio, uint32_t * stack)
22 {
...
//初始化優先級
43 task->prio = prio;
44 //任務表中一個優先級對應一個任務
45 g_task_table[prio] = task;
//設置優先級位圖相應的位
46 bitmap_set(&g_task_prio_bitmap, prio);
47 }
- 增加接口task_t *task_highest_ready()。該任務返回最高優先級的任務指針。代碼實現很簡單,就是從bitmap中獲取最低非0位,即需要運行的最高優先級,然後從任務表中返回任務指針即可。
task.c
159 task_t *task_highest_ready()
160 {
161 uint32_t highest_prio = bitmap_get_first_set(&g_task_prio_bitmap);
162 return g_task_table[highest_prio];
163 }
- 還記得之前的又長又臭的task_sched嗎,這次要推倒重來了,煥然一新的調度算法!上代碼
task.c
49 void task_sched()
50 {
51 uint32_t status = task_enter_critical();
52 task_t *temp_task_p;
53
54 if (g_sched_lock > 0) {
55 goto no_need_sched;
56 }
57
58 temp_task_p = task_highest_ready();
59 if (temp_task_p != g_current_task) {
60 g_next_task = temp_task_p;
61 task_switch();
62 }
63
64 no_need_sched:
65 task_exit_critical(status);
66 return;
67 }
這次代碼的邏輯就非常簡單了,獲取最高優先級的任務,如果當前任務已經是最高優先級了,那什麼也不做,否則就觸發一次任務切換,將任務切換到更高優先級的任務去。
- 修改延時函數 void task_delay(uint32_t ticks)。原來的延時函數只是將ticks賦值給當前任務的delay_ticks字段。多增加了一句bitmap_clear(&g_task_prio_bitmap, g_current_task->prio),就是說在調用延時函數後,需要把該任務對應的優先級位圖清0。那麼下次調度的時候就不會選中這個任務了。
task.c
82 void task_delay(uint32_t ticks)
83 {
...
86 bitmap_clear(&g_task_prio_bitmap, g_current_task->prio);
...
89 }
6.修改void task_system_tick_handler(void)。其流程爲遍歷任務表g_task_table,如果該某一個任務的延時時間到了,那麼就將它再次加入就緒表中,即將g_task_prio_bitmap相應的prio位置1。
task.c
96 void task_system_tick_handler(void)
97 {
98 uint32_t status = task_enter_critical();
99 uint32_t i = 0;
100 for (i = 0; i < OS_PRIO_COUNT; i++) {
101 if (g_task_table[i] == (task_t *)NULL) {
102 continue;
103 }
104
105 if (g_task_table[i]->delay_ticks > 0) {
106 g_task_table[i]->delay_ticks--;
107 } else {
108 bitmap_set(&g_task_prio_bitmap, i);
109 }
110 }
111
112 task_exit_critical(status);
113 task_sched();
114 }
增加void task_delay_s(uint32_t seconds)。這個其實跟優先級無關,只是將systick每1s發生中斷改成了每10ms發生一次中斷,這樣更符合正常RTOS的做法。這個接口用於延時幾秒。
task.c
91 void task_delay_s(uint32_t seconds)
92 {
93 task_delay(seconds * 100);
94 }
應用測試
將task1設置爲最高優先級0,task2設置爲優先級1,但運行的結果還是兩個任務交替的打印。因爲當task1延時的時候,它會把相應優先級位清0,即從優先級表中移除該任務,然後讓低優先級的task2運行。如果task1去掉延時函數,那麼task1會一直獨佔cpu而不讓低優先級的task2運行。
main.c
19 void task1_entry(void *param)
20 {
21 init_systick(10);
22 for(;;) {
23 printk("%s\n", __func__);
24 task_delay_s(1);
25 }
26 }
27
28 void task2_entry(void *param)
29 {
30
31 for(;;) {
32 printk("%s\n", __func__);
33 task_delay_s(2);
34 }
35 }
42 int main()
51 task_init(&task1, task1_entry, (void *)0x11111111, 0, &task1_stk[1024]);
52 task_init(&task2, task2_entry, (void *)0x22222222, 1, &task2_stk[1024]);
57 return 0;
58 }
4 延時隊列
本章源代碼位於07_delay_queue目錄下
小結一下,此時此刻,有數據結構任務表,優先級位圖。本小節我們增加一個延時隊列來存放處於延時狀態的任務。也就是說當任務處於延時狀態時,RTOS會將任務從任務表中移除添加到延時隊列中。過程如下圖:
實現
理論很簡單,來看下代碼實現:
- 首先要在任務控制塊task_t增加延時節點delay_node,並且增加一個字段state,代表任務狀態。當前任務狀態分爲兩種,就緒狀態OS_TASK_STATE_RDY和延時狀態OS_TASK_STATE_DELAYED。當然需要定義一個延時隊列變量g_task_delay_list,在task_init中初始化delay_node
task.h
8 #define OS_TASK_STATE_RDY 0
9 #define OS_TASK_STATE_DELAYED (1 << 1)
10
11 typedef uint32_t task_stack_t;
12 /*Task Control block*/
13 typedef struct task_tag {
14
15 task_stack_t *stack;
16 uint32_t delay_ticks;
17 uint32_t prio;
18
19 list_node_t delay_node;
20 uint32_t state;
21 }task_t;
task.c
15 static list_t g_task_delay_list;
22 void task_init (task_t * task, void (*entry)(void *), void *param, uint32_t prio, uint32_t * stack)
23 {
46 task->state = OS_TASK_STATE_RDY;
47 list_node_init(&task->delay_node);
51 }
- 增加一些輔助接口
task_ready接口用於將任務加入就緒任務表中(即加入任務表,將相應就緒位圖置1。
task_unready接口用於將任務從就緒任務表中移除。
task_delay_wait接口將任務加入延時隊列並且將任務狀態置爲延時狀態。
task_delay_wakeup接口將任務從延時隊列移除並且清除任務延時狀態。
task.c
186 void task_ready(task_t *task)
187 {
188 g_task_table[task->prio] = task;
189 bitmap_set(&g_task_prio_bitmap, task->prio);
190 }
191
192 void task_unready(task_t *task)
193 {
194 g_task_table[task->prio] = (task_t *)NULL;
195 bitmap_clear(&g_task_prio_bitmap, task->prio);
196 }
197
198 void task_delay_wait(task_t *task, uint32_t ticks)
199 {
200 task->delay_ticks = ticks;
201 list_insert_head(&g_task_delay_list, &(task->delay_node));
202 task->state |= OS_TASK_STATE_DELAYED;
203 }
204
205 void task_delay_wakeup(task_t *task)
206 {
207 list_remove(&g_task_delay_list, &(task->delay_node));
208 task->state &= ~OS_TASK_STATE_DELAYED;
209 }
- 修改task_delay接口
task_delay接口實現邏輯相較以前更爲清晰,就兩步:1.將任務加入延時隊列,2.將任務從就緒表中移除。
task.c
86 void task_delay(uint32_t ticks)
87 {
88 uint32_t status = task_enter_critical();
89
90 /* 1.Add task to delay list
91 * 2.Remove the task from task list
92 * 3.Clear the task bit from prioity bitmap
93 * */
94 task_delay_wait(g_current_task, ticks);
95 task_unready(g_current_task);
96
97 task_exit_critical(status);
98 task_sched();
99 }
- 修改task_system_tick_handler接口
這個函數原來是遍歷就緒表,對就緒表中每一個任務自減。而現在加入了延時隊列後,不需要再對就緒表進行遍歷,只需要遍歷延時隊列中任務即可。實現很簡單,遍歷延時隊列,對每一個任務的delay_tick自減1,如果該任務的delay_ticks爲0,那麼將該任務從延時隊列移除,加入就緒表中,最後觸發一次任務調度
task.c
106 void task_system_tick_handler(void)
107 {
108 uint32_t status = task_enter_critical();
109 list_node_t *head = &(g_task_delay_list.head);
110 list_node_t *temp_node = head->next;
111 task_t *task = (task_t *)NULL;
112 /*
113 * For each the delay list, and do:
114 * 1. Self sub the node delay ticks
115 * */
116 while (temp_node != head) {
117 task = container_of(temp_node, task_t, delay_node);
118 temp_node = temp_node->next;
119 if (--task->delay_ticks == 0) {
120 /*
121 * 1.Remove the task from delay list
122 * 2. Add the task to task table
123 * 3.Set the prio bit to bitmap
124 * */
125 task_delay_wakeup(task);
126 task_ready(task);
127 }
128 }
129
130 task_exit_critical(status);
131 task_sched();
132 }
- 不要忘了初始化延時隊列
在OS跑起來前,初始化一把延時隊列。
task.c
134 void init_task_module()
135 {
136 task_init(&g_idle_task_obj, idle_task_entry, (void *)0, OS_PRIO_COUNT - 1, &g_idle_task_stk[1024]);
137 g_idle_task = &g_idle_task_obj;
138
139 g_sched_lock = 0;
140 list_init(&g_task_delay_list);
141
142 g_next_task = task_highest_ready();
143 task_run_first();
144 }
應用測試代碼並無改動,還是兩個任務交替執行打印。這裏就不再截圖了。各位看官運行在07_delay_queue運行make run即可。
5 同優先級時間片調度
本節代碼位於08_prio_slice目錄下
第3節中我們實現了一個優先級對應於一個任務,那麼本節將帶領你完成一個優先級對應於多個任務。而同優先級的任務採用何種調度方法呢?在這裏,我們採用了時間片輪轉來調度同優先級的任務。
如上圖所示,task1和task3是同優先級的任務,此時優先級bitmap的第一個非0位是第零位。現假設一個任務的時間片就是一個systick,那麼當一次systick中斷髮生時,task1會取出插入到list0尾,而task1後面的task3會成爲新的第一個就緒任務,並且運行它。
代碼實現
- 首先要在任務控制塊task_t中添加跟時間片相關的字段slice以及就緒隊列相關的鏈表節點prio_list_node,同時我們定義最大時間片爲10個systick。
task.h
#define OS_SLICE_MAX 10
13 typedef struct task_tag {
14
15 task_stack_t *stack;
16 uint32_t delay_ticks;
17 uint32_t prio;
18
19 list_node_t delay_node;
20 uint32_t state;
21
22 list_node_t prio_list_node;
23 uint32_t slice;
24 }task_t;
- 之前就緒任務表g_task_table也需要修改了,之前是一個優先級對應於一個任務,而現在一個優先級對應了多個任務,所以此時存放在就緒表中的是一個任務鏈表,修改如下。
task.c
extern list_t g_task_table[OS_PRIO_COUNT];
- 初始化任務函數需要把slice和prio_list_node初始化,並把該任務加入到就緒表相應的鏈表尾部。代碼如下
task.c
22 void task_init (task_t * task, void (*entry)(void *), void *param, uint32_t prio, uint32_t * stack)
23 {
49 task->slice = OS_SLICE_MAX;
50 list_node_init(&task->prio_list_node);
51 list_append_last(&g_task_table[prio], &(task->prio_list_node));
52 bitmap_set(&g_task_prio_bitmap, prio);
53 }
- 修改task_system_tick_handler接口
在該函數中增加代碼如下,檢查當前的任務時間片是否用完,如果用完,那麼將CPU交給後一個同優先級的任務。從代碼上反應就是將任務從相應的就緒任務鏈表裏拿出來插入到該任務鏈表尾。
task.c
132 /*
133 * check whether the time slice of current task exhausts
134 * if time slice is over, move the task to the prio list tail
135 */
136 if (--g_current_task->slice == 0) {
137 if (list_count(&g_task_table[g_current_task->prio]) > 0) {
138 list_remove(&g_task_table[g_current_task->prio], &(g_current_task->prio_list_node));
139 list_append_last(&g_task_table[g_current_task->prio], &(g_current_task->prio_list_node));
140 g_current_task->slice = OS_SLICE_MAX;
141 }
142 }
- 修改task_highest_ready接口
之前直接返回就緒表中相應優先級的任務即可,而現在需要先拿出就緒表中的鏈表,然後從鏈表裏拿出第一個任務返回。
task.c
196 task_t *task_highest_ready()
197 {
198 /*
199 * Highest prio task
200 * |
201 * g_task_table[0] -> task -> task -> task;
202 * ....
203 * g_task_table[31] -> task -> task;
204 */
205 uint32_t highest_prio = bitmap_get_first_set(&g_task_prio_bitmap);
206 list_node_t *node = list_head(&(g_task_table[highest_prio]));
207 return container_of(node, task_t, prio_list_node);
208 }
- 修改task_ready和task_unready接口
task_ready之前是直接將該task放到就緒表中,而現在是將任務插入到相應就緒表中的優先級鏈表中。
task_unready稍微複雜一點,就是說只有在優先級鏈表已經沒有了任務,那麼才把優先級bitmap清0。否則該優先級的任務鏈表還存有一個任務,那麼優先級bitmap中的位就不會清零。
task.c
210 void task_ready(task_t *task)
211 {
212 list_append_last(&g_task_table[task->prio], &(task->prio_list_node));
213 bitmap_set(&g_task_prio_bitmap, task->prio);
214 }
215
216 void task_unready(task_t *task)
217 {
218 list_remove(&g_task_table[task->prio], &(task->prio_list_node));
219 if (list_count(&g_task_table[task->prio]) == 0) {
220 bitmap_clear(&g_task_prio_bitmap, task->prio);
221 }
222 bitmap_clear(&g_task_prio_bitmap, task->prio);
223 }
應用測試
測試代碼如下,創建3個task,task1的優先級爲0(最高),task2和task3的優先級爲2。此時當task1處於延時狀態時,task2和task3會根據時間片進行輪詢調度。可以看到task2和task3使用的是軟件延時,所以如果時間片調度的話,在task1處於延時狀態時,只會運行task2。但我們現在已經有了同優先級的時間片調度,所以能看到task2和task3交替運行。
main.c
19 void task1_entry(void *param)
20 {
21 init_systick(10);
22 for(;;) {
23 printk("%s\n", __func__);
24 task_delay_s(2);
25 }
26 }
27
28 void delay(uint32_t delay)
29 {
30 while(delay--);
31 }
32
33 void task2_entry(void *param)
34 {
35
36 for(;;) {
37 printk("%s\n", __func__);
38 delay(65536000);
39 }
40 }
41
42 void task3_entry(void *param)
43 {
44 for(;;) {
45 printk("%s\n", __func__);
46 delay(65536000);
47 }
48 }
69 task_init(&task1, task1_entry, (void *)0x11111111, 0, &task1_stk[1024]);
70 task_init(&task2, task2_entry, (void *)0x22222222, 1, &task2_stk[1024]);
71 task_init(&task3, task3_entry, (void *)0x33333333, 1, &task3_stk[1024]);
make run走你!
6任務掛起/喚醒
任務的掛起/喚醒其實很簡單,就是將任務從就緒表中的優先級隊列中將任務移除即可,而任務的喚醒只是將任務重新放到該鏈表尾部即可。並不添加任何新的數據結構去管理這些被掛機的任務,當然各位看官如果想自己用一個鏈表把這些掛起串起來也可以。
實現
那我們不多說了,應該很簡單,所以直接看代碼實現吧。
- 雖然不需要增加數據結構,但我們需要一個狀態位來標誌該任務是否是掛起狀態。並且我們認爲任務是可以多次掛起的,所以加入一個掛起計數。當掛起計數爲0的時候,也就是所以的掛起操作都對應一個喚醒操作時,才把任務加入到就緒鏈表中。
task.h
10 #define OS_TASK_STATE_SUSPEND (1 << 2)
13 typedef struct task_tag {
......
27 /*Suspend resume*/
28 uint32_t suspend_cnt;
29 }task_t;
- 實現接口task_suspend和task_resume。
當任務處於延時狀態時,任務是不能被掛起的,task_suspend首先判斷了任務是不是延時狀態。如果不是,那就把任務的掛起計數自增1,然後把任務的掛起位置1,然後把任務從就緒表中移除,如果掛起的任務是自己,那麼就觸發一次任務調度。
task_resume先判斷任務是不是掛起狀態。然後把任務的掛起計數自增1,將任務的掛起位清零,將任務加入到就緒表中。如果該任務掛起計數爲0,就觸發一次任務調度。
task.c
241 void task_suspend(task_t *task)
242 {
243 uint32_t status = task_enter_critical();
244
245 /*Don't suspend the task in delay state*/
246 if (!(task->state & OS_TASK_STATE_DELAYED)) {
247 /*If the task is first suspend*/
248 if (task->suspend_cnt++ <= 0) {
249 task->state |= OS_TASK_STATE_SUSPEND;
250
251 task_unready(task);
252 if (task == g_current_task) {
253 task_sched();
254 }
255 }
256
257 }
258
259 task_exit_critical(status);
260 }
261
262 extern void task_resume(task_t *task)
263 {
264 uint32_t status = task_enter_critical();
265
266 if (task->state & OS_TASK_STATE_SUSPEND){
267 if (--task->suspend_cnt == 0) {
268 task->state &= ~OS_TASK_STATE_SUSPEND;
269 task_ready(task);
270 task_sched();
271 }
272 }
273 task_exit_critical(status);
274 }
應用測試
測試代碼只有兩個任務,task1在打印完before suspend後會掛起自己,然後等待喚醒。此時RTOS會切換到task2來運行,task2會在打印完後延時1秒喚醒task1。可以推斷出打印的順序是
task1_entry efore_suspend
task2_entry
task1_entry after_suspend
main.c
25 void task1_entry(void *param)
26 {
27 init_systick(10);
28 for(;;) {
29 printk("%s:before suspend\n", __func__);
30 task_suspend(&task1);
31 printk("%s:after suspend\n", __func__);
32 }
33 }
40 void task2_entry(void *param)
41 {
42
43 for(;;) {
44 printk("%s\n", __func__);
45 task_delay_s(1);
46 task_resume(&task1);
47 }
48 }
make run走你!
7 任務刪除
本節代碼位於10_task_delete
任務的刪除分爲兩種
- 強制刪除
這種任務刪除的方式
優點在於任務能夠及時刪除
缺點在於任務刪除時會有一定概率導致待刪除任務持有的資源無法釋放。
- 設置刪除標誌,待刪除任務自己刪除
一個任務設置一個刪除標誌,等待待刪除任務自己調用刪除任務的函數。這樣做的優缺點正好與第一種強制刪除相反。
刪除任務很簡單,只需要把任務從就緒表,延時隊列中去掉即可。
代碼實現
在任務控制塊中加入任務刪除的字段。
- clean是任務刪除時候調用的回調函數,多用於釋放任務所佔有的資源
- clean_param是刪除回調函數所需要的參數
- request_del_flag用於非強制刪除時的刪除標誌
- task_force_delete強制刪除接口
- task_request_delete非強制刪除接口
- is_task_request_delete判斷該任務是否有刪除請求
- task_delete_self任務刪除自己
task.h
30 /*Task delete*/
31 void (*clean)(void *param);
32 void *clean_param;
33 uint8_t request_del_flag;
34 }task_t;
60 extern void task_force_delete(task_t *task);
61 extern void task_request_delete(task_t *task);
62 extern uint8_t is_task_request_delete(void);
63 extern void task_delete_self(void);
首先要在task_init函數中對新加的字段進行初始化
task.c
22 void task_init (task_t * task, void (*entry)(void *), void *param, uint32_t prio, uint32_t * stack)
23 {
...
57 task->clean = (void (*)(void *))NULL;
58 task->clean_param = (void *)NULL;
59 task->request_del_flag = 0;
60 }
task_force_delete接口很簡單,如果任務處於延時隊列中,那麼就將任務從延時隊列刪除,否則把任務從就緒表中移除,並且調用任務的清除函數。注意,不能刪除處於掛起狀態的任務。如果刪除的任務是當前任務,那麼觸發一次任務調度。
298 void task_force_delete(task_t *task)
299 {
300 uint32_t status = task_enter_critical();
301
302 if (task->state & OS_TASK_STATE_DELAYED) {
303 task_remove_from_delay_list(task);
304 } else if (!(task->state & OS_TASK_STATE_SUSPEND)) {
305 task_remove_from_prio_table(task);
306 }
307
308 if (task->clean) {
309 task->clean(task->clean_param);
310 }
311
312 if (g_current_task == task) {
313 task_sched();
314 }
315
316 task_exit_critical(status);
317 }
318
task_request_delete設置任務的request_del_flag爲1即可。
319 void task_request_delete(task_t *task)
320 {
321 uint32_t status = task_enter_critical();
322
323 task->request_del_flag = 1;
324
325 task_exit_critical(status);
326 }
is_task_request_delete返回當前任務的request_del_flag
328 uint8_t is_task_request_delete()
329 {
330 uint8_t delete;
331
332 uint32_t status = task_enter_critical();
333 delete = g_current_task->request_del_flag;
334 task_exit_critical(status);
335
336 return delete;
337 }
task_delete_self將自己從就緒表中移除,然後調用任務清除函數,並且觸發一次任務調度。因爲該接口只能由運行中的任務調用,所以不存在把任務從延時隊列中移除。
339 void task_delete_self(void)
340 {
341 uint8_t status = task_enter_critical();
342
343 task_remove_from_prio_table(g_current_task);
344
345 if (g_current_task->clean) {
346 g_current_task->clean(g_current_task->clean_param);
347 }
348
349 task_sched();
350
351 task_exit_critical(status);
352 }
應用測試
task2強制刪除task1,所以task1的for循環只會運行一次,即它的打只運行一次。
task4請求刪除task3,因爲task3不是強制刪除,所以task3的循環會運行兩次,當第二次循環的時候發現task4設置了刪除標誌,task3就會把自己刪除。
main.c
void task1_entry(void *param)
{
init_systick(10);
task_set_clean_callbk(g_current_task, task1_cleanup_func, (void *)0);
for(;;) {
printk("%s:before delay\n", __func__);
task_delay_s(1);
printk("%s:after delay\n", __func__);
}
}
void delay(uint32_t delay)
{
while(delay--);
}
void task2_entry(void *param)
{
uint32_t task_del = 0;
for(;;) {
printk("%s:before delay\n", __func__);
task_delay_s(1);
printk("%s:after delay\n", __func__);
if (!task_del) {
task_force_delete(&task1);
task_del = 1;
}
}
}
void task3_entry(void *param)
{
for(;;) {
printk("%s:before delay\n", __func__);
task_delay_s(1);
printk("%s:after delay\n", __func__);
if (is_task_request_delete()) {
task_delete_self();
}
task_delay_s(1);
}
}
void task4_entry(void *param)
{
uint32_t task3_del = 0;
for(;;) {
printk("%s:before delay\n", __func__);
task_delay_s(1);
printk("%s:after delay\n", __func__);
if (!task3_del) {
task_request_delete(&task3);
task3_del = 1;
}
task_delay_s(1);
}
}
8 任務查詢
任務查詢就是查詢任務在某一時刻的一些關鍵信息。只有extern void task_get_info(task_t *task, task_info_t *info)一個接口。因爲這一節代碼非常簡單,直接上代碼,也不測試了。
task.h
typedef struct task_info_tag {
uint32_t delay_ticks;
uint32_t prio;
uint32_t state;
uint32_t slice;
uint32_t suspend_cnt;
}task_info_t;
task.c
void task_get_info(task_t *task, task_info_t *info)
{
uint32_t status = task_enter_critical();
info->delay_ticks = task->delay_ticks;
info->prio = task->prio;
info->state = task->state;
info->slice = task->slice;
info->suspend_cnt = task->suspend_cnt;
task_exit_critical(status);
}
第三天將實現事件控制塊和存儲管理這一塊相關代碼。