任务上下文切换新解(MIPS处理器)

在前一篇博文 实时操作系统内核的任务调度点里总结了RTOS里的任务调度时机,当操作系统内核决定要运行另一个任务的时候,它将会将当前任务的上下文环境,通常是指CPU寄存器,保存到当期任务的堆栈上,并且恢复新任务的上下文环境使之继续运行,这个过程就称为上下文切换。
上面这段话,几乎在每一本讲嵌入式软件的教材、资料里都会有,但是能再讲深一点的却不多。楼主在从事嵌入式行业的前N年里也是一直停留在这句话的认识层面,但始终对这个神秘的过程非常好奇而不得要领。为什么各大经典教材并不会去详细描述该过程呢?楼主以为,原因有以下几条:
1 上下文切换是一个和操作系统内核紧密相关的过程,RTOS的厂家在销售其操作系统内核时都已将这些代码写好调试通过,所以能真正了解到其编写和调试过程的人并不多。而且因为这部分内容非常稳定,一旦成功运行起来,几乎很少需要修改。不需要修改,谁会去看呢?
2 上下文切换是一个处理器相关的过程,所谓的上下文一般指的也就是CPU寄存器,而每种处理器的寄存器各不相同,切换的动作也就互不相同,本文以MIPS为例进行讲解。
3 嵌入式应用程序开发人员大多使用C语言进行开发,并不会关注到这些具体的切换过程,在他们的眼中,这是一个很自然的过程。而上下文切换过程因为要直接操作CPU寄存器,所以必须使用汇编或者将汇编嵌入在C语言代码里来实现,这会让很多人“望汇编生畏”,不知道这些寄存器move过来move过去的到底在做什么。更麻烦的是,几乎不能通过打印等普通调试手段来了解上下文切换,只能通过硬件调试器(如JTAG)来单步跟踪CPU指令,并逐步查看内存的变化,才有可能真正理解。

对于嵌入式架构师、系统软件工程师来说,不应该有任何技术上的死角。楼主在这篇文章以一个常见的运行轨迹(如图1所示),以情景分析的方式介绍多次任务上下文切换的过程,希望对感兴趣的看官有一点帮助或者启发。

图1
从图中可以看出,CPU运行的过程为:高优先级task-->低优先级task-->中断-->高优先级task,
(后面把高优先级task都缩写为HighTask,低优先级task都缩写为LowTask)。
假设系统里只有2个优先级不同的task,从某一时刻起,HighTask正在运行,它在运行了一段时间后,进入阻塞,将CPU让给LowTask,这是第一次上下文切换;LowTask在执行中被中断打断,这是第二次;中断运行一段时间,并且会通过发送消息或者信号等形式触发HighTask继续运行,这是第三次;HighTask醒来运行一段时间,再次将CPU让给LowTask,这是第四次。我们一个一个的来看。
每个task在创建的时候,系统内核里都会为其创建一个堆栈(有的系统是应用自己申请堆栈空间),用来保存函数栈帧和调用中的临时变量等数据。第一次上下文切换发生之前,假设HighTask在让出CPU之前的函数调用过程为:FuncA-->FuncB-->XXX_WaitMsg-->XXX_ContextSwitch,它的堆栈分布情况如图2所示:

图2
这是一个向下增长的堆栈,从高地址往低走依次是FuncA、FuncB、XXX_WaitMsg、XXX_ContextSwitch的函数栈帧,HighTask是调用XXX_WaitMsg主动让出CPU,这个函数里会例行公事地把当期task从就绪态链表摘除,挂到阻塞态链表上,查找下一个要运行的任务(其实就是要找到它的任务控制块结构以及里面的SP指针,关于任务调度算法请参考 ucosii实时操作系统的任务调度),再调用XXX_ContextSwitch来切换上下文,XXX_ContextSwitch的实现一般如下
{
移动当前任务的堆栈指针,以腾出一段内存
保存当前任务的上下文
找到下一个要运行任务的堆栈指针
恢复下一个任务的上下文,其最后一个步骤是将保存的PC值配给CPU寄存器
}
这里需要引入两个“原创概念”:完整上下文和部分上下文。完整上下文就是指CPU里所有使用到的可变的寄存器内容;考虑一下这个case,HighTask最后是执行XXX_ContextSwitch函数,非常从容地让出CPU,它知道被切换出去之前执行到的之后一条指令在哪里,并也知道执行到这条指令这里,有哪些寄存器的值是有意义的,这些就是需要保存的用在被重新调度时以恢复现场的“部分上下文”。在楼主经历过的一个MIPS处理器的切换,只需要保存s0,s1,和ra3个寄存器,另外需要增加一个flag字段,用来表明本次保存的是部分还是完整的上下文,保存完之后堆栈结构如图3所示:

图3
这一步做完后,有一个很重要的动作,就是要更新当前任务的堆栈指针到一个全局的数据结构,这个数据结构里保存了所有task的堆栈指针。有了这个数据结构,我们就能随时获取到任意一个task的堆栈指针,以恢复其运行现场。

HighTask的上下文保存说完了,接着到了LowTask的上下文恢复,因为并不清楚LowTask的上下文是部分还是完整上下文,这一步我们跳过。LowTask开始执行,在运行一段时间后,系统来了一个中断,因为中断的发生是随时的、不可预期的,所以这里的上下文保存是一个完整的上下文。假设LowTask的函数调用过程是FuncC-->FuncD,被打断前,它的堆栈应该如图4所示:

图4
中断到来后,MIPS CPU会将PC转到一个固定地址的异常向量表去执行,它的流程如下:
{
减小当前任务的堆栈指针,以腾出一段内存
保存当前任务的完整上下文
记录当前任务的堆栈指针到gp寄存器
将CPU的SP寄存器设为中断堆栈地址(一般是一个全局变量)
执行中断响应函数,俗称Irq (这一步里可能会触发更HighTask,需要将gp寄存器的值修改为HighTask的sp)
从gp寄存器里取出下一个要运行的任务堆栈指针
恢复任务上下文,其最后一个步骤是将保存的PC值配给CPU寄存器
}
MIPS处理器需要保存的完整上下文信息几乎包括了所有的CPU寄存器,如:at,v0,v1,a0~a3,t0~t9,s0~s8,ra,epc(exception PC,记录了LowTask被中断前即将执行的指令地址),以及乘法器的结果(用指令mflo和mfhi获取),再加上表明本次是完整上下文的flag。保存完后LowTask的堆栈内存结构如图5所示:

图5
可以看出,完整上下文保存和恢复比部分上下文明显要耗时的多。
在异常处理的后半段,从gp寄存器里取出的sp指针是HighTask的堆栈,首先需要判断其flag,参考图3,是部分上下文,然后将s0,s1从堆栈里取出放入寄存器里,再取出ra的值,执行下面的指令就可以回到HighTask睡眠前的现场继续执行了(执行jr指令前,需要移动SP指针,指向XXX_ContextSwitch的栈帧)。
jr ra
好,现在HighTask继续执行,一段时间后,又要重复以前的故事了,HighTask主动让出CPU,做一次部分上下文的保存,并找到LowTask的SP,恢复其上下文。一样的,先找到flag,LowTask的上下文是被中断打断时的完整上下文,再一个一个的将这些寄存器值从堆栈里取出归还到CPU寄存器里,最后一步回到LowTask继续执行的指令是
jr k0(k0寄存器保存了epc,原因参考 MIPS的32个通用寄存器 )

你可能感兴趣的:(RTOS,处理器相关)