关于ucosii操作系统原理------(二)任务间通信

  在ucosii操作系统中,为了在各个任务之间可以发生数据交换以及通信,因此创造出了信号量,互斥量、邮箱、事件标志组以及消息队列等方法来完成任务间的数据交换以及通信,任务之间传递的这些不同种类的信号被统称为事件,与TCB类似,这里也定义了一个事件控制块(ECB)来管理各个事件。本章节主要是讲解各个任务间通信函数的源码是如何实现的。

一、OSTaskCreate()

  先来说说创建任务函数吧,这个函数功能是创建一个任务并且初始化这个任务。接下来看看它的源码,以下只粘贴出其关键源码,其余的比较简单就不过多介绍了。

    INT8U  OSTaskCreate (void   (*task)(void *p_arg),  void    *p_arg,OS_STK  *ptos,INT8U    prio)

首先第一个参数是一个函数指针,指向你所创建的任务,第二个参数是传递给任务的参数(我们在写代码时,这个参数一般是0,也就是NULL,后边再解释为什么是0),第三个参数是指向任务堆栈栈顶的指针,也就是任务堆栈指针。第四个参数是定义这个任务的优先级.

接下来我们任务创建函数里边包裹的两个关键函数

        psp = OSTaskStkInit(task, p_arg, ptos, 0u);  //这个函数是堆栈初始化函数,你打开它的源码你就会发现这个函数将CPU的所有寄存器的值保存在自己任务的栈里边,而这里的R1-R12都没有什么意义,用数字来表示只是为了方便调试。其他几个寄存器依次是xPSR = 0x01000000L,xPSR T 位(第 24 位)置 1,否则第一次执行任务时 Fault, PC 寄存器指向任务入口, R14 = 0xFFFFFFFEL,最低 4 位为 E,是一个非法值,主要目的是不让使用 R14,即任务是不能返回的。 R0 用于传递任务函数的参数,因此等于 p_arg,一般创建任务时候这个参数为0,是因为代表第一次做任务切换(可以看一下cpu_asm这个任务切换的汇编代码就懂了)。那为什么需要把这些寄存器保存在任务的栈里边呢?就是为了模仿程序被中断时候的保存断点数据的自动入栈操作(这里我上一章有讲),方便后续的任务切换。
   下边附上OSTaskStkinit()函数的源码:

      err = OS_TCBInit(prio, psp, (OS_STK *)0, 0u, 0u, (void *)0, 0u);//接下来的这行代码代表着任务的初始化,首先是将一个空TCB赋给这个任务,然后在这个TCB上填上这个任务的属性信息,具体信息可以看源代码。

二、OSTaskSuspend(prio)

  这个函数是挂起函数,表示可以挂起一个任务,挂起的意思就是说这个任务被挂起以后,这个任务将不会被调度。这个函数只有一个参数就是需要被挂起任务的优先级,那为什么它这里是优先级做参数,而不是需要被挂起的任务名做参数呢?这里我们就需要看一下这个函数的源码了,以下是这个函数的部分关键代码:

可以看到这个函数首先会检测需要挂起的任务是不是自身,如果需要挂起的是自身任务,那么会发生任务切换。如果需要挂起的是其他任务,看这行代码ptcb=OSTCBPrioTcb1[prio],这个数组是上一章节讲的数组用来为了更快的访问任务控制块,因此把已经被使用的任务控制块TCB都保存在这个数组里边,并且以各个任务的优先级为下标,这样就不用再去遍历链表,从而加快了其访问速度。因此,这里只需要用优先级来作为挂起函数的参数就足够了。

接下来看这两行代码: y            = ptcb->OSTCBY;
                                    OSRdyTbl[y] &= (OS_PRIO)~ptcb->OSTCBBitX;                                
                                    if (OSRdyTbl[y] == 0u) {
                                  OSRdyGrp &= (OS_PRIO)~ptcb->OSTCBBitY;}其中,OSTCBX/Y,OSTCBBITY/X都是为了快速访问就绪表的数据,这几行代码是为了清除这个任务的就绪标志位,也就是说将这个任务从就绪登记表里边清除掉,变成不可就绪,相当于一个有身份证的人被销户了。那么自然也不会被OS承认是任务,自然也就不会被OS调度。同理,OSTaskResume()函数也是类似的,这里就不多解释了。

三、OSSemCreate(cnt)

  接下来说一下信号量创建函数,信号量通常被用于等待一个资源事件的释放,一般我们都是定义一个ECB类型的指针变量,如sig=OSSemCreate(cnt)来创建一个信号量,但是这个函数里边的参数cnt却大有讲究,如果cnt=0,代表的是一个事件发生的次数,如果cnt==1,代表创建了一个互斥信号量(这里后边介绍OSSemPend()函数的时候会解释)。首先这个函数与OSTaskCreate()函数类似,也是将一个空事件控制块赋予给这个事件,并且把事件的各个属性填充进空事件控制块中完成初始化。关键源码如下:

从上述代码中可以看到,pevent就是你定义的信号量,首先把一个空的ECB赋给你定义的信号量事件,然后去判断此时的ECB是否为空,如果不为空的话,就将此时的ECB的下一空事件控制块赋给OSEventFreeList这个变量,保证下次这个变量代表着空ECB,注意这里是链表操作,OSEventPtr代表着下一个ECB。继续往下走,此时去检测这个信号量有没有被创建,如果创建成功并且拥有了事件控制块的话,此时将把各个事件属性登记在这个空的ECB上,最后OS_EventWaitListInit(pevent)函数代表将任务等待表中的每一位都清零,表示事件任务等待表中没有任何等待任务。

四、OSSemPost(OS_EVENT *pevent)

   这个函数用于释放一个信号量,当任务1需要等待一个信号量时,调用了这个函数的任务2释放这个信号量,只要任务1的优先级高于任务2,此时会发生任务切换到任务1中执行。那接下来我们看一下这个释放函数的部分源码:

首先OSEventGrp代表任务等待表中的各任务等待组是否有等待任务,如果有任务在等待pevent这个信号量,则利用OS_EventTaskRdy()将调用该函数的这个任务在任务等待表中的所有位置清零以后(任务等待表也是与任务登记表类似,只不过这里的1代表这一位对应的任务为事件的等待任务),再将任务在任务就绪表中对应位置置1,引发一次任务调度,此时即可发生任务切换(这里需要注意的是只有当等待信号量的任务优先级优先于释放信号量的任务,此时才会发生任务切换)。接下来一个关键点pevent->OSEventCnt++;这是与OSSemCreate(cnt)函数源码中pevent->OSEventCnt=cnt是对应起来的,也就是说如果你在创建这个信号量的时候,cnt设置为0,则就把0传给了OSSemPost()函数里边,然后再将这个cnt+1等于1;这里cnt在马上介绍的OSSemPend()函数里边有很大的作用。

五、OSSemPend(OS_EVENT *pevent.......)

  这个函数用于等待一个信号量,当调用这个函数时,如果上述第四节提到的信号计数量(cnt)不为0的话,此时在OSSemPend()函数中,会将cnt减1,然后函数正常返回,如果cnt为0的话,此时就会挂起调用了OSSemPend()函数的任务,直到超过时间期限或者等待到了信号量,那我们来看一下它的关键源码。

上述这段代码就是为了检测信号计数量的值是否为0,如果不为0,则减一,且函数正常返回。

上述这一段代码意思是如果cnt的值为0,那么就执行这段代码,此时首先将调用这个函数的当前运行任务的状态切换为信号量状态,然后将挂起状态设置为OK,接下来OS_EventTaskWait(pevent)这个函数的目的是为了挂起当前运行任务,操作也是将当前任务的就绪表清除让其不再就绪,接下来OS_Sched()函数发生任务切换。

紧接着上述代码是为了恢复当前任务的就绪状态,也就是恢复任务让其不再挂起。并且清除掉事件信号量,代表此时没有需要等待的事件。综上,这些代码基本上都是OSSemPend()函数的源码了,那为什么在这一个函数中当计数量cnt等于0的时候要先挂起任务,随后再恢复任务呢?这是因为要满足一个功能,那就是当cnt==0时,此时当前任务被挂起直到事件发生或者时间期限超过,那么想要实现这个功能就要在任务运行过程中,一次次的遍历这个函数内部,如果cnt==0,那就先挂起,再恢复,一直这样循环,如果再恢复完以后,此时再进行遍历这个函数时检测到cnt不为0,那么此时函数就正常返回。

六、OSMboxCreate(void *pmsg)

  接下来说一下邮箱,邮箱其实和信号量类似,邮箱也有释放,等待操作,只不过创建信号量时,其参数是信号计数量cnt,而邮箱的参数是消息指针pmsg。信号量是通过cnt初值是否为0来作为信号量的中转,而邮箱是通过是否接收到消息来作为中转,相比于信号量,邮箱还能在任务间传输数据。接下来我们来看看邮箱创建函数OSMboxCreate()函数,这个函数主要是为了创建一个邮箱并且进行初始化,与之前介绍的信号量创建函数类似,关键源码如下:

  上述代码首先也是将一个空的ECB赋给你所定义的邮箱变量,然后去判断此时的ECB是否赋给了你所定义的邮箱,如果是。此时更新ECB,使得下一次再次创建邮箱时,有空的ECB可用,接下来也是类似的操作,把事件的各个属性登记在这个空ECB上,最后初始化这个邮箱,让任务等待表中没有任何等待任务。Note:与信号量类似,如果创建邮箱函数的pmsg为NULL(0)的话,代表是通知事件的发生。如果pmsg不为空,也就是邮箱指针的初值指向特定的消息的话,此时邮箱当作互斥信号量使用。

七、OSMboxPost(OS_EVENT *pevent,void *pmsg)

   这是释放邮箱函数,与释放信号量类似,我们直接看源码:

首先也是去检测这个邮箱的等待任务组是否有任务在等待该邮箱消息,如果是,则将把调用释放邮箱函数的这个任务就绪,使其参与任务切换。接下来去判断这个邮箱的OSEventPtr成员,这个成员在创建邮箱或者信号量时代表的是下一个空ECB,在ECB中代表的是邮箱消息或者是队列消息指针,因此这里判断邮箱理是否存在消息,如果是,则函数返回,如果不是,就将你想要发送的pmsg参数赋给这个邮箱用于数据传输。

八、OSMboxPend(OS_EVENT *pevent...)

   等待邮箱函数,用于接收邮箱发送过来的数据,同时,如果没有接收到邮箱信息,调用该函数的任务挂起直到接收到消息或者超时,否则函数返回所接收到的数据消息。我们看下边源码:

与OSSemPend()函数类似,首先去判断邮箱消息是否为空,如果不为空,则返回所接收到邮箱消息,并且将其清空用于下一次使用。如果邮箱消息为空,挂起调用这个函数的任务直到超时或者事件发生。

上述代码与OSSemPend()函数一样的操作,这里就不再多说了。

九、OSMutexCreate(u8 prio,u8 *perr)

   在前边几节我已经提到过,其实二值信号量就是互斥量,代表着对资源的独占访问。我们在介绍这个互斥量创建函数的源码之前,我们先来说一下互斥量中任务优先级反转的问题。那这个问题是怎么产生的呢?

   就是如果有三个任务123,2的优先级在中间,任务1优先级最高,任务3优先级最低,那么1和3呢想实现对共享资源的访问,而任务2不需要访问这个资源;然后此时假如1想要访问共享资源,但是呢现在3正在访问共享资源,因此此时任务1只能等着任务3用完资源以后等待着信号量的到来,但是3在访问共享资源的时候呢又被2打断,此时就去执行2去了,这样会导致任务堵塞,让最高优先级的任务1迟迟访问不了该资源。现在怎么来解决这个问题呢?如果现在1在等待3释放信号量,那么此时会暂时将3的优先级调到比1还高的优先级(0),并且调度3运行,当3访问完共享资源并释放了信号量之后,此时才会恢复任务3的优先级,此时就会立马发生任务切换,让任务1来访问这个共享资源,从而任务1就不会被其他中等优先级的任务所堵塞。而上述这个过程就叫做任务优先级反转。

   接下来我们开始讲解这个函数的源码,首先这个函数的参数里边有两个参数,第一个参数是优先级,这个优先级代表着需要提升的优先级别,这也是为什么一开始我们要先讲解任务优先级反转的原因。第二个参数是错误状态。接下来我们看关键源码:

  首先还是老样子,pevent是一个指针变量,指向你所定义的互斥信号量,然后把一个空ECB赋给你所定义的信号量,接下来用一个if函数检测赋给你所定义的信号量的ECB是否可用,如果为0,也就是NULL,代表着这个ECB不可用,因此,会将你需要提升优先级为下标的TCB指针释放,并返回错误信息。如果可用,就更新ECB用于后续的互斥信号量创建时有新的空ECB可用。接下来将事件的类型填入这个空ECB,然后这里需要注意的是OSEventCnt(信号量计数值),首先将cnt的高8位用来保存需要提升的优先级,低8位用来保存一个常数OS_MUTEX_AVAILABLE,这个常数的值是0x00FF,这个常数代表着信号值只有为0xff的时候有效,否则无效。这个信号值是用来表明你所创建的信号量没有被任何任务所占用,处于有效状态。最后,还是利用OS_EventWaitListInit(pevent)来把任务等待表清零,表示没有任务等待该信号量。

十、OSMutexPost(OS_EVENT *pevent)

   这个函数和前边几节讲的类似,作用是释放一个互斥信号量。接下里看这个函数的关键源码,如下所示:

  首先第一行代码将信号计数量成员右移8位,只保留高8位,说明这行代码的主要作用是将提升的优先级别提取出来。第二行代码也是同样的道理,OS_MUTEX_KEEP_LOWER的值是0x00ff,因此相与之后得到低8位,所以这行代码主要是将任务的初始优先级提取出来。接下来就是判断当前任务OSTCBCur是不是为包含有互斥信号量的任务注意在这里OSEventPtr代表的是指向当前持有互斥信号量任务的TCB指针。如果不是,则返回犯错信息。如果是,就接下来判断当前任务的原始优先级是否被提升了优先级,如果是,就调用OSMUTEX_RdyAtPrio(OSTCB,prio)恢复原始任务的优先级。如果当前任务的原始优先级没有被提升,接下来去判断当前是否有任务在等待该互斥信号量,OSeventGrp为0代表没有任务等待,否则有任务等待。如果察觉有任务等待互斥信号量,则将等待任务表中优先级最高的任务调成就绪状态,引发任务调度,并把等待任务表清0。接下来就更新信号量计数值,将信号量计数值更新为原始状态,恢复原来任务的原始优先级。下边检查调用了OSmutexPend()函数的任务的原始优先级是否小于等于所要提升的优先级别,如果是,返回错误信息,因为需要提升的优先级别需要最高,因此值得最小。最后,如果察觉没有任务在等待这个互斥信号量的话,将信号量计数值更新为可用状态,表示没有任务在占用该互斥信号量。

十一、OSMutexPend(OSEVENT *pevent,timeout,.....)

    接下来介绍等待一个互斥信号量函数的源码,如果信号量计数值(cnt)的低8位为0xff,代表这个互斥量信号没有被占用,处于可用状态。于是任务就可以继续访问共享资源。否则就挂起等待其他任务访问完共享资源释放互斥信号量。其源码如下:

上述代码首先是获得需要提升的优先级存在pip中,然后检测这个互斥信号量是否可用,如果可用的话,就将当前任务的优先级保存起来,再将当前任务表示成已经含有互斥信号量的任务。然后去判断当前任务的优先级是否需要提升。然后函数返回。

上述代码表示如果这个互斥信号量不可用,被占用的话。就将互斥量持有者(也就是调用OSMutexPend函数的任务)的原始优先级保存在mprio中。然后将互斥信号量任务TCB指针赋给ptcb。接下来检查是否需要提升优先级。如果需要,就将互斥量持有者的优先级提升预防优先级反转,后续的操作和前几节pend函数的一样,不多解释了。

十二、OSQCreate(void **start,u16 size)

  接下来我们介绍消息队列创建函数,在介绍这个函数的源码之前,先介绍一下消息队列的相关知识。消息队列相当于是邮箱的加强版,邮箱只能发送一条消息,但是消息队列可以发送多条消息。其数据结构如下图所示:

从图中可以看到,事件块成员OSEventPtr指向一个消息队列控制块OSQ,这个控制块管理着一个指针数组MsgTb1[],其中队列控制块的结构如下:

   其中,队列控制块中的指针OSQPtr将所有队列控制块链接成链表。start代表消息指针数组的起始地址,end就代表结束地址,size代表消息指针数组的长度,Entries代表着存放消息指针的元素数目,OSQin代表的是指向一条插入消息的位置的指针,OSQout代表的是取出消息的位置的指针。这两个指针也叫做可移动指针,当移动指针in或者out移动到end的时候,可移动指针变成start,也就是说,start指针与end指针是等值的。这样消息指针数组就头尾相连了,效果如下图所示:

这里需注意,有两种方式往消息指针数组里边插入消息,分别是后进先出LIFO以及先进先出FIFO方式,用FIFO时,在in指针插入消息,用out指针输出消息。如果是后进先出方式,那么插入消息、取出消息都用指针out。

  接下来我们来说一下消息队列的创建函数关键源码:先来说一下这个函数的第一个参数void **start,很明显这是一个指向指针的指针,这是因为这个参数代表的是指针数组的起始地址,首先指针数组里边全是指针,而OSQstart这个指针指向这个指针数组,那就是一个二维指针,因此这里采用了指向指针的指针来定义这个参数类型。关键源码如下:

首先是获得一个空ECB赋给pevent变量,然后去判断这个空ECB是否被用了,如果被占用了,则更新FreeList为空ECB。接下来再检测一下pevent这个变量是否成功获得了空ECB,如果是,则将一个消息队列控制块OSQ赋值给变量pq,再去检测是否成功获得空OSQ,如果是,就填充空消息队列控制块的各个属性,最后将等待任务表清零,表示没有任务再等待这个消息队列。如果pq这个变量没有成功获得空消息队列控制块。则返回所得到的ECB。

十三、OSQPost(*pevent,*pmsg)

     

    该函数是发送消息函数,函数内部的两个参数一个是你创建的消息队列指针,一个是消息指针。注意这里OSQPost()函数是以先进先出的方式组织消息队列。接下来看其函数的关键源码:

     首先是判断这个消息队列是否在被其他任务所等待,如果任务等待表没有被清零的话,代表有任务在等待该消息队列。如果存在等待情况,就去寻找最高的优先级就绪任务,准备发生任务切换。接下来将一个消息队列控制块赋给pq变量,接下去判断这个OSQ控制块中消息数组中的元素数目是否大于这个消息指针数组总长度,如果是的话,返回这个消息队列已满。如果确保这个队列空间充足的话,将pmsg这个消息参数插入这个消息数组中,然后OSQIN+1,指向下一个要插入消息的位置,同时消息数组内的元素数目加1用于更新消息数组的实时数目;当OSQin这个移动指针移动到消息数组的结尾的时候,将OSQIn这个移动指针更新为消息数组的开始地址,进行新一轮的循环,然后函数返回。

十四、OSQPostFront(*pevent,*pmsg)

    这个函数与OSQPost()函数一样,都是用于发送消息,但不同的是,它是以后进先出的方式组织消息队列;它的源码如下:

     可以看到,它的源码与OSQPost()函数的源码几乎是一样的,只是将OSQIn变成了OSQout,正如一开始所说的,用先进先出方式LIFO方式时,out为输出消息,In为插入消息,而用FIFO方式时,插入消息和取出消息都用out。

     如果任务希望以广播的方式通过消息队列发送消息,则需要调用函数OSQPostOpt(pevent,msg,opt),调用这个函数发送消息时,如果参数opt的值是BRODCAST时,代表所有等待该消息队列的所有任务都会收到消息。

十五、OSQPend()

      该函数用于等待一个消息队列,如果等到消息传来,就从中提取消息返回,否则挂起任务。该函数的关键源码如下:

     从该源码片段可以看到,先去判断消息控制块中是否有消息可用,如果消息数目不为0的话,那么就将消息取出来,而这个取出消息指针上述也说过是通过out指针来完成的,且这定义的是一个指向指针的指针,因此需要在结构体指针前边再加*;接下来让out指针向后移动,指向下一个需要取出来的消息;然后将消息数目减一,表示队列中的消息少了一个。紧接着判断out指针是否到了结尾,如果取完了,则将out指针重新指向开始指针,开启下一次循环。最后返回取出来的消息;如果消息控制块中没有消息可用,则挂起该任务,并引发一次调度,直到超时或者查询到有消息数目。

  • 15
    点赞
  • 28
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值