1,调度器的主要工作
在多任务系统中,令CPU中止当前正在运行的任务转而去运行另一个任务的工作叫做任务切换,而按某种规则进行任务切换的工作叫做任务调度。 这番话有火车站的味道,列车的调度
在ucos中,任务调度由任务调度器来完成。任务调度器的主要工作有两项:1,从任务就绪表中查找具有最高优先级别的就绪任务2,实现任务的切换。ucos中有两种调度器:一种是任务级的调度器,另一种是中断级的调度器。任务级的调度器主要有OSSched()来实现。而中断级的调度器由OSIntExt()来实现。
2获得待运行就绪任务控制块的指针
由于操作系统是通过任务的控制块TCB来管理任务的,因此调度器真正实施任务切换之前的主要工作就是要获得待运行任务的任务控制块指针和当前任务的任务控制块指针。
由于被中止任务的任务控制块指针就存放在全局变量OSTCBCur中,所以调度器这部分的主要工作是要获得待运行任务的任务控制块指针
void OSSched(void)
{
#if OS_CRITICAL_METHOD == 3
OS_CPU_SR cup_sr;
#endif
INT8U y;
OS_ENTER_CRITICAL();
if((OSLockNesting | OSIntNesting) == 0)
{
y = OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy = (INT8U)((y << 3) + OSUnMapTbl[OSRdyTbl[y]]); //获得最高优先级任务
if(OSPrioHighRdy != OSPrioCur)
{
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy]; //统计任务切换次数的计数器加1
OSCtxSwCtr++;
OS_TASK_SW();
}
}
OS_EXIT_CRITICAL();
}
ucos允许应用程序通过调用函数OSSchedLock()和OSSchedUnlock()给调度器上锁和解锁。为了记录调度器被锁和解锁的情况,ucos定义了一个变量OSLockNesting;调度器每被上锁一次,变量OSLockNesting就加1;反之,调度器每被解锁一次,变量OSLockNesting就减1.因此可以通过访问变量OSLockNesting来了解调度器上锁的嵌套次数。
调度器OSSched()再确认未被上锁并且不是中断服务程序调用调度器的情况下,首先从任务就绪表中查得的最高优先级别就绪任务的优先级别OSPrioHighRdy;然后在确认了这个就绪任务不是当前正在运行的任务(OSPrioCur是存放正在运行任务的优先级别的变量)的条件下,用OSPrioHighRdy作为下标去访问数组OSTCBPrioTbl[],把数组元素OSTCBPrioTbl[OSPrioHighRdy]的值(及待运行就绪任务的任务控制块指针)赋给指针变量OSTCBHighRdy。于是下面就可以依据OSTCBHighRdy和OSTCBCur这两个分别指向待运行任务控制块和当前任务控制块的指针在宏OS_TASK_SW()中实施切换



众所周知,CPU是按CPU中的一个特殊功能寄存器--程序指针PC的指向来运行程序的。或者说,只有使PC寄存器获得新任务的地址,才会使CPU运行新的任务。既然如此,对于被中止任务,应该把任务的断点指针(在PC寄存器中)压入堆栈;而对于待运行任务而言,应该把任务堆栈中上次任务被中止时存放在堆栈中的中断指针推入PC寄存器。但遗憾的是,目前的处理器一般没有对程序指针寄存器PC的入栈和出栈指令。 所以要想办法引发一次中断(或者一次调用),并让中断向量指向OSCtxSw()(其实这个函数就是中断服务程序),利用系统在跳转到中断服务程序时会自动把断点压入堆栈的功能,把断点指针送入堆栈,而利用中断返回指令,能把断点指针推入CPU的PC寄存器的功能,恢复待运行任务的断点,这样就可以实现断点的保存和恢复了
由于任务切换时需要对CPU寄存器进行操作,因此在一般情况下,中断服务程序OSCtxSw()要用汇编来编写
由什么来引发中断呢?
这就是宏OS_TASK_SW()的作用了。如果使用微处理器具有软中断指令的话,可以在宏中封装一个软中断指令即可;如果使用的微处理器没有提供软中断指令,那么就可以尝试宏OS_TASK_SW()封装其他可以使PC等相关寄存器入栈的指令.
看了书,对uC/OS-II的任务调度又重新认识了,好书啊。
uC/OS-II有两种任务调度器:任务级的调度器OSSched(),中断级的调度器OSIntExt()。
OSSched()的任务调度部分
调度首先要做的就是找到当前最高优先级的任务并运行它,在uC/OS-II中,我们在任务就绪表中找到最高优先级任务标识(即它的优先级),进而获得该任务的依据——任务控制块。
因为找到最高优先级别并不难,所以调度器OSSched()的算法也简单。如下:
y = OSUnMapTbl[OSRdyGrp];
OSPrioHighRdy = (INT8U)((y<<3) + OSUnMapTbl[OSRdyTbl[y]]);
通过上面两行代码将当前最高优先级的任务的优先级存放在OSPrioHighRdy变量中。然后通过此变量从存放任务控制块指针的数组OSTCBPrioTbl[]中获得该任务的任务控制块指针,并存放在指针变量OSTCBHighRdy中。代码如下:
OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
只要获得了最高就绪任务的任务控制块指针,再加上存放在指针变量OSTCBCur中的当前运行任务的任务控制块,就可以进行任务切换的工作了。
OSSched()代码如下:
- void OS_Sched (void)
- {
- #if OS_CRITICAL_METHOD == 3 /* Allocate storage for CPU status register */
- OS_CPU_SR cpu_sr;
- #endif
- INT8U y;
- OS_ENTER_CRITICAL();
- if ((OSIntNesting == 0) && (OSLockNesting == 0)) { /* Sched. only if all ISRs done & not locked */
- y = OSUnMapTbl[OSRdyGrp]; /* Get pointer to HPT ready to run */
- OSPrioHighRdy = (INT8U)((y << 3) + OSUnMapTbl[OSRdyTbl[y]]);
- if (OSPrioHighRdy != OSPrioCur) { /* No Ctx Sw if current task is highest rdy */
- OSTCBHighRdy = OSTCBPrioTbl[OSPrioHighRdy];
- OSCtxSwCtr++; /* Increment context switch counter */
- OS_TASK_SW(); /* Perform a context switch */
- }
- }
- OS_EXIT_CRITICAL();
- }
在上面的代码中,调度禁区是用代码
- if ((OSIntNesting == 0) && (OSLockNesting == 0))
在调度器禁区这个方面,uC/OS-II是明显优于一般操作系统的。因为一般操作系统是禁止在系统调用中进行调度的,而uC/OS-II没有这个限制。所以uC/OS-II的调度禁区与其他操作系统相比就显得更小,可剥夺型也就显得更为强硬。所以,uC/OS-II是真正的可剥夺型内核。
OSSched()的任务切换部分
调度器获得了最高级就绪任务的任务控制块指针后,任务切换的工作是由宏OSCtxSw()来执行的。
所谓任务切换,就是中止正在运行的任务,转而去运行另外一个任务的工作。
任务断点的保存
这里很关键。作者写的也很清楚明了。
如果把任务被中止运行的位置叫做断点,而把当时处理器的PC、PSW等各寄存器中数据的集合叫做断点数据,那么当任务再次运行时,必须在断点处以断点数据作为初始数据接着运行才能实现“无缝”的继续运行。要实现这个目标,就必须在任务被中止时,把该任务断点数据保存起来,重新运行时再恢复这些断点数据。
断点数据保存到何处呢?当然,谁的东西谁保存这是最好的。就是哪个任务的断点数据则由哪个任务的堆栈来保存。这就是为什么每个任务都有一个私立的堆栈。
通常情况下,任务的断点数据叫做任务的上下文。
需要注意的是,在保存断点数据之后,还有把任务堆栈当前的指针(SP)保存在任务控制块的成员变量OSTCBStkPtr中。
任务切换
任务的切换实质是断点数据的切换,断点数据的切换也就是处理器堆栈指针的切换,被中止运行任务的任务堆栈指针要保护到该任务的任务控制块中,待运行任务的任务堆栈指针要由该任务控制块转存到处理器的SP中。保证完成上述任务的前提是要获得被中止任务和待运行任务的任务控制块,在此又一次看到了任务控制块的重要性。
为了完成任务切换,uC/OS-II定义了一个函数OSCtxSw(),它要完成下面7项工作:
- 把被中止任务的断点指针保存到任务堆栈中;
- 把处理器通用寄存器的内容保存到任务堆栈中;
- 把被中止任务的任务堆栈指针当前值保存到该任务的任务控制块的OSTCBStkPtr中;
- 获得待运行任务的任务控制块
- 使处理器通过任务控制块获得待运行任务的任务堆栈指针;
- 把待运行任务堆栈中通用寄存器的内容恢复到处理器的通用寄存器中;
- 使处理器获得待运行任务的断点指针(该指针是待运行任务在上一次被调度器中止运行时保留在任务堆栈中的)
由于uC/OS-II总是把当前正在运行任务的任务控制块的指针存放在指针变量OSTCBCur中,并且在调度器的调度过程中已经得到了待运行任务的任务控制块指针OSTCBHighRdy,所以完成第2~6项工作非常容易。示意性代码如下:
- 用压栈指令把处理器通用寄存器R1,/R2...压入堆栈:
- OSTCBCur->OSTCBStkPtr = SP; //把SP保存在中止任务控制块中
- OSTCBCur = OSTCBHighRdy; //使系统获得待运行任务控制块
- SP = OSTCBHighRdy->OSTCBStkPtr //把待运行任务堆栈指针赋予SP
- 用出栈指令把R1、R2...弹入处理器的通用寄存器;
处理第一项和第七项有些麻烦。因为处理器是按一种特殊功能处理器——程序指针PC(也叫做程序计数器)的指向来运行程序的。或者说,只有使PC寄存器获得新任务的地址,才会使处理器运行新的任务。既然如此,对于被中止任务,应把任务的断点指针(在PC寄存器中)压入任务堆栈;而对于待运行任务,应把任务堆栈里上次任务被中止时存放在堆栈中的中断指针推入PC寄存器。但是目前处理器一般没有对程序指针寄存器PC的出栈和入栈指令。所以不得不想其他办法用其他可以改变PC的指令来变通一下。也就是想办法引发一次中断(或者一次调用),并让中断向量指向OSCtxSw()(这个函数就是中断服务程序),利用系统在跳转到中断服务程序时会自动把断点指针压入堆栈的功能,把断点指针存入堆栈,而利用中断返回指令能把断点指针推入处理器的PC寄存器的功能,恢复待运行任务的断点,这样就可以实现断点的保存和恢复了。
由于任务切换时需要对处理器的寄存器进行操作,因此在一般情况下,中断服务程序OSCtxSw()都要用汇编语言来编写。适宜性代码如下:
- void OSCtxSw(void)
- {
- 用压栈指令把处理器通用寄存器R1,/R2...压入堆栈:
- OSTCBCur->OSTCBStkPtr = SP; //把SP保存在中止任务控制块中
- OSTCBCur = OSTCBHighRdy; //使系统获得待运行任务控制块
- OSPrioCur = OSPrioHighRdy;
- SP = OSTCBHighRdy->OSTCBStkPtr //把待运行任务堆栈指针赋予SP
- 用出栈指令把R1、R2...弹入处理器的通用寄存器;
- IRET;
- }
用什么引发中断呢?宏OS_TASK_SW()的作用就体现在这里了。如果使用的微处理器具有软中断指令,则可以在这个宏中封装一个软中断指令即可;如果使用的微处理器没有提供软中断指令,则可以试试在宏OS_TASK_SW()中封装其他可使PC等相关寄存器压栈的指令。
调度的时机
对于uC/OS-II,只有就绪任务表的内容发生变化时才需要调度。在uC/OS-II中,就绪任务表发生变化的情况有以下几种:
- 有新任务被创建,并在就绪任务表中进行了登记;
- 有任务被删除;
- 有处于等待状态的任务被唤醒;
- 由于异步事件的发生,在中断服务程序中激活了一个或几个任务;
- 正在运行的任务需要等待某个事件而进入等待状态;
- 正在运行的任务调用延时函数而自愿进入等待状态。
综上所述,uC/OS-II应在所有系统调用函数的末尾及中断服务程序结束之前调用调度器OSSched()。