首页 理论教育 操作系统实现:线程切换中断

操作系统实现:线程切换中断

时间:2023-10-21 理论教育 版权反馈
【摘要】:当中断处理程序执行完毕,最后一条指令iretd恢复上述保存在堆栈中的寄存器,然后继续执行中断发生前的代码。线程切换的机制清楚后,再来看Hello China实现在中断上下文中切换线程的细节部分。所有在中断上下文下的线程调度工作,是通过一个函数ScheduleFromInt来实现的。在操作系统刚刚启动,还没有发生线程切换的时候,是在一个初始化上下文中执行的,这时候的代码属初始化代码。

操作系统实现:线程切换中断

在Hello China的当前实现中,采用的是可抢占式的线程调度方式,即任何一个中断发生后,中断处理程序处理结束后,都会重新检查线程的就绪队列数组,选择一个优先级最高的线程投入运行。这样的调度机制,可确保优先级最高的线程能够马上得到调度。

这样就涉及一个问题:在中断上下文中,如何保存当前线程的上下文状态,并选择另外一个线程,恢复其上下文,并投入运行?本节对这个问题进行详细描述。

在进入正式讨论前,先介绍Intel IA32 CPU的一条指令——iretd。这条指令的用途很广泛,最基础的用途是从中断中返回。

在IA32构架的CPU中,每次中断发生的时候,CPU会做如下动作(没有考虑不同优先级之间的转换,比如用户态和核心态,而只考虑在核心态保护模式下的情况):

(1)把当前执行的线程所在的代码段寄存器(CS)、EIP寄存器和标志寄存器(EFLAGS)。

(2)根据中断向量号,查找中断描述表(IDT),并跳转到IDT指定的中断处理程序。

(3)中断处理程序执行完毕,执行一条iretd指令,该指令恢复先前在堆栈中保存的CS、EFLAGS、EIP寄存器信息,并继续执行。

因此,中断发生后,CPU跳转到中断处理程序前,当前线程堆栈的堆栈框架如图4-9所示。

当中断处理程序执行完毕,最后一条指令iretd恢复上述保存在堆栈中的寄存器,然后继续执行中断发生前的代码。可以看出,iretd指令的动作是一次性从堆栈中恢复EFLAGS、CS和EIP。

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

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

该指令除了用于从通常中断中返回之外,还用于任务的切换。假设在中断发生前,运行的线程是T1,这时候发生一次时钟中断,CPU按照上述方式,在T1的堆栈中保存T1的相关寄存器(EFLAGS、CS、EIP),然后跳转到中断处理程序。中断处理程序在执行具体的任务前,首先保存T1线程的其他相关寄存器(EAX/EBX等通用寄存器),然后才开始执行具体的中断处理任务(定时器处理、睡眠线程唤醒等)。执行完毕,中断处理程序会从就绪队列中选择一个优先级最高的线程,假设为T2,然后恢复其寄存器信息(包括EAX等通用寄存器,还包括线程T2的堆栈寄存器ESP),并建立上述堆栈框架(这时候的上述寄存器,就不是线程T1的,而是新选择的线程T2的),这时候的目标堆栈,也不是T1的,而是T2的,上述堆栈框架建立完成,执行iretd指令,这样恢复运行的就不再是线程T1,而是新选择的线程T2。

线程切换的机制清楚后,再来看Hello China实现在中断上下文中切换线程的细节部分。在当前的Hello China的实现中,中断处理程序被分成两部分实现。

(1)中断处理程序入口,采用汇编语言实现,该部分保存当前线程的寄存器(通用寄存器)信息,并把中断向量号压入堆栈,然后调用采用C语言实现的中断处理程序。

(2)C语言实现的中断处理程序,根据压入的堆栈号,再调用特定的中断处理函数(详细的中断处理过程请参考第8章)。

采用汇编语言实现的中断处理入口程序,对所有的中断和异常都是类似的,代码如下(为了解释方便,代码做了精简):

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

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

入口程序首先保存EAX寄存器,然后判断gl_general_int_handler是否为0,该标号实际上就是采用C语言实现的中断处理程序。若该标号为0,则说明对应的C语言实现的中断处理程序不存在(可能Master模块没有加载,gl_general_int_handler实际上是定义了一个函数指针,由master模块在初始化的时候,把这个指针填写为GeneralIntHandler函数),这样直接跳转到.ll_contiune编号处,恢复中断控制器后从中断中返回。

若gl_general_int_handler不为0,则说明存在对应的C语言处理函数(GeneralIntHandler函数),于是该中断入口程序首先保存当前线程的通用寄存器信息,然后把当前中断向量号压入堆栈,并调用gl_general_int_handler函数。在调用gl_general_int_handler函数前,当前线程各寄存器在堆栈中的框架如图4-10所示。

因为ESP是一个动态变化的指针,每次向堆栈中压入一个变量,ESP就增加对应的字节,因此,在上述堆栈框架中,保存的ESP寄存器的值是压入EBP后ESP的值。之所以保存该值,是因为gl_general_int_handler函数可以通过该值访问堆栈框架。

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

图4-10 当前线程的各寄存器在堆栈中的布局

gl_general_int_handler函数的原型如下:

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

可以看出,该函数有两个参数,即对应的中断向量号和堆栈框架指针。其中,堆栈向量号就是上述代码中压入的向量号,而堆栈框架指针就是上述堆栈框架中保存的ESP的值。需要注意的是,中断处理函数是在当前线程的堆栈中执行的。这样通过上述两个参数,GeneralIntHandler函数就可以访问中断向量号和堆栈框架。

GeneralIntHandler函数根据中断向量号,再调用对应的中断处理程序。比如,时钟中断的中断向量号是0x20,则GeneralIntHandler函数会根据该向量号查找一个数组,在该数组中保存了每个中断处理例程的地址,找到对应的例程后,GeneralIntHandler函数就会调用对应的例程。

所有在中断上下文下的线程调度工作,是通过一个函数ScheduleFromInt来实现的。中断处理程序在处理完所有其他任务后,调用该函数。这个函数实现了核心线程的重新调度,即选择优先级最高的就绪线程,然后恢复执行。下面是该函数的实现代码,为了阅读方便起见,分段进行解释(同时代码做了精简)。

978-7-111-41444-5-Chapter04-55.jpg(www.xing528.com)

在操作系统刚刚启动,还没有发生线程切换(时钟中断被禁止)的时候,是在一个初始化上下文中执行的,这时候的代码属初始化代码(也可以认为是一个初始化线程)。但Hello China的实现不把这部分代码作为任何线程,因此这时候,lpCurrentKernelThread是空值。就绪队列中却不是空的,因为初始化代码创建了shell、IDLE等线程,这些线程被放入就绪队列。

一旦初始化代码执行完毕,就会使能时钟中断,这时候,一旦发生时钟中断,该函数就会被调用。若lpCurrentKernelThread是空值,说明该函数(ScheduleFromInt)是第一次被调用,这时候,该函数会从就绪队列中取出第一个线程对象(优先级最高的线程对象),并调用__SwitchTo函数,切换到这个线程。__SwitchTo函数是实现线程切换的汇编语言函数,在后面会详细描述,现在只要知道,一旦以目标线程的硬件上下文信息调用了__SwitchTo函数,就会切换到目标线程开始运行。

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

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

上面这一段代码相对比较复杂,而且内部关系紧密,不容易拆开解释,因此放在下面统一解释,希望读者能够真正理解这段代码的含义。这段代码可以说是整个Hello China操作系统调度程序的核心。

代码首先把核心线程的上下文(即lpEsp指针,由GeneralIntHandler传递过来,指向线程的堆栈框架)保存起来,然后根据线程的不同状态,做不同的处理。首先,对于下列几种状态,调度程序是“直接放行”的,即允许当前线程(实际上是中断发生时的线程)继续执行。因为下面这些状态都是“过渡状态”,继续执行很短一段时间后,会马上让出CPU。

(1)KERNEL_THREAD_STATUS_SUSPENDED

在核心线程被挂起的时候,会处于这种状态。之所以产生这种状态的核心线程,是因为当前核心线程在调用SuspendKernelThread函数时,把自己指定为待挂起线程。该函数首先设置当前运行核心线程的状态为KERNEL_THREAD_STATUS_SUSPENDED,并放入挂起队列,然后从就绪队列中选择另外一个优先级最高的核心线程投入运行。

若在当前核心线程的状态刚刚被设置为SUSPENDED,还没有放入挂起队列的时候,发生了中断,这样当前核心线程的状态就是KENREL_THREAD_STATUS_SUSPENDED。这是一种临时状态,会在很短的时间内被切换出CPU。因此,发生中断的时候,若当前核心线程处于这种状态,则不作任何调度,而是恢复当前核心线程,继续让其执行(采取“放行”的策略)。因为在很短的时间内,又会发生一次线程调度。

(2)KERNEL_THREAD_STATUS_SLEEPING

核心线程在调用Sleep函数,但还未完全进入睡眠状态的时候,会处于正在运行,但状态为SLEEPING的情况。因为Sleep函数会首先把当前核心线程的状态设置为SLEEPING,然后插入睡眠队列,并从就绪队列中选择另外一个状态为KERNEL_THREAD_STATUS_READY的线程投入运行。

若核心线程的状态刚刚被设置为KERNEL_THREAD_STATUS_SLEEPING,还没有来得及被插入睡眠队列,这时候发生中断,则当前线程就是SLEEPING状态。对处于这种状态的核心线程,调度程序也不会打断,而是恢复其上下文,继续让其执行。因为在很短的时间内,该线程就会被切换出CPU。

(3)KERNEL_THREAD_STATUS_TERMINAL

在核心线程结束的时候,会处于KERNEL_THREAD_STATUS_TERMINAL。在核心线程结束运行的时候,首先会把自己的状态设置为KERNEL_THREAD_STATUS_TERMINAL,然后试图从就绪队列中选择另外一个状态为READY的线程投入运行。若这个过程中有中断发生,则在中断处理程序看来,当前核心线程会处于TERMINAL状态。对于这种状态的核心线程,也采取放行策略。

(4)KERNEL_THREAD_STATUS_BLOCKED

在核心线程等待一个核心对象的时候,会处于这种状态。核心线程调用WaitForThisObject或WaitForThisObjectEx函数,等待一个共享对象。在这些函数的处理中,首先把当前核心线程的状态设置为KERNEL_THREAD_STATUS_BLOCKED,然后把当前线程插入共享对象的等待队列。但若在插入等待队列前发生中断,则被中断的核心线程(当前核心线程)就会处于这种状态。

处于上述状态的核心线程,说明已经打算主动让出CPU了。既然人家态度很明确,已经想走了,调度程序就表现得很“大度”,礼貌性地“挽留”一下,或者至少不要主动“驱逐”人家。因此对于上述几种状态的核心线程,调度程序采取“直接放行”的策略。但在放行之前,也会增加其执行时间(增加一个时间片),然后调用__SwitchTo函数,重新切换到当前核心线程继续执行。这就是上述代码中switch语句开始处的几个连续case的含义。

接下来,如果核心线程的状态是RUNNING,则说明线程被中断打断的时候,处于正常执行状态,这时候就必须切换处理了。既然你没有退出的意思,而且你的时间到了,那么不客气,调度程序就只能下逐客令了。但这时候又要分两种情况进行处理,第一种情况是,当前核心线程的优先级足够高,就绪队列数组中没有比它更优先的线程。这种情况下,调度程序不得不“厚着脸皮”,重新把当前线程“请回来”继续执行。这就是case KERNEL_THREAD_STATUS_RUNNING语句后前半部分的处理动作。具体的实现方式是,使用当前核心线程的优先级调用GetScheduleKernelThread,试图从就绪队列中找到一个比当前核心线程优先级更高的就绪线程。如果返回NULL,说明就绪队列中没有比当前核心线程优先级更高的线程,于是恢复当前核心线程的上下文,使之继续执行。但在恢复其执行之前,需要增加其运行时间。

另外一种情况是,调用GetScheduleKernelThread函数成功,返回一个核心线程对象。这时候调度程序就毫不含糊了,决然放弃当前核心线程的执行,选择返回的线程恢复执行。这个过程也很简单,首先处理当前线程的收尾工作,主要是把其状态修改为READY,并加入就绪队列。然后启动新线程的恢复工作,先把其状态修改为RUNNING,增加其运行时间片,同时把当前核心线程指针(KernelThreadManager维护的一个全局变量,lpCurrentKernelThread)保存起来,然后调用__SwitchTo函数,切换到目标线程继续执行。

所有不属于上述情况(即上述switch块中的default语句)的线程状态,都会是一种异常状态,即内核BUG。这时候会调用BUG函数,打印出BUG发生时的相关诊断信息,然后整个系统停止运行。这与Windows操作系统的蓝屏异常情况类似,一旦遇到这种情况,说明系统核心数据结构已不连续,操作系统必须停止运行。当然,这种情况很少发生,除非用户程序直接修改(不是通过调用API修改)线程核心对象的状态。

至此,对中断上下文中的线程调度就解释完了。在此总结一下。

(1)所有硬件中断处理结束后,线程都会被重新调度。这一步是由ScheduleFromInt函数完成的。

(2)当前线程的上下文信息,是在中断处理程序的入口处(采用汇编语言编写的代码)进行保存的。

(3)在中断处理程序中,调用ScheduleFromInt函数来实现线程的调度,需要注意的是,这个函数在中断处理程序的最后部分被调用,因为该函数不会返回,直接切换到目标线程开始运行。

(4)对线程的切换,在IA32 CPU上采用iretd指令实现。

(5)ScheduleFromInt函数调用__SwitchTo函数切换到目标线程。

(6)对于状态是SUSPENDED、BLOCKED、TERMINAL、SLEEPING的线程,不做调度,而是恢复其上下文,使得这些线程继续运行。因为处于这些状态的线程,都是临时状态,很快就会被切换出去。

底层函数__SwitchTo实现了核心线程的切换,其实现是与特定CPU的架构相关的。在移植Hello China操作系统的核心时,对调度程序的移植,只需要修改这个函数即可,ScheduleFromInt等函数可不作任何修改,这样就大大提升了系统内核的可移植性。这个函数的具体实现,在本章的后续部分会有详细介绍,目前只要记住,以核心线程的硬件上下文指针调用该函数,即可切换到对应的核心线程。

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

我要反馈