• 手把手,嘴对嘴,讲解UCOSII嵌入式操作系统的任务调度策略(五)


    整个UCOSII嵌入式操作系统的任务调度策略便是如此,现在进行一个总结:

    ①某个任务在执行中,每隔一定周期发生滴答时钟中断,在中断中遍历整个任务链表,更新每个任务的延时时间,修改就绪状态。

    ②任务执行完毕后,进入延时函数,在延时函数中会把当前任务挂起(清空当前任务的就绪状态,使其进入未就绪状态),然后根据查表发找到在就绪任务中,优先级最高的那一个任务。

    ③找到新任务以后,人工强制发生一个中断,保存上个任务的堆栈信息,弹出下个任务的堆栈信息,同时更改PC指针,进行任务切换。

    经过以上三个步骤,便可以完成任务的调度。

    现在回到第一篇提出的那个问题:UCOSII到底是如何保证它的实时性的呢? 

    如果任务的调度都是发生在当前任务进入延时之后,似乎操作系统根本无法自身的保障实时性。

    比如一个优先级最低的任务由于某些处理非常耗费时间,它一直无法进入延时,导致无法进入任务切换,那么优先级高的任务反而是一只都无法被执行了……

    同样在第一篇说过,UCOSII系统除了在当前任务进入延时函数会发生调度之外,还有别的时机会进行任务切换:

      1.当前任务进入了延时。

      2.当前任务被挂起。

      3.当前任务执行时,发生了某些中断。

    第1点我们已经全部讲完,第2点非常好理解,我们现在看一个函数:OSTaskSuspend()

    这个函数的作用是把某个任务挂起(也就是不进行调度),现在来分析一个实例:

    有一个任务调用了这个函数:

    void App1_task(void *pdata)
    {
        while(1)
        {
            if (OS_ERR_NONE != OSTaskSuspend(OS_PRIO_SELF))
            {
                Dbg_SendStr("App1_task Suspend Error£¡
    ");
            }
            delay_ms(10);
        };
    }

    当前任务执行了红色代码之后,便会把自身挂起来,如果没有再别的地方对它进行激活,这个任务便永远也不会执行下去了。

    深入分析OSTaskSuspend函数:

    INT8U  OSTaskSuspend (INT8U prio)
    {
        BOOLEAN    self;
        OS_TCB    *ptcb;
        INT8U      y;
    #if OS_CRITICAL_METHOD == 3u                     /* Allocate storage for CPU status register           */
        OS_CPU_SR  cpu_sr = 0u;
    #endif
    
    
    
    #if OS_ARG_CHK_EN > 0u
        if (prio == OS_TASK_IDLE_PRIO) {                            /* Not allowed to suspend idle task    */
            return (OS_ERR_TASK_SUSPEND_IDLE);
        }
        if (prio >= OS_LOWEST_PRIO) {                               /* Task priority valid ?               */
            if (prio != OS_PRIO_SELF) {
                return (OS_ERR_PRIO_INVALID);
            }
        }
    #endif
        OS_ENTER_CRITICAL();
        if (prio == OS_PRIO_SELF) {                                 /* See if suspend SELF                 */
            prio = OSTCBCur->OSTCBPrio;
            self = OS_TRUE;
        } else if (prio == OSTCBCur->OSTCBPrio) {                   /* See if suspending self              */
            self = OS_TRUE;
        } else {
            self = OS_FALSE;                                        /* No suspending another task          */
        }
        ptcb = OSTCBPrioTbl[prio];
        if (ptcb == (OS_TCB *)0) {                                  /* Task to suspend must exist          */
            OS_EXIT_CRITICAL();
            return (OS_ERR_TASK_SUSPEND_PRIO);
        }
        if (ptcb == OS_TCB_RESERVED) {                              /* See if assigned to Mutex            */
            OS_EXIT_CRITICAL();
            return (OS_ERR_TASK_NOT_EXIST);
        }
        y            = ptcb->OSTCBY;
        OSRdyTbl[y] &= (OS_PRIO)~ptcb->OSTCBBitX;                   /* Make task not ready                 */
        if (OSRdyTbl[y] == 0u) {
            OSRdyGrp &= (OS_PRIO)~ptcb->OSTCBBitY;
        }
        ptcb->OSTCBStat |= OS_STAT_SUSPEND;                         /* Status of task is 'SUSPENDED'       */
        OS_EXIT_CRITICAL();
        if (self == OS_TRUE) {                                      /* Context switch only if SELF         */
            OS_Sched();                                             /* Find new highest priority task      */
        }
        return (OS_ERR_NONE);
    }

    直接从红色代码部分开始看,他首先判断一下我要挂起的任务是不是自己,现在我们传的参数就是OS_PRIO_SELF,所有它应该执行第一个if判断。

    在这个if判断中保存了一下需要挂起的任务的优先级,然后用蓝色代码判断一下需要挂起的任务是否存在(由于我们挂起的是自身,自身肯定是存在的,但是这并不表示这个判断多余,因为如果是一个优先级为1的任务调用这个函数去挂起一个优先级为2的任务,那判断一下还是很必要的)。

    然后接下来的几句代码就不用再解释了,和任务进入延时函数把自己的就绪状态情况是一毛一样的处理。

    直接看ptcb->OSTCBStat |= OS_STAT_SUSPEND这句代码,变量OSTCBStat 很容易理解,它表示当前任务的状态,整句代码的意义就是给当前任务设定一个已经被人工挂起了的状态,免得在任务调度的时候被调度出来(在滴答时钟中断中有这个变量的判断)。

    这句代码以后:

        if (self == OS_TRUE) {                                      /* Context switch only if SELF         */
            OS_Sched();                                             /* Find new highest priority task      */
        }

    这几句代码也已经很熟悉了,中间那个函数就是任务切换,先看看那个判断,如果我要挂起的是当前任务,那么就立即进行切换,如果挂起的是别的任务,那就不用切换,这个理解起来应该不难。

    在理解的第一种切换时机的前提下,第二种任务切换的时机很好理解,但是第二种任务切换的时机仍然不能保证任务执行的实时性,如果低优先级的任务既不进入延时,也不挂起,高优先级的任务依然无法执行。

    现在来看第三种,当中断发生时,任务切换……

    在任务执行期间,发生频繁的中断必然就是滴答时钟中断,现在重新回到以前看过的那个中断服务函数:

    void SysTick_Handler(void)
    {
        if(delay_osrunning==1)                      //OS开始跑了,才执行正常的调度处理
        {
            OSIntEnter();                           //进入中断
            OSTimeTick();                           //调用ucos的时钟服务程序
            OSIntExit();                            //触发任务切换软中断
        }
    }

    这一次的重点不再是第二个函数,而是第一个和第三个函数:OSIntEnter,OSIntExit。

    这两个函数是成对出现,从函数名便可看出,OSIntEnter是进入中断时候调用,OSIntExit是离开中断时候调用。

    由于滴答时钟是周期性调用,因此这两个函数也是周期性被调用

    OSIntEnter的定义如下:

    void  OSIntEnter (void)
    {
        if (OSRunning == OS_TRUE) {
            if (OSIntNesting < 255u) {
                OSIntNesting++;                      /* Increment ISR nesting level                        */
            }
        }
    }

    入口函数的定义很简单,就是对变量OSIntNesting执行加处理,表示我现在正在执行中断函数,如果发生了中断,或者有中断嵌套,那么这个变量肯定是大于1的,在系统的很多地方,都需要判断这个变量,因为很多地方都不能在中断中执行。

    出口函数的定义就有些复杂了:

    void  OSIntExit (void)
    {
    #if OS_CRITICAL_METHOD == 3u                               /* Allocate storage for CPU status register */
        OS_CPU_SR  cpu_sr = 0u;
    #endif
    
        if (OSRunning == OS_TRUE) {
            OS_ENTER_CRITICAL();
            if (OSIntNesting > 0u) {                           /* Prevent OSIntNesting from wrapping       */
                OSIntNesting--;
            }
            if (OSIntNesting == 0u) {                          /* Reschedule only if all ISRs complete ... */
                if (OSLockNesting == 0u) {                     /* ... and not locked.                      */
                    OS_SchedNew();
                    OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
                    if (OSPrioHighRdy != OSPrioCur) {          /* No Ctx Sw if current task is highest rdy */
    #if OS_TASK_PROFILE_EN > 0u
                        OSTCBHighRdy->OSTCBCtxSwCtr++;         /* Inc. # of context switches to this task  */
    #endif
                        OSCtxSwCtr++;                          /* Keep track of the number of ctx switches */
                        OSIntCtxSw();                          /* Perform interrupt level ctx switch       */
                    }
                }
            }
            OS_EXIT_CRITICAL();
        }
    }

    直接从红色部分开始看,首先判断系统是否在运行,在系统运行的前提下,对变量OSIntNesting进行减处理。

    当进入中断以后,调用入口函数,对变量OSIntNesting加1,中断内容处理完以后,对变量OSIntNesting减1,当变量OSIntNesting为0的时候,表示没有进行中断处理,这个时候才可以进行任务切换。

                if (OSLockNesting == 0u) {                     /* ... and not locked.                      */
                    OS_SchedNew();
                    OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
                    if (OSPrioHighRdy != OSPrioCur) {          /* No Ctx Sw if current task is highest rdy */
    #if OS_TASK_PROFILE_EN > 0u
                        OSTCBHighRdy->OSTCBCtxSwCtr++;         /* Inc. # of context switches to this task  */
    #endif
                        OSCtxSwCtr++;                          /* Keep track of the number of ctx switches */
                        OSIntCtxSw();                          /* Perform interrupt level ctx switch       */
                    }
                }

    然后判断一下系统是否上锁,如果上锁了,任然不能进行调度。

    当一切条件就绪以后,调用函数OS_SchedNew,这个函数也已经熟悉了,作用就是寻找在就绪任务中,优先级最高的那一个。

    把优先级最高的任务保存在OSPrioHighRdy中,如果当前任务不等于优先级最高的任务,那么就调用系统函数OSIntCtxSw进行任务切换……

    看到这里,应该能够回答那个问题了:如何保证系统的实时性?

    void SysTick_Handler(void)
    {
        if(delay_osrunning==1)                      //OS开始跑了,才执行正常的调度处理
        {
            OSIntEnter();                           //进入中断
            OSTimeTick();                           //调用ucos的时钟服务程序
            OSIntExit();                            //触发任务切换软中断
        }
    }

    在中断服务函数中,第二个函数负责更新任务就绪表,第三个任务负责切换任务,因为滴答中断是周期性发生的,所以任务切换也是周期性发生的。

    当有一个优先级低的任务执行时,如果有优先级更高的任务就绪了,那么只要发生了一次滴答中断,任务就能被立即切换过去,延时只有一个滴答时钟的时间,如果定义的时钟周期是1ms,那么低优先级的任务最多也就能运行1ms,然后便会强行剥夺CPU的执行权限,转交给高优先级的任务。

    由于存在这种机制,因此便能保证UCOSII系统任务的实时性。

    总结

     个人认为,对于一个嵌入式操作系统而言,最核心和最重要的,便是任务调度的机制与策略,只要实现了这个功能,那么一个嵌入式操作系统的架构也就搭建了起来,至于其他的消息,邮箱,队列等功能,都是在这个架构上实现增值产品。

    只要深入理解了UCOSII系统的任务调度的原理,那么自己手动实现一个简易的操作系统内核,似乎也并不是一件触不可及的事情。

    如果有兴趣,可以去这里看看(https://www.zhihu.com/question/25628124)里面有很多大神都实现了自己的操作系统。

  • 相关阅读:
    88. Merge Sorted Array
    87. Scramble String
    86. Partition List
    85. Maximal Rectangle
    84. Largest Rectangle in Histogram
    83. Remove Duplicates from Sorted List
    82. Remove Duplicates from Sorted List II
    81. Search in Rotated Sorted Array II
    80. Remove Duplicates from Sorted Array II
    计算几何——点线关系(叉积)poj2318
  • 原文地址:https://www.cnblogs.com/han-bing/p/8990919.html
Copyright © 2020-2023  润新知