首页 理论教育 操作系统实现之路-上下文保存和切换函数

操作系统实现之路-上下文保存和切换函数

时间:2023-10-21 理论教育 版权反馈
【摘要】:前面提到了两个完成线程切换和上下文保护的底层函数__SwtichTo和__SaveAndSwitch。这时候必须手工建立上述堆栈框架,以对当前核心线程的执行上下文进行保存,然后恢复目标线程的上下文。因此,在调用该函数前,必须获得当前线程的上下文,以及待调度线程的上下文,这些工作都是ScheduleFromProc函数完成的。但CALL指令执行后,堆栈中只保存了函数执行完毕后返回的指令,没有压入CS和EFLAGS。

操作系统实现之路-上下文保存和切换函数

前面提到了两个完成线程切换和上下文保护的底层函数__SwtichTo和__SaveAndSwitch。本节对这两个函数的实现进行描述。

首先回顾一下核心线程硬件上下文的定义:

978-7-111-41444-5-Chapter04-63.jpg

上述定义中各变量的含义已经做了讲解。前面也讲到,在中断或异常发生后,由汇编语言编写的中断处理程序入口模块,会建立如图4-11所示的堆栈框架(为了方便查阅,我们把这个图再放到这里。这可能是整个线程切换部分中最重要的图示了)。

978-7-111-41444-5-Chapter04-64.jpg

图4-11 中断发生后的堆栈框架

建立上述框架后,调用GeneralIntHandler函数,该函数接受两个参数,一个是中断向量,另一个是保存的堆栈指针,即图中的ESP值(该值指向了EBP寄存器在堆栈中的位置)。显然,这个堆栈框架与核心线程上下文的定义(__KERNEL_THREAD_CONTEXT)是吻合的,因此,只要在GeneralIntHandler里把传递过来的ESP指针保存到核心线程对象的lpKernelThreadContext里就可以了。

在切换到新的线程时,只需要把新线程的lpKernelThreadContext装载到ESP寄存器中,就切换到了新线程的堆栈,然后恢复所有寄存器,并执行iretd指令即可。__SwitchTo函数就是这样实现的。下面是其实现代码。为了移植方便,__SwitchTo函数放在了ARCH目录下的源代码中,在移植Hello China到其他硬件平台的时候,只需要移植该目录下的相关代码即可,其他目录下的代码,都是与CPU无关的(当然,这只是理论情况,在移植的时候还是需要对每行代码都检查一遍的)。

978-7-111-41444-5-Chapter04-65.jpg

该函数使用__declspec(naked)进行修饰,这个修饰的含义是,不在函数的前面增加任何附加的汇编代码(即函数是naked的,“裸体”的)。在微软的编译器中,缺省情况下,编译器会在每个函数的开始处,插入一些汇编代码。这些汇编代码保存了EBP寄存器,然后把ESP寄存器的内容复制到EBP寄存器里面。这样使用EBP寄存器,即可访问函数的参数了。这个过程与上述代码中的前两条汇编指令所达到的效果是一样的,实际上据作者观察,大多数情况下,微软的编译器就是在函数的开始处,插入“push ebp”和“mov ebp,esp”两条指令。

但是线程的切换过程需要对CPU的每个动作都非常清楚,因此为了保险,作者还是让编译器不要插入辅助代码,而由作者自行插入,虽然这两者所达到的效果是一样的。在把ESP寄存器的值复制到EBP之后,就可通过EBP寄存器访问函数的参数列表了。上述代码中的第三条指令,就是把__SwitchTo函数的参数,实际上就是切换目标线程的硬件上下文,保存到了ESP指针处。我们知道,核心线程的硬件上下文,就是其被中断时的堆栈框架。这样一旦把硬件上下文恢复到ESP寄存器,本质上就是恢复了目标上下文的线程堆栈。这样再依次恢复目标线程的通用寄存器(线程被中断打断的时候,这些通用寄存器被中断处理程序的入口汇编代码保存,现在必须按照相反的顺序恢复它们),并执行iretd指令,即可切换到目标线程了。

需要注意的是,__SwitchTo函数是在中断上下文中调用的,因此在调用iretd指令恢复线程执行前,必须解除8259中断控制器的中断请求。在操作系统初始化的时候,8259中断控制器是被初始化为应答工作方式的,即中断发生后,8259在收到CPU的中断应答前,将不会发起任何其他中断。上述iretd前面的代码,就是对8259控制器做了应答。

但在系统调用上下文中切换的时候,却有些麻烦,因为这时候是没有中断发生的,图4-11中的堆栈框架无法建立。这时候必须手工建立上述堆栈框架,以对当前核心线程的执行上下文进行保存,然后恢复目标线程的上下文。这就是__SaveAndSwitch函数的实现了。下面是该函数的原型:(www.xing528.com)

978-7-111-41444-5-Chapter04-66.jpg

该函数被ScheduleFromProc函数调用,用于完成核心线程在过程上下文中的调度。因此,在调用该函数前,必须获得当前线程的上下文,以及待调度线程的上下文,这些工作都是ScheduleFromProc函数完成的。

__SaveAndSwitch被调用后(通过CALL指令),当前线程的堆栈框架中只保存了两个参数——lppOldContext和lppNewContext,以及函数返回地址,如图4-12所示。

978-7-111-41444-5-Chapter04-67.jpg

图4-12 __SaveAndSwitch调用后的堆栈框架

为了建立目标堆栈框架,在__SaveAndSwitch函数所在的源文件内,定义了两个静态全局变量,并借助这两个静态全局变量实现了当前线程堆栈框架的保存。代码如下:

978-7-111-41444-5-Chapter04-68.jpg

978-7-111-41444-5-Chapter04-69.jpg

需要注意的是,该函数被调用的时候,当前核心线程还在执行,尚未切换到新的核心线程。该函数首先保存当前核心线程的一些寄存器信息(保存到当前正在运行的核心线程的堆栈中),然后把堆栈指针保存在lppOldContext变量中(该变量实际上就是指向当前核心线程对象的lpKernelThreadContext变量)。

该函数的一个难点在于,如何建立与中断发生时完全一样的堆栈框架。在中断发生的时候,CPU自动把CPU的标志寄存器(EFLAGS)、CS段寄存器和返回的指令压入堆栈中。但CALL指令执行后,堆栈中只保存了函数执行完毕后返回的指令,没有压入CS和EFLAGS。而且中断发生的时候,CS和EFLAGS是位于返回指令之前的。因此为了建立这样的堆栈框架,首先需要把返回的指令地址保存起来,然后依次压入EFLAGS和CS,再压入刚才保存的返回指令地址。这样就建立了与中断发生时相通的堆栈框架。然后再保存当前核心线程的通用寄存器。之后,再把堆栈指针保存到当前核心线程的硬件上下文指针(lpKernelThreadContext变量,由lppOldContext指向)中。

保存完当前核心线程的上下文信息之后,通过lppNewContext变量,获得新核心线程的上下文信息的指针(实际上就是待运行核心线程的堆栈指针),然后把ESP寄存器的值恢复为新核心线程的堆栈指针,这时候操作的堆栈已经是新核心线程的堆栈了。通过连续的几条POP指令,进行新核心线程的上下文恢复,然后执行一条iretd指令,就切换到新核心线程被换出的位置并开始运行了。

在上述保存和恢复线程堆栈框架过程中,发现仅仅通过CPU提供的几个寄存器已经不能解决问题,于是定义了几个静态全局变量,用于数据的交换工作。

免责声明:以上内容源自网络,版权归原作者所有,如有侵犯您的原创版权请告知,我们将尽快删除相关内容。

我要反馈