UCOSII任务调度与同步、通信机制

前段时间总结了Linux下多线程同步机制。今天总结下,在实时操作系统UCOSII下是如何进行任务调度的,以及有哪些任务间的同步、通信机制。
插两句闲话,个人感觉,RTOS只有在对实时性强要求(例如控制类)的产品上才需要使用。物联网相关的嵌入式产品大部分都只需要一个良好的系统架构(可以进行快速开发、利于后期维护修改),该类产品其实并不需要使用RTOS

UCOSII下的任务调度(可以使用OS_Enter_Critical()和OS_Exit_Critical()使能和失能中断,来控制是否进行任务调度)

在Linux下(此处主要讨论pthread所创建的线程,也就是用户态线程),用户态线程的调度需要自己实现,Linux内核无法对用户态线程进行调度。
在UCOSII下,为了保证实时性,采用了高优先级抢占调度策略(注意:是高优先级抢占,而不是最高优先级抢占)。简单解释一下,也就是说高优先级任务无条件抢占CPU,如果高优先级任务一直未挂起,则低优先级任务将一直不会被执行,且在UCOSII中原则上不能创建两个同优先级的任务。UCOSII使用3种机制来保证高优先级任务(间接调用OS_Sched()进行任务调度)的执行

1、用户主动调用API函数。在用户任务中调用OSFlag_Pend()、OSMboxPend()、OSMutexPend()、OSQPend()、OSSemPend()、OSTaskSuspend()、OSTimeDly()以及OSTimeDlyHMSM()这几个API函数都将间接调用OS_Sched()函数,查找当前就绪表中优先级最高的任务(若当前任务优先级是最高的,且未阻塞挂起,则当前任务依然在就绪表中。若当前任务由于调用这些API阻塞挂起了,则当前任务会从就绪表中移除,再进行后续的查找工作),并调用OS_TASK_SW()函数,立马进行任务切换
在使用OSFlagPOST()、OSMboxPost()、OSMutexPost()、OSQPost()、和OSSemPost()等API时,若存在对应的等待(阻塞挂起)任务,则会将对应的任务加入就绪表,并立马进行任务调度,切换到此时就绪表中优先级最高的任务去运行(这一点与Linux的多线程调度机制不同,Linux需要等待当前任务时间片使用完)
上一张UCOSII下的任务状态图:

2、通过系统时钟(Systick)进行调度。每隔一个系统时钟间隔进行一次任务就绪状态检测,如果检测到有更高优先级的任务处于就绪态,则会进行任务切换。系统时钟间隔应当要适合,太长影响系统实时性,太短则会造成系统花费过多的资源处理定时器中断

3、通过触发外部中断程序进入系统调度。当发生了外部中断,造成系统任务就绪状态变化,在退出中断处理函数时,调用OSIntExit()函数。在这个函数中,如果检测到更高优先级任务处于就绪状态,则调用OSIntCtxSw()函数进行任务切换
注:OSIntEnter()和OSIntExit()是在中断服务程序中应该被调用的,前者用来告诉OS进入中断了,中断嵌套层数加1。后者用来告诉OS退出中断了,中断嵌套层数减1,并进行任务调度

UCOSII下的任务通信、同步机制

在Linux下因为线程间通信都是在应用层加之线程间共享内存空间,所以线程间一般都采用一段共享空间进行通讯,只要选择正确的同步方式保证共享空间操作的原子性即可。但在RTOS中由于涉及硬件和上层应用交互的需求,所以衍生出了更加丰富的通信手段,邮箱、消息队列等(当然这些手段也可以自己在Linux上实现)
下面上一张图,先简单总结下UCOSII下的任务通讯与同步机制

1、信号量(整数型型号量sem。可能导致优先级翻转问题,因为API内部不会暂时提升任务优先级):
建立信号量的工作,必须在任务启动(start)之前完成

  • 创建:
    OS_EVENT *OSSemCreate (INT16U cnt)
    cnt为初始值,可看做代表允许cnt个任务同时访问某资源。
    若没有可用的ECB则返回空指针

  • 等待信号量:
    OSSemPend (OS_EVENT *pevent, INT16U timeout, INT8U *perr)
    若运行到此处,pevent中的cnt为0,该任务将挂起等待信号量,直到该信号量给到此任务一个信号。若运行到此处,cnt非0,则将cnt减一后继续执行该任务
    timeout为超时时间,表示该任务进入等待信号量后的挂起时间,时间一到该任务将进入就绪态,若设为0则表示永久等待

  • 发送信号量:
    INT8U OSSemPost (OS_EVENT *pevent)
    调用此API首先会检测pevent信号量是否有被等待,若有被等待,则将等待列表中最高优先级的任务转变为就绪状态,接着会调用OSSched()函数进行任务调度,立即转到此时就绪态中系统优先级最高的任务去执行,若没有任务比当前任务优先级高,则继续执行该任务。若在调用此API时没有任务在等待该信号量,则该信号量的cnt加1

  • 放弃信号量等待:
    INT8U OSSemPendAbort (OS_EVENT *pevent, INT8U opt, INT8U *perr)
    无任务等待pevent信号量则继续执行该任务。若存在任务等待该信号量,且opt为OS_PEND_OPT_BROADCASE,则表示广播方式,释放所有等待该信号量的任务,使其全部变为就绪态,并进行任务调度。若opt为OS_PEND_OPT_NONE则表示将等待该信号量的任务中优先级最高的任务变为就绪态(放弃等待该信号量)并进行任务调度

  • 删除信号量:
    OS_EVENT *OSSemDel (OS_EVENT *pevent, INT8U opt, INT8U *perr)
    opt = OS_DEL_NO_PEND 若是没有等待信号量的任务,则删除该任务
    opt = OS_DEL_ALWAYS 不管有没有任务等待,始终删除。若有任务等待则会现将所有等待任务都更新为就绪态。此后再使用OS_Sched()进行任务调度,则会立即转到就绪态中更高优先级的任务去执行
    由于ECB控制块数量有限(默认为十个),所以不用的信号量应该尽快删除

  • 无等待的请求一个信号量:
    INT16U OSSemAccept (OS_EVENT *pevent)

  • 查询一个信号量的当前状态:
    INT8U OSSemQuery (OS_EVENT *pevent, OS_SEM_DATA *p_sem_data)

  • 重新设置信号量cnt值(一般不用):
    void OSSemSet (OS_EVENT *pevent, INT16U cnt, INT8U *perr)

2、互斥信号量(二值型型号量mutex。能解决任务优先级翻转问题,API内部暂时提升当然任务优先级):

  • 创建:
    OS_EVENT *OSMutexCreate (INT8U prio, INT8U *perr)
    prio:空闲的较高优先级(一般将最高优先级保留在此处使用),用于暂时提升任务优先级
    perr:存放出错的信息

  • 等待互斥型信号量:
    void OSMutexPend (OS_EVENT *pevent, INT16U timeout, INT8U *perr)
    pevent:等待的信号量
    timeout:等待超时时间,0值为永久等待
    perr:存放出错的信息

  • 释放互斥型信号量:
    INT8U OSMutexPost (OS_EVENT *pevent)
    pevent:释放的信号量

  • 无阻塞请求互斥型信号量:
    BOOLEAN OSMutexAccept (OS_EVENT *pevent, INT8U *perr)
    调用此API会立即返回,不会根据信号量的状态而阻塞。返回值0表示信号量不可用,返回1表示得到互斥型信号量
    pevent:请求的互斥型信号量
    perr:存放出错信息

  • 查询互斥型信号量当前状态:
    INT8U OSMutexQuery (OS_EVENT *pevent, OS_MUTEX_DATA *p_mutex_data)
    返回查询出错码
    pevent:需要查询的信号量
    p_mutex_data:存放查询到的信号量状态信息

3、事件标志组
预先设定好N个事件,当N个事件都达成时,才进入下一步操作

  • 创建:
    OS_FLAG_GRP *OSFlagCreate (OS_FLAGS flags, INT8U *perr)
    flags:创建事件标志中的初始值
    perr:存放出错信息
    创建完后才可使用此事件标志组

  • 等待事件标志组信号:
    OS_FLAGS OSFlagPend (OS_FLAG_GRP *pgrp, OS_FLAGS flags, INT8U wait_type, INT16U timeout, INT8U *perr)
    pgrp:某个事件标志组地址
    flags:过滤器,用于表示哪几位有效条件才成立。例如:(OS_FLAGS)3,表示请求低2位的信号
    wait_type:如何算有效。OS_FLAG_WAIT_CLR_ALL(过滤器所有位为0),OS_FLAG_WAIT_SET_ALL(过滤器所有位为1),OS_FLAG_WAIT_CLR_ANY(过滤器任意位为0),OS_FLAG_WAIT_SET_ANY(过滤器任意位为1)。若再加上OS_FLAG_CONSUME则表示有效后清除过滤位的信号(一般配合SET使用,不加此,若任务只需要一次信号,则可以不加此)
    timeout:超时时间,0表示永久不超时
    perr:存放出错信息

  • 向事件标志组发信号:
    OS_FLAGS OSFlagPost (OS_FLAG_GRP *pgrp, OS_FLAGS flags, INT8U opt, INT8U *perr)
    pgrp:某个事件标志组地址
    flags:选择需要发送的信号,例如:(OS_FLAGS)1给最低位发信号
    opt:选择发送置1还是置0信号,OS_FLAG_SET置1信号,flags位置1。OS_FLAG_CLR置0信号,flags位置0
    perr:存放出错信息

  • 不等待事件标志组(若过滤器事件并不成立也不挂起当前任务):
    OS_FLAGS OSFlagAccept (OS_FLAG_GRP *pgrp, OS_FLAGS flags, INT8U wait_type, INT8U *perr)
    pgrp:某个事件标志组地址
    flags:过滤器,用于表示哪几位有效条件才成立。例如:(OS_FLAGS)3,表示请求低2位的信号
    wait_type:如何算有效。OS_FLAG_WAIT_CLR_ALL(过滤器所有位为0),OS_FLAG_WAIT_SET_ALL(过滤器所有位为1),OS_FLAG_WAIT_CLR_ANY(过滤器任意位为0),OS_FLAG_WAIT_SET_ANY(过滤器任意位为1)。若再加上OS_FLAG_CONSUME则表示有效后清除过滤位的信号(一般配合SET使用,不加此,若任务只需要一次信号,则可以不加此)
    perr:存放出错信息

  • 删除事件标志组():
    OS_FLAG_GRP *OSFlagDel (OS_FLAG_GRP *pgrp, INT8U opt, INT8U *perr)
    pgrp:某个事件标志组地址
    opt:opt值为OS_DEL_NO_PEND表示当前若没有任务阻塞等待此标志组时,删除此标志组。若有任务等待则不做处理,继续运行当前任务。值为OS_DEL_ALWAYS表示不管当前标志组是否有任务等待,都删除该标志组。若有任务等待则会现将所有等待任务都更新为就绪态。此后再使用OS_Sched()进行任务调度,则会立即转到就绪态中更高优先级的任务去执行
    perr:存放出错信息

  • 查询事件标志组的当前事件标志状态:
    OS_FLAGS OSFlagQuery (OS_FLAG_GRP *pgrp, INT8U *perr)
    pgrp:某个事件标志组地址
    perr:存放出错信息

4、邮箱(可看做是信号量的升级版,可以实现一个任务向另一个任务发送一个指针变量)

  • 创建:
    OS_EVENT *OSMboxCreate (void *pmsg)
    pmsg:传入需要传递的消息指针,在创建邮箱的同时将此消息放入邮箱中,不想在创建时候就传入消息,或者只想将邮箱当成信号量来使用可以填入NULL

  • 等待邮件:
    void *OSMboxPend (OS_EVENT *pevent, INT16U timeout, INT8U *perr)
    此处需要强调,此API返回一个邮件指针,若指针非空则表示取出了邮箱中的消息
    若邮箱中没有邮件则此任务会被挂起,直到等待超时,或用了Post发出邮件(在post中会将等待列表中最高优先级的任务转为就绪态,并进行调度运行,但超时只会将任务转变为就绪态,并不进行调度)。
    pevent:邮箱地址
    timeout:等待超时时间,超时后任务将由挂起转为就绪态(但不立即进行调度),0值为永久等待
    perr:存放出错信息

  • 无等待查询邮件:
    void *OSMboxAccept (OS_EVENT *pevent)
    pevent:待查询的邮箱地址
    若存在邮件则返回邮件地址,若不存在邮件则继续运行,不挂起当前任务

  • 发送邮件:
    INT8U OSMboxPost (OS_EVENT *pevent, void *pmsg)
    pevent:邮箱地址
    pmsg:邮件地址(邮件地址,需要为一个固定地址,局部变量的地址可能导致BUG)

  • 放弃等待邮件:
    INT8U OSMboxPendAbort (OS_EVENT *pevent, INT8U opt, INT8U *perr)
    pevent:邮箱地址
    opt:值为OS_ERR_NONE,表示将等待任务列表中优先级最高的置为就绪态,放弃此次等待。OS_ERR_PEND_ABORT,表示所有等待任务都变为就绪态。最后都将进行任务调度,调用就绪态中最高优先级的任务去运行
    perr:存放出错信息

  • 查询一个邮箱的当前状态:
    INT8U OSMboxQuery (OS_EVENT *pevent, OS_MBOX_DATA *p_mbox_data)
    pevent:邮箱地址
    p_mbox_data:邮箱信息地址(邮箱信息将填入此地址中)

  • 删除邮箱:
    OS_EVENT *OSMboxDel (OS_EVENT *pevent, INT8U opt, INT8U *perr)
    pevent:邮箱地址
    opt:opt若为OS_DEL_NO_PEND表示若当前没有任务等待则删除此邮箱,若有任务等待则填写错误信息,当前任务继续运行。若为OS_DEL_ALWAYS则表示强制删除此邮箱,所有等待列表中的任务都将变为就绪态,并进行任务调度,运行就绪态中优先级最高的任务
    perr:存放出错信息

5、消息队列(可以看做是邮箱的升级版,可以实现从一个任务向另一个任务发送多个指针变量,也就是说用消息队列传递,在接收处可以用while全部接收完,而且每个指针所指向的数据结构变量也可以有所不同)

  • 创建(返回队列地址):
    OS_EVENT *OSQCreate (void **start, INT16U size)
    需要先定义一个存放消息指针的指针数组void *MyQueryMsg[SIZE]
    start:此处填入该指针数组的地址,用于存放消息指针
    size:填入队列所能存放数据的最大值
    例:myquery = OSQCreate(MyQueryMsg, SIZE);

  • 等待消息队列中的消息:
    void *OSQPend (OS_EVENT *pevent, INT16U timeout, INT8U *perr)
    返回msg的地址(根据POST的方式(FIFO或LIFO)来获得msg)
    pevent:消息队列地址
    timeout:等待超时时间,时间到后任务进入就绪态(但不立即进行任务调度)
    perr:存放出错信息

  • 无等待地从一个消息队列中取得消息:
    void *OSQAccept (OS_EVENT *pevent, INT8U *perr)
    pevent:消息队列地址
    perr:存放出错信息

  • 向消息队列发送一个消息(FIFO):
    INT8U OSQPost (OS_EVENT *pevent, void *pmsg)
    pevent:消息队列地址
    pmsg:需要发送的消息地址,注意不要使用临时变量的地址,极可能出现BUG

  • 向消息队列发送一个消息(后进先出LIFO):
    INT8U OSQPostFront (OS_EVENT *pevent, void *pmsg)
    同上

  • 清空一个消息队列:
    INT8U OSQFlush (OS_EVENT *pevent)
    pevent:消息队列地址

  • 查询一个消息队列状态:
    INT8U OSQQuery (OS_EVENT *pevent, OS_Q_DATA *p_q_data)
    pevent:消息队列地址
    p_q_data:存放消息队列信息的结构体变量

  • 删除一个消息队列:
    OS_EVENT *OSQDel (OS_EVENT *pevent, INT8U opt, INT8U *perr)
    pevent:消息队列地址
    opt:opt若为OS_DEL_NO_PEND表示若当前没有任务等待则删除此队列,若有任务等待则填写错误信息,当前任务继续运行。若为OS_DEL_ALWAYS则表示强制删除此队列,所有等待列表中的任务都将变为就绪态,并进行任务调度,运行就绪态中优先级最高的任务

利用闲暇时间封装的UCOSii事件库:
https://github.com/avalonLZ/Libraries/tree/master/C/ucosii