一篇讲透嵌入式操作系统任务调度

进互联网公司操作系统和网络库是基础技能,面试过不去的看,这里基于嵌入式操作系统分几章来总结一下任务调度、内存分配和网络协议栈的基础原理和代码实现。

处理器上电时会产生一个复位中断,接下来会执行复位中断服务函数,这才是软件执行的起始点。复位函数先后调用SystemInit和__main函数,SystemInit是处理器自带的库函数,一般执行各种时钟和外设的初始化;__main函数执行C语言运行环境的初始化,包括将目标程序从flash搬运到RAM、初始化全局变量等内存段初值,初始化C语言库函数等操作,最后跳转到main函数,执行用户程序。不同型号的处理器启动流程不太一致,如果不是需要设计bootloader,可以没必要关心。

mian的主要工作大致为:内存初始化、硬件中断初始化,此外还会分配基础的资源如锁、信号量等,最后创建idletask。idletask任务优先级最低,里面一般循环执行WFI指令使芯片保持低功耗状态。

任务相关结构初始化很简单,主要是分配任务块空间,并挂接在freelist链表上。后续分配任务时,从freelist链表上取就行。

                           

一篇讲透嵌入式操作系统任务调度_第1张图片


接下来看看任务创建的步骤:

首先将需要回收的任务资源挂在freetasklist上去。这里是尽量延迟了任务的回收时间,不是在任务该回收的时候而是在新任务创建的时候回收旧任务。这样可以尽量减少CPU占用。所以每次新建任务,都是从freetasklist链表上取一个TCB下来然后根据用户需求分配栈大小,设置任务优先级和入口函数等。这里要注意操作freetasklist的过程中要关闭中断,防止操作过程中来了中断引起任务调度,导致crash。这里写代码需要注意因为参数检查、分配内存等流程都有可能出错,在设计程序的时候最好统一收口,所有错误有一个统一的出口处理,这样可以防止遗忘开中断等重要操作。大致流程如下:

一篇讲透嵌入式操作系统任务调度_第2张图片

从freelist头部摘取一个任务块,并分配任务栈空间、设置优先级等,新创建的任务需要挂接到priqueue链表数组中等待任务调度:

一篇讲透嵌入式操作系统任务调度_第3张图片

这里有个小知识点,一般的链表指向的都是结构体的首部,这样可以将链表指针直接强制转换成对应的数据结构(这里是任务块)。而在任务队列中链表位于任务块结构体中间,需要用宏来获取到链表指针对应的任务块首地址,这个宏的实现大同小异,各个操作系统都是借鉴Linux来实现的,详情百度:list_entry。

接下来就要说说操作系统是怎么做到常数级的任务切换时间的。初学者写的操作系统任务调度功能时可能会出现一种场景,那就是任务量少的时候任务切换时间快,任务量多的时候任务切换时间变慢,这种任务切换耗时不确定对用户任务影响时非常大的。我们来看看ucos系统是如何设计来保证常数级任务切换时间的。

一个全局的priqueue链表数组,上面挂接ready状态的任务队列,一个全局int32数,用于标记某优先级是否有需要调度的队列。要扩展到128位优先级也非常方便,设置4个int32数组即可。每次取优先级最高的任务,直接用CLZ汇编命令从bitmap中读出需要调度的最高优先级任务。

一篇讲透嵌入式操作系统任务调度_第4张图片

插入ready任务的时候需要将bitmap对应位置1,任务从ready状态移除的时候需要检查该优先级是否还有需要调度的任务,如果没有了,bitmap对应位需要置0。讲完嵌入式操作系统的进程调度,再来看看Linux的CFS的基础原理,就好理解多了,嵌入式系统低优先级队列可能会存在饿死现象,Linux的CFS调度算法给每个优先级分配了不同权重,根据就绪队列里所有任务的权重之和来分配任务的时间,来保证完全公平调度。

内存分配

前面分配任务块、分配任务栈等都用到了内存分配动作,具体的内存分配算法有:best-fit算法、TLSF算法、LWIP中的最快匹配算法、伙伴算法等,基础原理类似,下期再分析。

时钟中断

适配操作系统首要任务就是把时钟tick给起起来,这是芯片的心跳,所有的任务切换和延时操作都是基于时钟tick中断驱动的。

以ARM芯片的Cortex-M3核为例,启动时钟中断主要是调用osSetVector将tick回调函数设置进中断向量表里面的15号中断:

中断向量表长这样子:

一篇讲透嵌入式操作系统任务调度_第5张图片

前面15个中断号属于系统中断,后面预留中断号可供用户配置,M3是240个,M0是32个。

结合PendSV中断,可以在tick中断中完成别的事物(如定时器处理等),通过低优先级的PendSV中断来执行任务切换动作,从而减少中断响应时间。具体的分析在之前文章中:嵌入式操作系统的任务调度

一篇讲透嵌入式操作系统任务调度_第6张图片

在设置tick中断的时候还需要配置systick定时器的中断间隔。systick定时器是个简单的向下计数的24位计数器,寄存器位置位于系统控制空间基址SysTick_BASE偏移16字节,将需要的频率数值写入即可。systick寄存器的内部细节为:

一篇讲透嵌入式操作系统任务调度_第7张图片

详情可参考《ARM Cortex-M3 Cortex-M4权威指南》9.5节systick相关部分,其余相关linux中断知识可参考:Linux中断编程

顺带说一下,如果职业规划是往互联网方向转,那么寄存器硬件知识了解即可。如果想在物联网嵌入式领域深耕,不同ARM芯片之间的区别是一定要掌握的。

我们可以给tick中断配置为每10ms中断一次,防止过多的任务上下文切换占用CPU资源。中断到来后系统都要处理哪些事务呢?通常情况下会维护一个全局计数器,该中断到来时变量自增,然后会处理定时器任务和超时任务。

超时任务是什么呢?比如用户任务等待某个资源(锁、信号量等),如果获取不到就会设置超时时间阻塞等待,直到资源可用或者任务超时。因此每次时钟中断到来都需要判断是否有任务超时。

定时器任务就比较简单了,可以使用全局链表有序挂接定时任务,每次只需要判断链表头的任务是否到时,到时了摘取下来执行对应的回调函数即可。

不同的操作系统可能会在tick中断里面做一些别的事情,比如定时器对齐等。

貌似还有很多没写完,下周末再来总结。最后推荐嵌入式操作系统入门的三本书:《嵌入式操作系统内核调度--底层开发者手册》、《嵌入式网络那些事--STM32物联实战》、《ARM Cortex-M3 Cortex-M4权威指南》

一篇讲透嵌入式操作系统任务调度_第8张图片

你可能感兴趣的:(一篇讲透嵌入式操作系统任务调度)