@会网络的老鼠

涂飞平的博客空间

一段精彩的任务切换代码解释! [原]

12 年前 0

最近看了嵌入式操作系统的一段任务切换的代码,虽然是为Intel 80188处理器写的,但其中的处理为我们理解单处理器模拟多任务的方式有很大的好处,至少会让我们有个直观的认识!(其实这段代码与毛德操写的《Linux内核分析》中列出的Linux任务切换部分很相似,虽然更简单一点。但简单就更能显示其核心,不是吗?^_^)。
其实这段代码与我前面有篇笔记《模拟多任务》中使用的代码是相似的,那段代码的思路就来自《Linux内核分析》中介绍的Linux任务的切换部分,只是比这里的还要简单一点,仅仅使用函数来模拟而已^_^
这篇笔记记下我对于这段代码的理解,当然整个嵌入式系统远远不止这些,其他的一些主题的理解我抽时间慢慢写出来,可以和喜欢嵌入式系统的朋友讨论,也权当是自己的学习笔记^_^
言归正传
这里对这个函数做个大致的说明:
函数的声明为:

void contextSwitch(Context * pOldContext, Context * pNewContext);
参数说明:
pOldContext与pNewContext是两个准备交换处理器控制权的环境上下文指针!

其中的Context结构如下:

struct Context
{
int IP;
int CS;
int Flags;
int SP;
int SS;
int SI;
int DS;

};

//看了这个结构。如果了解过windows用户态程序调试机制或者异常机制的朋友都会觉得很熟悉,这个结构
//与window中定义的上下文是一致的(但保存的信息少很多了),就是保存当前任务的所有寄存器的状态!

下面是函数的核心代码(注:参数传递方式为自左向右的堆栈传递!):

_contextSwitch PROC FAR ;这里FAR表示的是远过程,即返回地址同时包含了CS和IP

push bp
mov bp, sp

;由堆栈得到 pOldContext指针
;由于push bp 占用2字节(这里代码是运行在Intel 80188上的,记住哟^_^)
;同时由于是远过程调用,所以返回地址占用了4字节,故第一个参数应该在ss:[bp+6]位置上!
les di, dword ptr ss:[bp+6] ;将参数按4字节方式取出,低2字节放在di中,高2字节放在es中
mov dx, es
mov ax, di

; 判断是否有pOldContext存在,如果没有就可以省去保存的工作了
; 直接跳转到加载新任务状态的代码
; if (pOldContext == NULL) goto fromIdle;
or ax, dx
jz fromIdle

;
; 保存当前任务(就是调用此函数的任务)的运行现场(环境上下文)
; es:[di]就是pOldContext
mov dx, cs
lea ax, switchComplete ;注意这里保存并非下一条记录,而是跳出的地址
mov es:[di], ax ;保存ip 如果不明白为什么,请参看前面Context结构的定义
mov es:[di+2], dx ;保存cs

; 保存CPU的标志寄存器。
; 由于标志寄存器的保存不能使用mov指令,
; 这里采用了压栈与退栈来完成类似的工作
pushf
pop es:[di+4]

;
; 保存堆栈段信息
;
mov dx, ss
mov es:[di+6], sp
mov es:[di+8], dx

;
; 保存数据段信息
;
mov dx, ds
mov es:[di+10], si
mov es:[di+12], dx

fromIdle: ;开始将pNewContext记录的状态恢复到CPU中,然后将控制权交给新任务
; 得到pNewContext指针并将段设置为es,数据偏移设置为di
; ss:[bp+6]为pOldContext,那么pNewContext就是[bp+10]
; 为什么是增加4个字节呢?因为这个函数是FAR函数调用,参数中包含了段寄存器值
les di, dword ptr ss:[bp+10]
mov dx, es
mov ax, di

;
; 恢复数据段信息
;
lds si, dword ptr [di+10]

; 恢复堆栈,从这个时候之后,函数使用的堆栈就已经不是
; 原来调用任务所使用的堆栈了!其实这个时候环境已经开始
; 发生本质的变化了!
mov dx, es:[di+8]
mov ax, es:[di+6]
pushf ; 保存当前中断状态
pop cx
cli ; 关中断,准备开始切换堆栈了!
mov ss, dx ; 堆栈切换的时候不允许发生中断,所以这里要先关中断
mov sp, ax ; 切换完成后再开中断,确保堆栈切换不受干扰
push cx
popf ; 恢复中断状态.

;恢复被切换到的新任务的标志寄存器
;这里采用了一个很重要的技巧来蒙骗CPU,就是构造一个假的堆栈来让它跳到我们原来保存的地方 。
;这个办法也是没有办法的办法,因为Intel CPU没有直接设置CS和IP值的指令
;通过构造一个假中断的入口堆栈框架,来同时恢复标志,CS,IP三个寄存器的值
push es:[di+4] ;这里压入新任务Flags到堆栈中

;
; 恢复返回的地址
;
push es:[di+2]
push es:[di]

;
; 通过中断返回指令完成如下动作
; es:[di]->IP,es:[di+2]->CS,es:[di+4]->Flags
;
iret

switchComplete: ;这是所有调度完成后,新任务返回后的第一条执行指令,记得我们保存的时候保存
;的这句吗?lea ax, switchComplete
pop bp ;仔细观察,你会发现,虽然很多代码在这中间运行,但堆栈的指针都没有变化
;(这里应该站在整个任务上层观察,虽然两个任务发生了堆栈变化,但如果采用一个
;极限思维方式来考虑,假设这两个任务根本就是同一个任务,你可能就豁然开朗了。
;它们的堆栈的确没有变化),
;这里pop操作可以平衡原来任务调用中push bp使用的堆栈,然后就安然退出
ret

_contextSwitch ENDP

这个函数是整个嵌入式操作系统的核心所在,因为是硬件平台相关的,所以这里采用的汇编来编写,对于上层操作,还有很多事情要做,比如对于任务的管理,因为Context(环境上下文)毕竟都是属于任务的。所以这个系统使用了一个高级函数来封装了任务管理和任务调度!
代码列在下面,就不再解释了^_^
void schedule(void)
{
Task * pOldTask;
Task * pNewTask;

if (state != Started) return;

//
// Postpone rescheduling until interrupts are completed.
//
if (interruptLevel != 0)
{
bSchedule = 1;
return;
}

//
// If there is a higher-priority ready task, switch to it.
//
if (pRunningTask != readyList.pTop)
{
pOldTask = pRunningTask;
pNewTask = readyList.pTop;

pNewTask->state = Running;
pRunningTask = pNewTask;

if (pOldTask == NULL)
{
contextSwitch(NULL, &pNewTask->context);//这就是我们上面解释的核心函数了
}
else
{
pOldTask->state = Ready;
contextSwitch(&pOldTask->context, &pNewTask->context);
}
}

} /* schedule() */

观察上面代码,我们会发现其中有很多可以值得借鉴的地方。其实在windows这样高级的操作系统中,所做工作的原理是一致的,只是复杂的程度不同而已。
windows在任务切换的时候除了要保存这些信息,还要保存页框的物理地址,而恢复的时候也要恢复页框地址到CR3寄存器中!当然:这里的任务切换包含的进程概念,如果单是线程的切换就没有这么麻烦,你可以认为线程切换的时候做了上面代码相似的工作^_^

编写评论