特权同学

Qsys与uC/OS-II学习笔记5:任务切换

0
阅读(3129)

Qsys与uC/OS-II学习笔记5:任务切换

         上个笔记提到调用任务延时函数后,系统将会进行任务切换,否则当前运行任务就会一直霸占着CPU的使用权。那么这个任务延时函数中到底有什么奥秘?调用它为什么能够让任务切换自如?这个笔记咱就要揭开uC/OS-II的一大设计精髓——任务切换。

         特权同学并非软件工程或是计算机科班出身,还真没学过什么操作系统,对于CPU内部架构和工作机制的理解和认识完全靠自身的实践、摸索加一些教科书的研读。对于一些概念的阐述或许不够专业,如果有些偏差也非常欢迎大家提出来加以纠正,但是我想这些“草根”式的图文或许多少能够帮助大家快速的理解和认识一些工作机理,但愿“八九不离十”应该是形容这种状态比较合适的词汇吧。其实如果能起到这样的效果,那么对这些文章而言也就足够。毕竟一板一眼、中规中矩的教科书我们看得太多了,真的是有些审美疲劳了。

         因为要说任务调度的机理,那么我们不得不先把几乎所有嵌入式处理器相关的书籍中都会提及的中断概念再提一下。虽然讲中断的书满大街都是,但是我想像图1这样一个简单示意图就能够把中断说清楚的还真不多(怎么有点“王婆卖瓜自卖自夸”的嫌疑,脸红中~~)。一个“裸奔”的CPU软件,无非就是一个main函数里面while(1)中包办所有功能,偶尔来个中断响应一些实时性要求较高的处理,仅此而已。那么,很显然,中断响应时有一个脱离当前main函数的举动发生,想要让中断响应前后CPU回到原有的main函数执行状态,则必须有一些额外的工作要干,第3和6步的出栈、入栈便是。这个示意图中,大家需要明白,当某个函数或某些指令占用CPU时,意味着CPU中的寄存器存储着和当前处理状态相关的各种中间数据信息;同样的道理,当中断函数占有CPU数据时,CPU中的寄存器存储着和中断函数相关的各种中间数据。堆栈是专门开辟的一片存储空间,用于和CPU寄存器相映射。一个在main函数中运行着的程序(包括在它的子函数中运行),如果被一个中断信号打断后去执行中断处理,最后返回,这样一个过程,发生了以下一些事情:

  1. 应用程序(即Main函数)中执行某条指令,此时应用程序控制CPU的使用权,表现为占有CPU寄存器。
  2. 一个中断源产生,应用程序停下了CPU的使用。
  3. 应用程序停下来那一刻的CPU寄存器内容被copy到堆栈中,即我们称之为入栈操作。
  4. 程序转到中断处理函数执行,表现为即将到来的下一时刻中断处理函数占有CPU使用权。
  5. 中断处理函数拥有CPU使用权,表现为占有CPU寄存器,直到中断处理函数执行完成。
  6. CPU寄存器恢复执行入栈操作前的状态,即从堆栈中copy之前入栈的信息,我们称之为出栈。
  7. 应用程序回到中断前的下一条指令开始执行,虽然经过2-6步的“意外事件”,但是除了应用程序比预期延时了一小段时间执行外,好像这个“意外事件”没有发生过一样。

图1

         再来看uC/OS-II中的任务切换是如何实现的,应该说,和传统CPU的中断机制有着异曲同工之妙。说白了,uC/OS-II其实也是假借中断之名偷梁换柱般完成了任务的切换。因为uC/OS-II中的每个task都好比“裸奔”着的软件程序中的main函数,他们都有机会独立的占用CPU的使用权。Task的写法通常有两种:

void user_task(void* pdata)

{

  while (1)

  {

    //用户代码

  }

}

或者

void user_task(void* pdata)

{

  //用户代码

  OSTaskDel(OS_PRIO_SELF);  //删除当前任务

}

 

         前者我们已经接触过,在用户代码的最后我们通常也会加上任务延时函数,让出CPU的控制权。而后者相当于一次性完成的任务,执行过一次该任务后,自我删除,从此销声匿迹,除非该任务在其他任务函数中被重新建立恢复。

OSTaskDel();函数

INT8U         OSTaskDel               (INT8U            prio);

         当任务创建并由OSStart()函数启动后,要么处于运行态(同一时刻有且只有一个运行态的任务),要么处于就绪态,如果处于某个正在运行的任务使用OSTaskDel()函数删除了该任务本身或者其他任务(空闲任务OSTaskIdle()是唯一不能被删除的任务),那么被删除的任务并不是从存储代码的程序中物理消失了,这段代码还在,只不过它已经不在任务切换优先级列表中了,以后的任务切换中不会考虑运行该任务,我们说这种状态叫做休眠态,如果要从休眠态唤醒到就绪态,则需要重新创建该函数。

OSTaskIdle()函数

         空闲任务是在OSInit();函数中被建立的,看这个函数的具体内容,发现它其实并没有干什么大事,无非是在那里“消磨时间”,也的确是这样。但uC/OS-II中必须建立空闲函数,而且它的优先级一定是最低的,至于为什么,我们接下来先讲讲任务切换的机理,然后大家很容易就能明白的。

void  OS_TaskIdle (void *p_arg)

{

#if OS_CRITICAL_METHOD == 3                      /* Allocate storage for CPU status register           */

    OS_CPU_SR  cpu_sr = 0;

#endif

 

 

 

    (void)p_arg;                                 /* Prevent compiler warning for not using 'p_arg'     */

    for (;;) {

        OS_ENTER_CRITICAL();

        OSIdleCtr++;

        OS_EXIT_CRITICAL();

        OSTaskIdleHook();                        /* Call user definable HOOK                           */

    }

}

 

         如图2所示,这便是任务切换的大体流程。和中断很相似,这里假设task1要切换到task2,task1中一定会调用任务延时函数(上一个笔记已经提到,这是任务切换的必要条件),咱还是简单的12345把它说明白:

  1. Task1拥有CPU的控制权,当前CPU寄存器存储着和task1当前执行指令相关的信息。
  2. Task1调用了任务延时函数。
  3. CPU寄存器的数据信息被copy到了堆栈中,即入栈,这个堆栈是task1独有的,圣神不可侵犯。
  4. 把Task2独有的堆栈数据信息paste到CPU寄存器中,即出栈,这是要恢复task2在上一次拥有CPU控制权的最后一条执行指令留下的现场。当然也可能task2之前未曾拥有过CPU控制权,没关系,那么默认这个堆栈是应该是个空的。
  5. 模拟产生了一个软中断源产生,任务延时函数被中断,停止继续执行。
  6. 模拟中断处理函数,大概这个函数中什么都不做,为的是有一个中断返回的动作(还真有点买椟还珠的味道)。
  7. 中断返回时,当前的CPU寄存器是task2的工作现场,那么这意味着task2已经拥有了CPU的控制权。怎么样?真得是被“偷梁换柱”了,CPU已经从task1的while(1)里面被“俘虏”到了task2中。至此,一次任务切换完成。

图2

         大家可以好好消化一下,其实任务的切换还真的不是传说中那么神奇。到这里,还是没有把任务延时函数的神秘面纱揭开,没关系,一个一个来,咱要各个击破,每个知识点都吃透了才行。

         我们先给出任务延时的一个最基本函数OSTimeDly()的程序,注意看该函数的最后调用了OS_Sched()函数,该函数便是CPU任务切换的“罪魁祸首”,好奇心强的朋友可不能放过它。

void  OSTimeDly (INT16U ticks)

{

    INT8U      y;

#if OS_CRITICAL_METHOD == 3                      /* Allocate storage for CPU status register           */

    OS_CPU_SR  cpu_sr = 0;

#endif

 

 

 

    if (OSIntNesting > 0) {                      /* See if trying to call from an    ISR                  */

        return;

    }

    if (ticks > 0) {                             /* 0 means no delay!                                  */

        OS_ENTER_CRITICAL();

        y = OSTCBCur->OSTCBY;        /* Delay current task                                 */

        OSRdyTbl[y] &= ~OSTCBCur->OSTCBBitX;

        if (OSRdyTbl[y] == 0) {

            OSRdyGrp &= ~OSTCBCur->OSTCBBitY;

        }

        OSTCBCur->OSTCBDly = ticks;              /* Load ticks in TCB                                  */

        OS_EXIT_CRITICAL();

        OS_Sched();                              /* Find next task to run!                             */

    }

}

 

         OS_Sched()函数完成任务级的调度,该函数完成了前一个任务CPU寄存器的入栈和后一个任务CPU寄存器的出栈,并且在最后做了一次“模拟”中断返回的操作,这个操作是由OS_TASK_SW()函数里完成的。

void  OS_Sched (void)

{

#if OS_CRITICAL_METHOD == 3                            /* Allocate storage for CPU status register     */

    OS_CPU_SR  cpu_sr = 0;

#endif

 

 

 

    OS_ENTER_CRITICAL();

    if (OSIntNesting == 0) {                           /* Schedule only if all ISRs done and ...       */

        if (OSLockNesting == 0) {                      /* ... scheduler is not locked                  */

            OS_SchedNew();

            if (OSPrioHighRdy != OSPrioCur) {          /* No Ctx Sw if current task is highest rdy     */

                OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];

#if OS_TASK_PROFILE_EN > 0

                OSTCBHighRdy->OSTCBCtxSwCtr++;         /* Inc. # of context switches to this task      */

#endif

                OSCtxSwCtr++;                          /* Increment context switch counter             */

                OS_TASK_SW();                          /* Perform a context switch                     */

            }

        }

    }

    OS_EXIT_CRITICAL();

}

 

         函数void OSCtxSw(void);咱还真无法右键open打开,通常不是用C语言写的,而是用汇编来完成这个操作。

         再回到 OSTaskIdle()函数,试想想如果系统中只有两个用户任务task1和task2,如果他们都调用任务延时函数,那么模拟中断返回后系统应该到哪里继续执行程序呢?不得而知,或许程序就要跑飞了,基于此,OSTaskIdle()函数就有存在的必要了,虽然它好像不干什么事,但至少它能保证系统在没有task可执行的时候处于一个可控的状态中。除此以外,OSTaskIdle()函数中还做了一件或许大家多少还是有些在意的CPU使用率的计算,它是通过计算空闲时间来推断每秒钟CPU的使用率的。