本文简要介绍了UCOSIII的多任务系统,可以作为操作系统的入门介绍。

操作系统介绍

轮询系统

轮询系统即是在裸机编程的时候,先初始化好相关的硬件,然后让主程序在一个死循环里面不断循环,顺序地做各种事情,通常只适用于那些只需要顺序执行代码且不需要外部事件来驱动就能完成的事情 缺点:如果加入按键操作等需要检测外部信号的事件,整个系统的实时响应能力就会体现不好。试想一下,但按键按下时,程序正在运行顺序1程序,而且顺序1程序占用的程序时间片比较长,系统就有可能错过对按键的检测(直到按键松开),实时性极差,用户无法接受。



前后台系统

相比轮询系统,前后台系统就是在轮询系统的基础上加入了中断。外部事件的响应在中断里面完成,事件的处理还是回到轮询系统中完成,中断在这里我们称为前台,main()函数里面的无限循环我们称为后台。
在顺序执行后台程序时,如果有中断,那么中断会打断后台程序的正常执行流,转而去执行中断服务程序,在中断服务程序中标记事件。如果事件要处理的事情很简短,则可在中断服务程序里面处理,如果事件要处理的事情比较多,则返回后台程序处理。通过中断可以大大提供程序的实时响应能力,避免造成外部事件的丢失。



uCOSⅢ多任务系统基础

什么是任务

在裸机系统中,系统的主体就是main()函数里面顺序执行的无限循环,这个无限循环里面CPU按照顺序完成各种事情。在多任务系统中,我们根据功能的不同,把整个系统分割成一个个独立的且无法返回的函数,这个函数我们称为任务,也有人称之为线程。 ###任务状态管理 任务的5种状态: ①睡眠状态:任务只以代码的形式驻留在程序空间,还没有交给操作系统管理时的情况 ②就绪状态:系统为任务配备了任务控制块且在任务就绪表中进行了就绪登记 ③运行状态:处于就绪状态的任务经过调度获得了CPU的使用权 ④等待状态:正在运行的任务,需要等待一段时间或者需要等待一个事件发生在运行的时候为等待状态 ⑤中断服务状态:一个正在运行的任务一旦响应中断申请就会中止运行而去执行中断服务程序



任务堆栈

任务如何才能切换回上一个任务并且还能继续从上次被中断的地方开始运行呢?那就要恢复现场,现场就是CPU的内部各个寄存器。在刚创建一个新任务时,就必须把系统启动这个任务时所需的CPU各个寄存器的初始值事先存放在任务堆栈中。这样当任务获得CPU使用权时,就把任务堆栈的内容复制到CPU的各个寄存器,从而可以让任务顺利的启动并运行。 ### 任务控制块
任务控制块就相当于任务的身份证,里面存有任务的所有信息,比如任务的栈,任务名称,任务的形参等。OS_TCB是一个结构体,描述了任务控制块,任务控制块中的成员变量用户不能直接访问,更不可能改变他们,在os.h中定义。 ### 任务创建函数
任务的栈,任务的函数实体,任务的TCB最终需要联系起来才能由系统进行统一调度。那么这个联系工作就由任务创建函数OSTaskCreate来实现。该函数在os_task.c中实现。





任务时间管理

uCOSIII中的任务是一个无限循环并且还是一个抢占式内核,为了使高优先级的任务不至于独占CPU,可以给其他优先级较低任务获取CPU使用权的机会,uCOSIII中除空闲任务外的所有任务必须在合适的位置调用系统提供的延时函数或者任务调度函数,让当前的任务暂停运行一段时间并进行一个任务切换。
   延时函数有两种,OSTimeDly()—指定节拍数
和OSTimeDlyHMSM()—参数直观。
   OSTimeDly()函数有三种工作模式:相对模式、周期模式和绝对模式。
   延时任务任务可通过在其他任务中调用函数OSTimeDlyResume()取消延时而进入就绪状态,此函数最后会引发一次任务调度。
   OSTimeDlyHMSM()函数仅在相对模式下工作。

uCOSⅢ多任务系统进阶

时间片轮转调度器

 UCOSIII允许一个优先级下有多个任务,每个任务可以执行指定的时间(时间片),然后轮到下一个任务,这个过程就是时间片轮转调度,当一个任务不想在运行的时候就可以放弃其时间片。 如果某一优先级下有多个任务话,这些任务是如何被调度和运行的:先判断该任务的时间片是否有剩余,如果有就直接返回不切换;如果没有,就进行切换。每次任务切换后运行的都是处于就绪任务列表OSRdyList[ ]链表头的任务,当这个任务的时间片用完后这个任务就会被放到链表尾,然后再运行新的链表头的任务。
 时间片轮转调度器用于时间片轮转调度,为函数OS_SchedRoundRobin(),此函数由OSTimeTick或者OS_IntQTask()调用,函数在文件os_core.c中定义。
 相关API函数
配置函数:OSSchedRoundRobinCfg(DEF_ENABLED, 1, &err);
放弃本次时间片:OSSchedRoundRobinYield(OS_ERR *p_err); ### 系统内部任务



中断服务管理任务OS_IntQTask()

当把os_cfg.h文件中的宏OS_CFG_ISR_POST_DEFERRED_EN置1就会使能中断服务管理任务,UCOSIII会创建一个名为OS_IntQTask()的任务,该任务负责“延迟”在ISR中调用的系统post服务函数的行为。中断服务管理任务的任务优先级永远是最高的,为0!

时钟节拍任务OS_TickTask()

时钟节拍任务的作用是跟踪正在延时的任务,以及在指定时间内等待某个内核对象的任务,时钟节拍任务的优先级尽可能的高一点,一般设置时钟节拍任务的任务优先级为1。 ### 定时任务OS_TmrTask()   UCOSIII提供软件定时器功能,定时任务是可选的,将宏OS_CFG_TMR_EN设置为1就会使能定时任务,在OSInit()中将会调用函数OS_TmrInit()来创建定时任务。定时任务的优先级通过宏OS_CFG_TMR_TASK_PRIO定义,一般将定时器任务优先级设置为2。 ### 统计任务OS_StatTask()   在UCOSIII中统计任务可用来统计CPU的使用率、各个任务的CPU使用率和各任务的堆栈使用情况,默认情况下统计任务是不会创建的,如果要使能统计任务的话需要将宏OS_CFG_STAT_TASK_EN置1,宏OS_CFG_STAT_TASK_EN在os_cfg.h文件中有定义。



空闲任务OS_IdleTask()

任务OS_IdleTask()是必须创建的,不过不需要手动创建,在调用OS_Init()初始化UCOS的时候就会被创建。
空闲任务特点:
空闲任务是UCOSIII创建的第一个任务;
空闲任务是UCOSIII必须创建的;
空闲任务优先级总是为OS_CFG_PRIO_MAK-1;
空闲任务中不能调用任何可使空闲任务进入等待态的函数! ## 信号量和互斥信号量 ### 信号量
信号量像是一种上锁机制,代码必须获得对应的钥匙才能继续执行,一旦获得了钥匙,也就意味着该任务具有进入被锁部分代码的权限。一旦执行至被锁代码段,则任务一直等待,直到对应被锁部分代码的钥匙被再次释放才能继续执行。
信号量用于控制对共享资源的保护,但是现在基本用来做任务同步用。
要想获取资源的任务必须执行“等待”操作,如果该资源对应的信号量有效值大于1,则任务可以获得该资源,任务继续运行。如果该信号量的有效值为0,则任务加入等待信号量的任务表中。如果等待时间超过某一个设定值,该信号量仍然没有被释放掉,则等待信号量的任务就进入就绪态,如果将等待时间设置为0的话任务就将一直等待该信号量。
信号量通常分为两种:二进制信号量和计数型信号量
二进制信号量只能取0和1两个值,计数型信号量的信号量值大于1,计数型信号量的范围由OS_SEM_CTR决定,OS_SEM_CTR可以为8位,16位和32位,取值范围分别为:0~255,0~65535和0~4294967295。
二值信号量用于那些一次只能一个任务使用的资源,比如I/O设备,打印机。计数型信号量用于某些资源可以同时被几个任务所使用,比如一个缓存池有10个缓存块,那么同时最多可以支持10个任务来使用内存池。 ### 优先级反转
优先级反转在可剥夺内核中是非常常见的,在实时系统中不允许出现这种现象,这样会破坏任务的预期顺序,可能会导致严重的后果,下图就是一个优先级反转的例子:



互斥信号量

遵循解决优先级反转的一般解决思路,UCOSIII支持一种特殊的二进制信号量:互斥信号量,用它可以解决优先级反转问题:







消息传递

任务间通信

一个任务或者中断服务程序有时候需要和另一个任务交流信息,这个就是消息传递的过程就叫做任务间通信,任务间的消息传递可以通过两种途径:一是通过全局变量,二是通过发布消息。使用全局变量的时候,每个任务或者中断服务程序都必须保证其对全局变量的独占访问(通常的解决办法:关中断、临界区、信号量)。消息也可以通过消息队列作为中介发布给任务。 ### 消息队列
消息一般包含:指向数据的指针,表明数据长度的变量和记录消息发布时刻的时间戳,指针指向的可以是一块数据区或者甚至是一个函数,消息的内容必须一直保持可见性,因为发布数据采用的是引用传递是指针传递而不是值传递,也就说,发布的数据本身不产生数据拷贝。也就是说:我们可以使用动态内存分配的方式来给消息分配一个内存块,或者,也可以传递一个指向全局变量、全局数据结构、全局数组或者函数的指针。不能使用局部变量作为消息来进行传递!!
     在UCOSII中有消息邮箱和消息队列,但是在UCOSIII中只有消息队列

存储管理

作为一个RTOS操作系统,内存管理是必备的功能,因此UCOSIII也就内存管理能力。通常应用程序可以调用ANSI C编译器的malloc()和free()函数来动态的分配和释放内存,但是在嵌入式事实操作系统中最好不要这么做,多次这样的操作会把原来很大的一块连续存储区域逐渐地分割成许多非常小并且彼此不相邻的存储区域,这就是存储碎片。存储碎片最终导致的结果就是,应用不能申请到大小合适的连续内存。UCOSIII中提供了一种替代malloc()和free()函数的方法,它提供了自己的动态内存方案。UCOIII将存储空间分成区和块,一个存储区有数个固定大小的块组成。