自制嵌入式操作系统 DAY2

2024-06-03 17:58

本文主要是介绍自制嵌入式操作系统 DAY2,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

话不多说,直接进入正题,今天要实现的便是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;1920 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 }3334 void task2_entry(void *param)35 {36     uint32_t counter = test_sync_val;37     uint32_t status = 0;3839     for(;;) {40         printk("%s\n", __func__);4142         //status = task_enter_critical();43         counter = test_sync_val;44         task_delay(5);45         test_sync_val = counter +1;46         //task_exit_critical(status);4748         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, PRIMASK88     blx     lr8990 set_primask:91     msr     PRIMASK, r092     blx     lr9394 disable_irq:95     cpsid   i96     blx     lr9798 enable_irq:99     cpsie   i
100     blx     lr

应用测试

应用代码和上一小节基本一样,只是将注释的临界区保护打开。不多说,直接看运行结果,可以看到两个任务能够很好同步了共享资源的读写。
main.c

 18 uint32_t test_sync_val = 0;1920 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 }3334 void task2_entry(void *param)35 {36     uint32_t counter = test_sync_val;37     uint32_t status = 0;3839     for(;;) {40         printk("%s\n", __func__);4142         status = task_enter_critical();43         counter = test_sync_val;44         task_delay(5);45         test_sync_val = counter +1;46         task_exit_critical(status);4748         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;1011 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)来执行。

实现

  1. 在任务控制块结构中添加表示优先级的字段prio
    task.h
  8 /*Task Control block*/9 typedef struct task_tag {1011     task_stack_t *stack;12     uint32_t delay_ticks;13     uint32_t prio;14 }task_t;
  1. 修改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 }
  1. 增加接口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 }
  1. 还记得之前的又长又臭的task_sched吗,这次要推倒重来了,焕然一新的调度算法!上代码
    task.c
 49 void task_sched()50 {51     uint32_t status = task_enter_critical();52     task_t *temp_task_p;5354     if (g_sched_lock > 0) {55         goto no_need_sched;56     }5758     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     }6364 no_need_sched:65     task_exit_critical(status);66     return;67 }

这次代码的逻辑就非常简单了,获取最高优先级的任务,如果当前任务已经是最高优先级了,那什么也不做,否则就触发一次任务切换,将任务切换到更高优先级的任务去。

  1. 修改延时函数 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 }2728 void task2_entry(void *param)29 {3031     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                   09 #define OS_TASK_STATE_DELAYED               (1 << 1)1011 typedef uint32_t task_stack_t;12 /*Task Control block*/13 typedef struct task_tag {1415     task_stack_t *stack;16     uint32_t delay_ticks;17     uint32_t prio;1819     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();8990     /*  1.Add task to delay list91      *  2.Remove the task from task list92      *  3.Clear the task bit from prioity bitmap93      * */94     task_delay_wait(g_current_task, ticks);95     task_unready(g_current_task);9697     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        1013 typedef struct task_tag {1415     task_stack_t *stack;16     uint32_t delay_ticks;17     uint32_t prio;1819     list_node_t delay_node;20     uint32_t state;2122     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 }2728 void delay(uint32_t delay)29 {30     while(delay--);31 }3233 void task2_entry(void *param)34 {3536     for(;;) {37         printk("%s\n", __func__);38         delay(65536000);39     }40 }4142 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 {4243     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);
}

第三天将实现事件控制块和存储管理这一块相关代码。

这篇关于自制嵌入式操作系统 DAY2的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1027706

相关文章

自制的浏览器主页,可以是最简单的桌面应用,可以把它当成备忘录桌面应用

自制的浏览器主页,可以是最简单的桌面应用,可以把它当成备忘录桌面应用。如果你看不懂,请留言。 完整代码: <!DOCTYPE html><html lang="zh-CN"><head><meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0"><ti

自制HTML5游戏《贪吃蛇》

一、游戏简介         贪吃蛇是一款经典的电子游戏,最早在1976年由Gremlin公司推出,名为"Blockade"。游戏的玩法简单却富有挑战性,玩家控制一条蛇在封闭的场地内移动,通过吃食物增长身体,同时避免撞到自己的身体或场地边界。随着时间的推移,贪吃蛇游戏经历了多次演变,但其核心玩法依然受到玩家的喜爱。 二、为什么选择贪吃蛇游戏 经典性:贪吃蛇是一款历史悠久的游戏,其经典

easyx之图形库复习--自制写轮眼的图形绘制

引子效果图如下: 什么是easyx? EasyX 是针对 C/C++ 的图形库,可以帮助使用C/C++语言的程序员快速上手图形和游戏编程。比如,可以用 VC + EasyX 很快的用几何图形画一个房子,或者一辆移动的小车,可以编写俄罗斯方块、贪吃蛇、黑白棋等小游戏,可以练习图形学的各种算法,等等。 个人认为有点像api,web前段等功能 如何画(针对我效果图而言需要的而言)? 基础库

go语言day2

使用cmd 中的 go install ; go build 命令出现 go cannot find main module 错误怎么解决? go学习-问题记录(开发环境)go: cannot find main module; see ‘go help modules‘_go: no flags specified (see 'go help mod edit')-CSDN博客

疯狂刷题python版 | 使用PySide6自制刷题软件【源码+解析】

疯狂刷题python版 | 使用PySide6自制刷题软件【源码+解析】 一、前言二、思考三、软件设计四、软件实现(一)使用QWebEngineView控件通过JavaScript代码和chrome内核进行数据交互和逻辑控制(二)用户分别通过浏览器 GUI和PySide6 GUI进行操作(三)使用PySide6 GUI获取用户计算机本地资源 五、遇到问题及解决方案(一)如何把excel数据转

《30天自制操作系统》学习笔记——暂停

离上一篇笔记隔了挺长时间了。为什么呢?说实话我现在慢慢感受到这本书的局限了。为何?可以说,从第十五天开始吧,给操作系统引入了多任务。但是这个多任务是为何要这么实现,书上基本不讲。很多涉及的知识背后的原理,书上提及很少。甚至是很早前的GDT,为什么会有这样的设计?还有第21天的安全保护,到底怎么回事?说实话,因为一开始没有学习32位处理器的经历,看这些自然是莫名其妙的。前段时间看《L

自制调色小工具给图片加滤镜,修改图片红、绿、蓝通道及亮度,修改图片颜色

上篇:   上篇我们给地图添加了锐化、模糊等滤镜,这篇来写一个小工具给图片调色。 调色比锐化等滤镜要简单许多,直接拿到像素值修改即可。不需要用到卷积核。。。(*^▽^*) 核心原理就是图像结构,使用context.getImageData获取图像像素结构。 const imageData = context.getImageData(0, 0, canvas.width, ca

OS_Wanlix《嵌入式操作系统内核调度:底层开发者手册》

按照书上所讲,写了一份OS_Wanlix源码,给大家参考。 如果有需要,可以在评论中说明,会考虑写一个详细的代码说明 https://github.com/jklinCN/OS_Wanlix 运行结果:

典型嵌入式操作系统

当前,国际上存在上百种嵌入式操作系统。从来源看,一种是从通用操作系统演化而来的通用性嵌入式操作系统,如WinCE、Linux等,而大多数是面向特定领域嵌入式操作系统,下面列出了目前使用比较广泛的嵌入式操作系统产品。 1.嵌入式实时操作系统         嵌入式操作系统已被广泛应用于我们的日常生活之中,VRTX、VxWorks、LynxOS、Nucleus和QNX属于在嵌入式实

在树莓派上使用自制的USB声卡

若想知道USB声卡的制作过程,请看本人的另一篇博文:【基于PCM2912a的USB声卡设计】 基础条件: raspbian系统,我用的版本是2014-12-24-wheezy-raspbian.img,其他版本没测试。