目录
前言
上篇内容我们讲了任务控制块和任务就绪表等内容:实时操作系统---内核结构,这篇内容我们继续探讨实时操作系统的内核结构的任务调度。
一:任务调度
cos-I的任务调度思想是:“近似地每时每刻让优先级最高的就绪任务处于运行状态”。在具体做法上,它在系统或用户任务调用系统函数及执行中断服务程序结束时来调用调度器,以确定应该运行的任务并运行它。
上面我们提到了任务调度器,接下来我们就详细探讨下调度器是怎样工作的。
1:调度器的主要工作
在多任务系统中,令CPU中止当前正在运行的任务转而去运行另一个任务的工作叫任务切换,而按某种规则进行任务切换的工作叫做任务的调度。在μC/OS-Ⅱ中,任务调度由任务调度器来完成。
任务调度器的主要工作有两项:
-
一是在任务就绪表中查找具有最高优先级别的就绪任务。
-
二是实现任务的切换。
μC/OS-I有两种调度器:一种是任务级的调度器;另一种是中断级的调度器。任务级的调度器由函数OSSched()来实现,而中断级的调度器由函数 OSInt Ext()来实现。这里主要介绍任务级的调度器 OSSched()。调度器把任务切换的工作分为两个步骤:第一步是获得待运行任务的TCB指针;第二步是进行断点数据的切换。
下面我们就先介绍下调度器执行任务切换的第一步:获取待运行任务的TCB指针
2:获得待运行就绪任务控制块的指针
由于操作系统是通过任务的任务控制块TCB来管理任务的,因此调度器真正实施任务切换之前的主要工作就是要获得待运行任务的任务控制块指针和当前任务的任务控制块指针。因为被中止任务的任务控制块指针就存放在全局变量 OSTCBCur中,所以调度器这部分的工作主要是要获得待运行任务的任务控制块指针。任务级调度器 OSSched()的源代码如下:
void OSSched (void)
{
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];
OSCtxSwCtr++;
OS_TASK_SW();
}
}
OS_EXIT_CRITICAL();
}
u/os-允许应用程序通过调用函数OSSchedLock()和 OSSched Unlock()给调度器上锁和解锁。调用 OSSchedlock()
的任务保持对 CPU 的控制权,尽管有个优先级更高的任务进入了就绪态。然而,此时中断是可以被识别的,中断服务也能得到(假设中断是开着的)为了记录调度器被锁和解锁的情况,uC/OS-I定义了一个变量 OSLockNesting:调度器每被上锁一次,变量 OSLockNesting就加1;反之,调度器每被解锁一次,变量OSLockNesting就减1.因此可以通过访问变量 OSLockNesting了解调度器上锁的嵌套次数。
调度器 OSSched()在确认未被上锁并且不是中断服务程序调用调度器的情况下,首先从任务就绪表中查得的最高优先级别就绪任务的优先级别 OSPrio HighRdy;然后在确认了这个就绪任务不是当前正在运行的任务(OSPrioCur是存放正在运行任务的优先级别的变量)的条件下,用 OSPrio HighRdy作为下标去访问数组 OSTCBPrio Tb1[],把数组元素 OSTCBPrioTb1[LOSPrioHighRdy]的值(即待运行就绪任务的任务控制块指针)赋给指针变量 OSTCBHighRdy.于是下面就可以依据 OSTCBHighRdy和 OSTCBCur这两个分别指向待运行任务控制块和当前任务控制块的指针在宏OS_TAK_SW中实施任务切换了。
给调度器上锁
void OSSchedLock (void)
{
if (OSRunning == TRUE) {
OS_ENTER_CRITICAL();
OSLockNesting++;
OS_EXIT_CRITICAL();
}
}
给调度器开锁.
void OSSchedUnlock (void)
{
if (OSRunning == TRUE) {
OS_ENTER_CRITICAL();
if (OSLockNesting > 0) {
OSLockNesting--;
if ((OSLockNesting | OSIntNesting) == 0)
OS_EXIT_CRITICAL();
OSSched(); (2)
} else {
OS_EXIT_CRITICAL();
}
} else {
OS_EXIT_CRITICAL();
}
}
}
3:任务切换
其实任务切换的工作是靠 OSCtxSw()来完成的。简单地说,任务切换就是中止正在运行的任务(当前任务),转而去运行另外一个任务的操作。当然,这个任务应该是就绪任务中优先级别最高的那个任务。
为了了解调度器是如何进行任务切换的,先来探讨一下一个被中止运行(可能因为中断或者调用)任务,将来又要“无缝”地恢复运行应该满足什么条件。为了讨论上的方便,如果把任务被中止运行时的位置叫做断点,把当时存放在CPU的PC、PSW和通用寄存器等各寄存器中的数据叫做断点数据,那么当任务恢复运行时,必须在断点处以断点数据作为初始数据接着运行,才能实现“无缝”的接续运行。因此,要实现这“无缝”的接续运行,则必须在任务被中止时就把该任务的断点数据保存到堆栈中;而在被重新运行时,则要把堆栈中的这些断点数据再恢复到CPU的各寄存器中,只有这样才能使被中止运行的任务在恢复运行时可以实现“无缝”的接续运行。
简单点说任务切换就是以下两点
-
将被挂起任务的寄存器内容入栈;
-
将较高优先级任务的寄存器内容出栈,恢复到硬件寄存器中。
从图中我们可以看出一个被中止的任务能否正确地在断点处恢复运行,其关键在于是否能正确地在CPU各寄存器中恢复断点数据;而能够正确恢复断点数据的关键是CPU的堆栈指针SP是否有正确的指向。因此也可知,在系统中存在多个任务时,如果在恢复断点数据时用另一个任务的任务堆栈指针(存放在控制块成员 OSTCBStkPtr中)来改变CPU的堆栈指针SP,那么CPU运行的就不是刚才被中止运行的任务,而是另一个任务了,也就是实现任务切换了。
当然,为防止被中止任务堆栈指针的丢失,被中止任务在保存断点时,要把当时CPU的SP的值保存到该任务控制块的成员 OSTCBStkPtr中,综上所述,任务的切换就是断点数据的切换,断点数据的切换也就是CPU堆栈指针的切换,被中止运行任务的任务堆栈指针要保护到该任务的任务控制块中,待运行任务的任务堆栈指针要由该任务控制块转存到CPU的SP中。下面上几张图看下任务切换的过程
从上面3张图我们可以看出任务切换主要需要以下7个步骤
- 把被中止任务的断点指针保存到任务堆栈中;
- 把CPU通用寄存器的内容保存到任务堆栈中;
- 把被中止任务的任务堆栈指针当前值保存到该任务的任务控制块的 OSTCBStkPtr中;
- 获得待运行任务的任务控制块;
- 使CPU通过任务控制块获得待运行任务的任务堆栈指针;
- 把待运行任务堆栈中通用寄存器的内容恢复到CPU的通用寄存器中;
- 使CPU获得待运行任务的断点指针(该指针是待运行任务在上一次被调度器中止运行时保留在任务堆栈中的)。
由于C/OS-I总是把当前正在运行任务的任务控制块的指针存放在一个指针变量OSTCBCur中,并且在调度器的前面代码中已经得到了待运行任务的任务控制块指针 OSTCBHighRdy,所以完成第②~⑥项工作非常容易。代码段如下:
用压栈指令把CPU通用寄存器R1、R2、…压入堆栈;
OSTCBCur-> OSTCBStkPtr= SP;//把SP保存在中止任务控制块中
OSTCBCur =OSTCBHighRdy;//使系统获得待运行任务控制块
SP=OSTCBHighRdy->OSTCBStkPtr;/把待运行任务堆栈指针赋予sP
用出栈指令把R1、R2、…弹入CPU的通用寄存器;
完成第①项和第⑦项工作就有一些麻烦。众所周知,CPU是按CPU中的一个特殊功能寄存器—程序指针PC(也叫做程序计数器)的指向来运行程序的。或者说,只有使PC寄有器获得新任务的地址,才会使CPU运行新的任务。既然如此,对于被中止任务,应该把任务的断点指针(在PC寄存器中)压入任务堆栈;而对于待运行任务而言,应该把任务堆栈里上次任务被中止时存放在堆栈中的中断指针推入PC寄存器。但遗憾的是,目前的处理器一般没有对程序指针寄存器PC的出栈和入栈指令。这就不得不想办法用其他可以改变PC值的指令(如 CALL INT或IRET指令等)来变通一下了。也就是说,想办法引发一次中断(或者次调用),并让中断向量指向 OSCtxSw()(其实这个函数就是中断服务程序),利用系统在跳转到中断服务程序时会自动把断点指针压入堆栈的功能,把断点指针存入堆栈,而利用中断返回指令IRET(或有相同功能的指令)能把断点指针推入CPU的PC寄存器的功能,恢复待运行任务的断点,这样就可以实现断点的保存和恢复了。由于任务切换时需要对CPU的寄存器进行操作,因此在一般情况下,中断服务程序OSCtxSw(都要用汇编语言来编写。在此只给出 OSCtxSw()的示意性代码
Void OSCtxSw(void)
{
//将R1,R2,R3及R4推入当前堆栈;
OSTCBCur->OSTCBStkPtr = SP;
OSTCBCur = OSTCBHighRdy;
OSPrioCur= OSPrioHighRdy;
SP = OSTCBHighRdy->OSTCBSTKPtr;//使SP指向待运行任务堆栈
//将R4,R3,R2及R1从新堆栈中弹出;
RET;//执行中断返回指令;
}
那么由什么来引发中断呢?这就是宏OS_TASK_SWO)的作用了。如果使用的微处理器具有软中断指令的话,可以在这个宏中封装一个软中断指令即可;如果使用的微处理器没有供软中断指令,那么就可以试一试在宏OS_TASK_SW()中封装其他可以使PC等相关寄有器压栈的指令(例如调用指令)。