上下文切换(Context Switch)是指CPU的控制权由运行任务转移到另外一个就绪任务时所发生的一种事件操作 [1],其本质是保存当前任务的CPU寄存器值到当前任务的堆栈中、以及从将要切换进来的任务堆栈中恢复各个CPU寄存器的值。在进行操作系统移植的时,上下文切换的实现是一个必不可少的步骤[2],有了上下文切换,操作系统的多任务并发执行才有了可能。
本文针对基于PowerPC指令集的E600[3]内核,对嵌入式强实时操作系统µCOS-II[4]的上下文切换的特点和结构进行研究和分析,提出µC/OS-II在E600内核上进行上下文切换的解决方案。
E600内核分析
E600内核基于PowerPC指令集架构,由32KB的L1指令Cache和L1数据Cache以及1MB的L2缓存组成,它是一种高性能设计,支持多种执行单元。E600内核实现了PowerPC指令架构中的32位部分[5][6],提供了32位有效地址,8位、16位和32位的整数数据类型以及32位和64位浮点数据类型支持。支持多达4Peta字节虚拟存储空间,以及多达64GB物理存储空间。飞思卡尔半导体公司的MPC7410,MPC7447,MPC7448,MPC8641,MPC8641D处理器均是基于E600内核。解决了µC/OS-II在E600内核上的上下文切换的实现问题,我们将会很容易在飞思卡尔半导体公司的上述芯片上实现µC/OS-II的移植,为了简化起见,我们不考虑E600内核中的浮点寄存器和AltVec寄存器,那么在E600内核中和上下文切换相关的寄存器如图1所示。
µC/OS-II在E600内核上下文切换的研究与实现
µC/OS-II上下文切换的结构分析
上下文切换的实质是保存当前CPU中寄存器的值到当前任务的堆栈中,然后把要切换进来的下一个任务堆栈中保存的寄存器的值恢复到CPU的寄存器中,然后执行中断返回指令让切换进来的任务获得CPU的控制权[7][8]。整个切换的过程由µC/OS-II通过调用OSCtwSw()函数实现,OSCtwSw()函数的实现与具体的目标平台相关。本文的研究的对象是PowerPC E600内核,所以我们将分析如何在该平台实现OSCtwSw()函数。
我们首先用伪代码来分析OSCtwSw()的功能:
Void OSCtwSw()
{
保存CPU中所有寄存器的值到当前任务的堆栈中;
在当前任务的进程控制块中保存当前任务的堆栈指针:
OSTCBCur->OSTCBStkPtr=Stack Poiter;
获取将要重新开始运行的任务堆栈指针:
Stack pointer=OSTCBHighRdy->OSTCBStkPtr; // OSTCBHighRdy执行切换进来的新任务的进程控制块
从新任务的堆栈中恢复CPU所有寄存器的值;
执行中断返回指令让新任务获取CPU控制权开始运行;
}
从上面的伪代码所表达的语义中我们可以看出,实现上下文切换需要解决三个问题:
任务上下文在堆栈中的布局;切换出去任务的上下文保存,以及切换进来任务上下文的恢复,下面我们来逐个解决这三个问题。
上下文切换的主要性能瓶颈是保存和恢复相关寄存器的时间开销,而这一部分的实现与具体的硬件平台相关,我们要做的就是充分利用硬件提供的指令,来降低保存和恢复相关寄存器的时间。例如E600硬件平台提供了stmw指令和lmw指令来批处理的保存和恢复通用寄存器的值,我们就可以利用这两条指令来尽可能的降低保存和恢复相关寄存器的时间开销。
进程上下文在堆栈中布局
µC/OS-II中任务上下文保存在每个任务的私有堆栈中,因此我们要设计CPU寄存器在任务堆栈中的布局,以便尽可能的降低相关寄存器保存和恢复时间,另外还有提供方便检查和调试内存中堆栈的信息的标志位。
根据以上原则,任务堆栈的布局如图2所示。
进程堆栈布局初始化的代码如下:
OS_STK *OSTaskStkInit (void (*task)(void *pd), void *pdata, OS_STK *ptos, INT16U opt)
{
STK * stkp;
OS_CPU_SR INITIAL_SRR1;
INITIAL_SRR1 = OSGetMSR() ; // 获取E600的MSR寄存器值,并将MSR[EE]置位
stkp = (STK *) (ptos);//获取栈顶指针,并强制转化为堆栈布局的类型。
stkp--;
//将通用寄存器R4至R31初始化为它们的编号值
stkp->R31 = 31L;
stkp->R30 = 30L;
stkp->R29 = 29L;
……….
stkp->R05 = 05L;
stkp->R04 = 04L;
stkp->R03 = (INT32U)pdata; // R3
stkp->R02 = (INT32U)&_SDA2_BASE_; // R2
stkp->R00 = 0x00L; //R0
stkp->CR = 0;
stkp->CTR_ = 0;
stkp->XER_ = 0;
stkp->LR_ = (INT32U)task; //LR
stkp->SRR0_ = (INT32U)task; //SRRO
stkp->SRR1_ = INITIAL_SRR1; // SRR1
stkp->R01 = (INT32U)ptos; // R1
return (OS_STK *)stkp;
}
需要注意的是我们把通用寄存器的值初始化为各个的寄存器的编号,经过这种初始化之后,我们就可以在堆栈中通过读取各个寄存器的编号来验证每个寄存器初始化是否正确;另外由于E600内核没有额外的堆栈寄存器存放堆栈指针,因此我们使用通用寄存器R1作为堆栈寄存器来栈指针。为了方便访问堆栈中各个寄存器值,定义四个宏:
#define STK_FRM_SZ 160 //32 gpr + 7spr + sp -> (40* 4)
#define XTR_REG_OFFS 132 // (33 * 4)
#define R0_OFFS 8
#define R2_OFFS 12
其中STK_FRM_SZ表示堆栈的大小,我们需要保存32个通用寄存器GPR以及8个特殊寄存器的值;
XTR_REG_OFFS表示通用寄存器的大小;
R0_OFFS表示r0寄存器在堆栈中的偏移;
R2_OFFS表示r2寄存器在堆栈中的偏移。
上述代码中我们实现了对每个任务堆栈的初始化,当任务被调度到CPU的时,我们只需要把该任务的堆栈中所保存的各个寄存器的值恢复到CPU寄存器中,通过中断返回指令让该任务获取CPU控制权即可使得该任务在CPU上运行。
任务上下文保存
任务上下文的保存就是将与当前任务执行状态相关的CPU寄存器的值保存到当前任务的堆栈中,为减少函数调用方面的开销,我们用宏store_context来实现。
具体实现的伪代码如下:
#define store_context \
mtspr 272,r0; /*备份r0寄存器*/ \
mfmsr r0; \
ori r0, r0, 0x30; \
mtmsr r0; /*打开地址映射*/ \
isync; \
mfspr r0,272; /*恢复r0寄存器值*/ \
stwu r1,-STK_FRM_SZ(r1); /*创建堆栈*/ \
stw r0,R0_OFFS(r1); /*保存r0寄存器值*/ \
stmw r2,R2_OFFS(r1); /* 保存r2-r31* / \
/*前面保存32GPR+SP, 后面保存6个SPR */ \
mflr r31 /*读取LR至r31寄存器*/ \
mfxer r30; /*读取XER至r30寄存器*/ \
mfctr r29; /*读取CTR至r29寄存器*/ \
mfsrr1 r28; /*读取SRR1至r28寄存器*/ \
mfsrr0 r27; /*读取PC到r27寄存器*/ \
mfcr r26; /*读取CR到r26寄存器 */ \
stmw r26,XTR_REG_OFFS(r1); \
/*保存r26至 r31寄存器的值至该任务堆栈中*/
从上面的代码可以看出,我们是按照一定顺序来保存寄存器的值,这样做的目的是构造条件来使用E600内核提供的批处理指令stmw保存寄存器的值,以便降低保存任务上下文所耗费的时间。
进程上下文恢复
上下文恢复是将切换进来的任务相关的CPU寄存器的值从该任务的堆栈中恢复到CPU寄存器中的过程,同样我们定义了一个宏restore_context来实现,具体实现如下:
#define restore_context \
lmw r24,XTR_REG_OFFS(r1); \
/*把堆栈中通用寄存器值恢复到CPU寄存器中*/ \
mtcr r26; /* 恢复CR至 r26*/ \
mtsrr0 r27; /*恢复SRR0至 r27*/ \
mtsrr1 r28; /*恢复SRR1至 r28*/ \
mtctr r29; /*恢复CTR至 r29*/ \
mtxer r30; /*恢复 XER至 r30*/ \
mtlr r31; /*恢复 LR至 r31*/ \
/*后面32GPR + SP, 前面6SPR*/ \
lmw r2,R2_OFFS(r1); \
/*恢复CPU6个特殊寄存器的值*/ \
lwz r0,R0_OFFS(r1); /*恢复r0 */ \
lwz r1,0(r1); /*恢复堆栈指针*/
从上面的代码我们可以看出,恢复寄存器的值也是按照一定的顺序,目的也是构造E600内核提供的批处理指令lwz的使用条件,通过使用lwz指令来尽可能的降低恢复进程上下文所耗费的时间。
至此,我们实现了进程堆栈的初始化、以及上下文的保存与恢复。上下文切换的实现,为µC/OS-II在单核处理器上实现多任务的并发操作提供了可能[6]。下面通过实验来验证我们方案的可行性。
实验验证
为了验证µC/OS-II在E600内核中上下文切换方案的正确性,我们选择sbc8641d[9]开发板作为验证平台,该开发板有两个基于E600内核的MPC8641D[10]处理器,我们使用该开发板的其中一个核来验证上下文切换实现方案的正确性。
具体方案是在µC/OS-II中我们创建了两个任务,每个任务输出一段信息到串口后马上阻塞自己,让出CPU给另外一个任务。如果上下文切换实现正确的话,两个任务将会交替运行,在串口上交替输出出相关信息。
两个进程的具体实现的代码如下:
static void AppTask1 (void *pdata)
{
pdata = pdata;
while (TRUE)
{
OSPrintk("AppTask1 running!\n");
OSTimeDly(200);
}
}
static void AppTask2 (void *pdata)
{
while (TRUE)
{
OSPrintk("AppTask2 running!\n");
OSTimeDly(200);
led_on();
}
}
上述验证方案在sbc8641d开发板上经过测试,在串口的输出信息如图3所示。
从上面的实验结果可以看出,两个任务交替输出信息,这说明我们设计的上下文切换的正确性。
结论
通过对µC/OS-II实时操作系统的上下文切换机制进行分析,本文提出的针对E600的上下文切换的实现方案是µC/OS-II基于E600内核移植过程中非常重要的环节,该方案对于基于E600内核的一系列PowerPC处理器都是适用的,为µC/OS-II在基于E600内核的一系列处理器平台上进行开发提供了便利。
后续工作我们将进一步研究µC/OS-II的时钟中断和外部中断的实现机制,进一步的提供优化µC/OS-II对时钟中断和外部中断的处理效率,提供中断的响应时间。
参考文献
[1]刘月吉,张盛兵. 一种DSP 的快速上下文切换机制[J].计算机应用研究.2012,01,第29卷,第1期:204-206
[2]黄鑫. Reworks 上下文切换在Tricore 上的实现[J].计算机工程.2011年12月,第37卷增刊:369-370
[3] Freescale Corporation.e600 PowerPC Core Reference Manual.Rev.0,03/2006
[4][美]Jean J. Labrosse(著),邵贝贝(译).嵌入式实时操作系统µC/OS-II(第二版)[M]. 北京航空航天大学出版社, 2003:P307
[5]Freescale Corporation. Programming Environments Manual for 32-Bit Implementations of the PowerPC Architecture. Rev.3,9/2005
[6] PowerPC User Instruction Set Architecture Book I, Version 2.02,January 28, 2005
[7]蒋建春,汪同庆.基于µCOS-II 的嵌入式数控系统实时性分析[J].计算机工程,2006年11月,第32卷,第22期:223-224
[8]楚红雨,李磊民.实时操作系统µC/OS-II 在ARM9 上移植的实现[J].计算机工程,2005年10月,第30卷,第20期:226-228
[9]Wind River SBC8641D Engineering Reference Guide ERG-R0331-001 Revision B,2007
[10]Freescale Corporation. MPC8641D Integrated Host Processor Family Reference Manual[M]. MPC8641DRM.Rev.2,07/2008
评论