自制嵌入式操作系统 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

相关文章

Java预备知识 - day2

1.IDEA的简单使用与介绍 1.1 IDEA的项目工程介绍 Day2_0904:项目名称 E:\0_code\Day2_0904:表示当前项目所在路径 .idea:idea软件自动生成的文件夹,最好不要动 src:src==sourse→源,我们的源代码就放在这个文件夹之内 Day2_0904.iml:也是自动生成的文件,不要动 External Libraries:外部库 我这

github有趣项目:renpy自制“剧情游戏”

之前的剧情游戏《完蛋!我被美女包围了》很是火热,一度登上Steam热销榜第一。Ren’Py(https://github.com/renpy/renpy) 是一个可视小说引擎,可以快速方便的制作类似剧情游戏。它是一个免费的游戏引擎,支持多端运行打包。支持3D镜头移动(是对于二维堆叠图像的,好像还不支持三维模型),Live2D等功能。支持的音频格式:Opus、Ogg Vorbis、MP3、MP2、F

【为项目做准备】Linux操作系统day2

这两天学校的事情总压着,day2拖了好几天..day2内容是进程数据结构 进程数据结构信号处理任务状态进程调度运行统计信息进程亲缘关系进程权限用户和组标识符(IDs)linux capabilities 内存管理文件与文件系统用户态与内核态用户态与内核态的转换函数调用栈内核栈和task_struct的关系 进程数据结构 进程or线程,在内核中,统一叫任务(Task),由一个统

Android智能家居实训day2

设置使用的布局文件 setContentView(R.layout.filename);,之后使用布局嵌套,一个布局内部可以嵌套另一个布局,内部的布局相当于外部布局的一个子控件,可以把它当作一个整体来操作,例如在今天的八宫格使用布局嵌套的时候,每一个格子是一个线性布局布局内使用垂直方向,而每两个布局作为一行,一共四行,这样就再拿一个布局框起来使用水平方向最后再把这四个布局用垂直方向。在布局之间分配

自制efficientnet网络

用到的技术cnn,残差连接,全局池化注意力机制,点卷积切换通道,深度卷积提取空间特征 import os os.environ["KERAS_BACKEND"] = "tensorflow"  # @param ["tensorflow", "jax", "torch"] os.environ['TF_CPP_MIN_LOG_LEVEL'] = '2' import matplotlib.pyp

【读书笔记-《30天自制操作系统》-14】Day15

本篇内容开始讲解多任务。本篇内容结构很简单,先讲解任务切换的原理,再讲解任务切换的代码实践。但是涉及到的知识不少,理解上也有些难度。 1. 任务切换与多任务原理 1.1 多任务与任务切换 所谓多任务,指的是操作系统同时运行多个任务。但是这种说法实际上是不准确的。如果只有一个CPU,是无法事实上实现同时运行多个任务的。而之所以给用户以多个任务在同时运行的错觉,其实是因为多个任务之间在快速地

算法训练营——day2数组部分例题

1 移除元素-力扣27(简单) 1.1 题目: 移除元素1 1.2 思路及解法 只能覆盖,不能删除 暴力遍历解法 class Solution {public int removeElement(int[] nums, int val) {int size=nums.length;for(int i=0;i<size;i++){if(nums[i]==val){for(int j=

【读书笔记-《30天自制操作系统》-13】Day14

相比前几篇的内容,本篇不仅内容更为简单,而且与显示相关,更为有趣。首先通过调用VBE的显示模式提高显示画面的分辨率,然后分别实现按下键盘按键显示对应的字符,以及通过鼠标移动窗口。因为是以前面讲过的很多内容为基础,程序代码很简单,而且能切实看到成果,也更有趣。 1. 提高画面分辨率 现在要把显示画面的分辨率提高到640x480,就又要修改BIOS的画面模式设定所用的汇编语言代码了。 ; 设定

自制实战吃鸡手柄原理

在前面的讨论中,通过类似物理点击的方式来实现了声控触发射击键的点击 【Arduino】自制声控点击器(吼叫吃鸡助手)_辅助机械臂物理物理键盘点击器神器-CSDN博客 为了更有实战效果,我们可以把玩具枪改造为一个手柄,这样操作更加方便, 枪形手柄使用蓝牙来进行连接,发送操作指令。还可以安装一个陀螺仪传感器在上面,控制画面的转动,瞄准键也可以设置一个按钮。 有2种方案来进行控制, 1

使用自制COCO数据集进行PaddleDetection模型训练

本次模型训练基于百度飞浆的Baseline: 19届智能车百度创意组识别 - 飞桨AI Studio星河社区 (baidu.com) 一、收集数据及数据处理 用摄像头拍摄实物,这里先选用baseline中给好的数据集。创建VOC文件夹,文件夹里包含Annotations和JPEGImages两个文件夹。需要进行标注操作的图片将会放在JPEGImages文件夹里,标注生成的xml文件将会放