首页 理论教育 操作系统实现之路:线程切换的系统调用

操作系统实现之路:线程切换的系统调用

时间:2023-10-21 理论教育 版权反馈
【摘要】:系统调用上下文中的线程切换,与时钟中断上下文中的线程切换基本上是一样的,唯一的不同是,系统调用上下文的线程切换,其切换前建立的堆栈框架不一样。明白了这个函数,系统调用上下文的线程重调度机制就清楚了。

操作系统实现之路:线程切换的系统调用

除了在系统时钟中断处理程序中完成线程的调度(线程切换)外,在运行的线程试图获取共享资源(调用WaitForThisObject函数),而共享资源当前状态为不可用的时候,也需要发生切换,这时候,当前线程(获取共享资源的线程)会阻塞,并插入共享资源的本地线程队列,再从就绪队列中提取优先级最高的线程投入运行。这个过程不是发生在中断上下文中的,而是发生在系统调用上下文中,这个时候的线程切换,称为“系统调用上下文中的切换”。在Hello China的实现中,任何一个系统调用结束后,都会执行核心线程的重调度工作,这样可确保最高优先级的线程总能得到及时的调度。

系统调用上下文中的线程切换,与时钟中断上下文中的线程切换基本上是一样的,唯一的不同是,系统调用上下文的线程切换,其切换前建立的堆栈框架不一样。在中断上下文中的切换,堆栈框架的建立是CPU自己完成的(即中断发生后,CPU把当前线程的CS、EFLAGS和EIP寄存器自动压入堆栈),而在系统调用上下文中,堆栈框架是由CALL指令建立的。系统调用上下文中的线程调度,是通过调用ScheduleFromProc函数来实现的。这个函数的参数是当前核心线程的上下文指针,但也可以以NULL为参数调用该函数,这时候该函数会通过KernelThreadManager维护的全局变量lpCurrentKernelThread,来获取到当前核心线程的硬件上下文。下面通过一个比较典型的系统调用WaitForEventObject(事件对象等待函数,对用户来讲,统一用WaitForThisObject来呈现)的实现,来说明ScheduleFromProc函数的使用方式:

该函数首先判断当前事件对象的状态,若当前状态为FREE(EVENT_STATUS_FREE),则函数等待成功,直接返回,否则说明当前事件对象处于未发信号状态,需要等待,这个时候,当前线程首先把自己的状态设置为KERNEL_THREAD_STATUS_BLOCKED,然后插入事件对象的等待队列。在插入等待队列之后,使用当前线程的上下文对象(lpContext),调用ScheduleFromProc函数,来引发一个重新调度。

下面重点考察ScheduleFromProc函数的实现。明白了这个函数,系统调用上下文的线程重调度机制就清楚了。但与ScheduleFromInt一样,这个函数还是有些复杂的。倒不是逻辑有多复杂,而是这个函数是一个整体,不像其他函数一样,可以比较清楚地分成几个部分单独讲解。但如果读者对ScheduleFromInt函数理解清楚了,那么对ScheduleFromProc函数的理解应该也不会困难,这两个函数的实现逻辑,大致上是类似的。下面先考察函数的源代码,为了解释方便,我们对源代码做了简化:

该函数可以在任何非中断上下文中被调用,完成核心线程的重调度。这样,该函数必须判断当前线程的状态,以确定进一步的动作。需要注意的是,当前线程的状态,不一定是RUNNING,而很多情况下,都是非RUNNING的“临时”状态,比如,当前线程等待一个共享对象,而该共享对象又是不可使用的,于是当前线程就需要把自己插入共享对象的等待队列,然后把状态设置为BLOCKED,并调用ScheduleFromProc重新调度,这样就出现了当前线程状态是BLOCKED状态的情况。但与ScheduleFromInt不同,ScheduleFromInt函数对于非RUNNING状态的临时状态,采取的是放行的策略,因为处于这种临时状态的核心线程,将很快自动放弃执行(实际上,处于这些临时状态的核心线程,正是通过调用ScheduleFromProc完成了CPU的让出工作)。而ScheduleFromProc则不同,它着重处理的就是这些非RUNNING状态的核心线程,因为一旦核心线程把自己设置为BLOCKED等非RUNNING状态,就表示核心线程希望退出运行,退出的方式,就是通过调用ScheduleFromProc实现的。如果这个函数也“谦让”一下,继续让这些非RUNNING状态的线程继续执行,就会产生死循环。

ScheduleFromProc函数的代码,也是以switch-case语句为骨架的,根据当前核心线程的线程状态分别处理。因此分析每个case语句的处理动作,是解释这个函数的最好方式。下面就分别解释:

(1)KERNEL_THREAD_STATUS_RUNNING

在当前核心线程调用WaitForThisObject等系统调用的时候,若试图等待的共享资源可用,则当前线程的状态不会被修改。但Hello China采用的是抢占式的调度方式,因此在任何系统调用中,会重新检查系统就绪队列,看是否存在比当前优先级更高的核心线程,即执行一个核心线程调度过程。

这种情况下,在调用ScheduleFromProc的时候,就会出现当前核心线程是RUNNING的情况。对于这种情况,ScheduleFromProc做如下处理:

1)调用GetScheduleKernelThread函数,试图从就绪队列中选择一个可调度线程。在调用该函数的时候,会以当前核心线程的优先级作为参数,这样GetScheduleKernelThread会返回比当前核心线程优先级更高的核心线程,若没有,则返回NULL。

2)若返回NULL,说明当前就绪队列中没有核心线程比当前线程优先级更高,于是直接返回,以使当前核心线程继续运行。

3)若能够找到一个比当前核心线程优先级更高的线程,则把当前核心线程状态修改为READY,并放入就绪队列。然后增加刚刚获取的核心线程的运行时间片信息,修改其状态为KERNEL_THREAD_STATUS_RUNNING,并修改当前核心线程指针指向该线程,调用__SaveAndSwitch函数,切换到该线程。这样当前核心线程就会被打断,从而“让路”给更高优先级的核心线程。

这种调度方式,可确保任何比当前核心线程优先级高的线程,能够在最快的时间内得到调度,从而提升系统的整体实时性。

(2)KERNEL_THREAD_STATUS_READY

在操作系统刚刚完成初始化,还没有选择任何核心线程运行的时候,当前核心线程会被设置为这种状态。在系统初始化的过程中,会创建shell、IDLE等系统核心线程。在初始化完成后,会把当前核心线程设置为创建的任何一个核心线程,不论设置为哪个核心线程,其状态都是KERNEL_THREAD_STATUS_READY。

系统初始化完成之后,会调用ScheduleFromProc函数,以切换到一个优先级最高的线程。实际上,系统初始化过程,是不属于任何核心线程的,但也可以看做是一个初始化核心线程。一旦初始化完成,切换到其他的核心线程,则这个“初始化核心线程”也就运行结束了。

这样初始化完成,调用ScheduleFromProc的时候,当前核心线程就是READY状态。针对这种状态,调度程序做如下处理:(www.xing528.com)

1)调用GetScheduleKernelThread函数,从就绪队列中提取一个核心线程。在调用该函数的时候,会以当前核心线程的优先级为参数,这样就约束了GetScheduleKernelThread函数,只能返回大于或等于当前核心线程优先级的就绪线程。

2)若GetScheduleKernelThread返回NULL,说明系统发生问题了。因为当前核心线程被创建的时候,一定是加入到就绪队列的,GetScheduleKernelThread函数至少应该返回当前核心线程。若返回NULL,则打印出调试信息(BUG()函数),并返回。

3)若返回的核心线程对象与当前核心线程是同一个,则说明当前核心线程就是系统中优先级最高的,于是增加当前核心线程的时间片计数,并修改其状态为RUNNING,直接返回,以使当前核心线程继续执行。

4)若返回的核心线程对象不是当前核心线程对象,则增加新核心线程的时间片计数,修改其状态,并切换到该线程开始执行。

(3)KERNEL_THREAD_STATUS_SUSPENDED

若当前核心线程对象的状态为KERNEL_THREAD_STATUS_SUSPENDED,则说明当前核心线程对象调用了SuspendKernelThread函数,试图挂起自己。SuspendKernelThread函数在把当前核心线程设置为SUSPENDED状态之后,会把当前核心线程插入挂起队列,并调用ScheduleFromProc函数,重新调度线程。

若当前核心线程处于该状态,则调度程序执行下列动作:

1)调用GetScheduleKernelThread函数,试图从当前就绪队列中选择一个状态为就绪的核心线程。需要注意的是,这时调用GetScheduleKernelThread函数,是以参数0作为第二个参数的,这样可导致该函数返回就绪队列中任何优先级大于或等于0的核心线程,即只要就绪队列中有核心线程对象存在,就会返回一个核心线程对象。

2)若上述函数返回NULL,说明系统出现了问题。因为就绪队列中肯定会有核心线程存在的,至少有IDLE线程存在。

3)若上述调用返回了一个合法的核心线程对象,则修改返回的核心线程状态信息,增加其运行时间片计数,调用__SaveAndSwitch函数,保存当前核心线程的上下文信息,并切换到新的核心线程开始运行。

(4)KERNEL_THREAD_STATUS_SLEEPING

当前核心线程调用Sleep函数,试图进入睡眠状态的时候,会发生当前核心线程状态是SLEEPING的情况。因为Sleep函数首先把当前核心线程设置为KERNEL_THREAD_STATUS_SLEEPING状态,并插入睡眠队列,然后调用ScheduleFromProc函数。对于这种状态的核心线程,ScheduleFromProc的处理机制与当前核心线程状态为KERNEL_THREAD_STATUS_SUSPENDED的处理机制一样。

(5)KERNEL_THREAD_STATUS_TERMINAL

线程运行结束的时候,会首先设置自己的状态为TERMINAL,并调用ScheduleFromProc函数。ScheduleFromProc函数对当前线程是该状态的处理动作,与KERNEL_THREAD_STATUS_SUSPENDED的处理机制一样。

与ScheduleFromInt一样,ScheduleFromProc也是通过调用GetScheduleKernelThread函数来获取一个最高优先级的就绪线程。在切换到目标线程的时候,也需要递增其运行时间片。与ScheduleFromInt不同的是,这个函数调用了__SaveAndSwitch函数实现了线程的切换。这个函数与__SwitchTo不同,除切换到目标核心线程外,还保存了当前核心线程的硬件上下文。在中断上下文的线程调度中,CPU和汇编语言代码自动保存了线程的硬件上下文,因此对于当前线程的上下文无需保存。但是在ScheduleFromProc中,由于没有中断发生,必须手工建立起当前核心线程的硬件上下文,然后才能切换到目标线程。在4.2.10节将详细解释__SwitchTo和__SaveAndSwitch这两个底层的硬件上下文操作函数。

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

我要反馈