4.2.10 线程的切换——系统调用上下文
除了在系统时钟中断处理程序中完成线程的调度(线程切换)外,在运行的线程试图获取共享资源(调用WaitForThisObject函数),而共享资源当前状态为不可用的时候,也需要发生切换,这时候,当前线程(获取共享资源的线程)会阻塞,并插入共享资源的本地线程队列,然后再从就绪队列中提取优先级最高的线程投入运行。这个过程不是发生在中断上下文中的,而是发生在系统调用上下文中,这个时候的线程切换,我们称为“系统调用上下文中的切换”。
系统调用上下文中的线程切换,与时钟中断上下文中的线程切换基本上是一样的,唯一的不同是,系统调用上下文的线程切换,其切换前建立的堆栈框架不一样。在中断上下文中的切换,堆栈框架的建立是CPU自己完成的(即中断发生后,CPU把当前线程的CS、EFlags和EIP寄存器自动压入堆栈),而在系统调用上下文中,堆栈框架的建立,则是由CALL指令建立的。我们通过一个例子说明这个问题。比如,一个EVENT对象的WaitForThisObject函数实现如下。
static DWORD WaitForEventObject(__COMMON_OBJECT* lpThis)
{
__EVENT* lpEvent =NULL;
__KERNEL_THREAD_OBJECT* lpKernelThread =NULL;
__KERNEL_THREAD_CONTEXT* lpContext =NULL;
DWORD dwFlags =0L;
if(NULL==lpThis)
return OBJECT_WAIT_FAILED;
lpEvent=(__EVENT*)lpThis;
//ENTER_CRITICAL_SECTION();
__ENTER_CRITICAL_SECTION(NULL,dwFlags);
if(EVENT_STATUS_FREE==lpEvent->dwEventStatus)
{
//LEAVE_CRITICAL_SECTION();
__LEAVE_CRITICAL_SECTION(NULL,dwFlags);
return OBJECT_WAIT_RESOURCE;
}
else
{
lpKernelThread=KernelThreadManager.lpCurrentKernelThread;
//ENTER_CRITICAL_SECTION();
lpKernelThread->dwThreadStatus=KERNEL_THREAD_STATUS_BLOCKED;
//LEAVE_CRITICAL_SECTION();
__LEAVE_CRITICAL_SECTION(NULL,dwFlags);
lpEvent->lpWaitingQueue->InsertIntoQueue(
(__COMMON_OBJECT*)lpEvent->lpWaitingQueue,
(__COMMON_OBJECT*)lpKernelThread,
0L);
lpContext=&lpKernelThread->KernelThreadContext;
KernelThreadManager.ScheduleFromProc(lpContext);
}
return OBJECT_WAIT_RESOURCE;
}
该函数首先判断当前事件对象的状态,若当前状态为FREE(EVENT_STATUS_FREE),则函数等待成功,直接返回,否则,说明当前事件对象处于未发信号状态,需要等待,这个时候,当前线程首先把自己的状态设置为KERNEL_THREAD_STATUS_BLOCKED(把当前线程状态设置为BLOCKED后,该线程会一直执行,直到阻塞。详细信息请参考4.2.9节),然后插入当前对象的等待队列。在插入等待队列之后,使用当前线程的上下文对象(lpContext),调用ScheduleFromProc(该函数是KernelThreadManager的一个成员函数)函数,来引发一个重新调度。
若把调用ScheduleFromProc函数的C代码翻译成汇编代码,应该是下面这个样子。
push lpContext //Prepare parameter for ScheduleFromProc routine. call ScheduleFromProc //Call this routine. mov eax,OBJECT_WAIT_RESOURCE retn
即首先把lpContext压入堆栈,然后调用ScheduleFromProc函数。上述指令执行完毕之后,当前堆栈框架的样子如图4-13所示。
图4-13 调用ScheduleFromProc函数前的堆栈框架
之所以深入分析堆栈框架,是因为这是线程切换的关键。可以看出,ScheduleFromProc函数是这个过程的关键,该函数的功能是先保存当前线程的硬件上下文,把当前线程的硬件上下文保存到lpContext对象中(这也是为什么该函数需要当前线程上下文对象指针作为参数的原因),然后再从就绪队列中选择一个线程,并恢复所选线程的上下文,使所选线程投入运行。下面是该函数的代码,为方便起见,分段进行解释。
__declspec(naked) static VOID ScheduleFromProc(__KERNEL_THREAD_CONTEXT* lpContext) {
首先该函数使用__declspec(naked)来进行修饰,这样的目的是防止编译器生成任何附加的汇编代码,以免对当前线程的堆栈框架造成影响。这个修饰关键字的详细含义,请参考本书附录部分相关内容。
#ifdef __I386__ __asm{ push ebp mov ebp,esp add ebp,0x08 //add ebp,0x04 ?????? push ebp //Save the ESP register. sub ebp,0x08 //sub ebp,0x04 ?????? push eax push ebx push ecx push edx push esi push edi pushfd } //Now,we have saved the current kernel's context into stack successfully. #else #endif
上述汇编代码完成了针对IA32 CPU上当前线程上下文(硬件寄存器)的保存(保存到当前线程的堆栈中)。上述代码执行完毕后,当前线程的堆栈框架应该如图4-14所示。
图4-14 当前线程各寄存器在堆栈中的布局
上述代码执行完毕之后,ESP指向堆栈中EFlags寄存器所在的位置,EBP则指向堆栈中EBP寄存器所在的位置,这样通过EBP寄存器的值,就可以很容易地访问到lpContext变量的值,这是十分重要的。
当前线程的上下文保存到堆栈中之后,需要进一步从堆栈中保存到当前线程对象的Context数据结构中。之所以先保存到堆栈中,再从堆栈中保存到Context数据结构中,下面的代码完成了从堆栈到Context的保存。
//The following code saves the current kernel thread's context into kernel thread object. #ifdef __I386__ __asm{ mov eax,dword ptr [ebp+0x08] //Now,the EAX register countains the lpContext. mov ebp,esp mov ebx,dword ptr [ebp] mov dword ptr [eax+CONTEXT_OFFSET_EFLAGS],ebx //Save eflags. add ebp,0x04 mov ebx,dword ptr [ebp] mov dword ptr [eax+CONTEXT_OFFSET_EDI],ebx //Save EDI. add ebp,0x04 mov ebx,dword ptr [ebp] mov dword ptr [eax+CONTEXT_OFFSET_ESI],ebx //Save ESI. add ebp,0x04 mov ebx,dword ptr [ebp] mov dword ptr [eax+CONTEXT_OFFSET_EDX],ebx //Save EDX. add ebp,0x04 mov ebx,dword ptr [ebp] mov dword ptr [eax+CONTEXT_OFFSET_ECX],ebx //Save ECX. add ebp,0x04 mov ebx,dword ptr [ebp] mov dword ptr [eax+CONTEXT_OFFSET_EBX],ebx //Save EBX. add ebp,0x04 mov ebx,dword ptr [EBP] mov dword ptr [eax+CONTEXT_OFFSET_EAX],ebx //Save EAX. add ebp,0x04 mov ebx,dword ptr [ebp] mov dword ptr [eax+CONTEXT_OFFSET_ESP],ebx //Save ESP. add ebp,0x04 mov ebx,dword ptr [ebp] mov dword ptr [eax+CONTEXT_OFFSET_EBP],ebx //Save EBP. add ebp,0x04 mov ebx,dword ptr [ebp] mov dword ptr [eax+CONTEXT_OFFSET_EIP],ebx //Save EIP. add ebp,0x04 } //Now,we have saved the current kernel thread's context into kernel thread object. #else #endif ChangeContext(); //Call ChangeContext to re-schedule all kernel threads. }
上述代码十分简单,首先把lpContext的值从堆栈中复制到EAX寄存器(通过EBP寄存器访问堆栈),由于lpContext是一个指针,因此可以通过间接寻址的方式,通过EAX寄存器访问线程上下文数据结构(Context)的各成员。需要注意的是,这个过程一直是通过EBP寄存器来从当前线程堆栈拷贝寄存器数据的。
把当前线程的上下文信息保存完毕之后,就需要执行一个线程切换动作,从就绪队列中选择一个就绪的线程替换当前线程了。ChangeContext函数就是完成这项工作的,该函数代码如下。
static VOID ChangeContext() { __KERNEL_THREAD_OBJECT* lpKernelThread =NULL; __KERNEL_THREAD_CONTEXT* lpContext =NULL; BYTE strThread[12]; DWORD dwThread =0L; DWORD dwFlags =0L; lpKernelThread=(__KERNEL_THREAD_OBJECT*) KernelThreadManager.lpReadyQueue->GetHeaderElement( (__COMMON_OBJECT*)KernelThreadManager.lpReadyQueue, NULL); if(NULL==lpKernelThread) //If this case occurs,the system may crash. { PrintLine("In ChangeContext routine."); PrintLine(lpszCriticalMsg); PrintLine("Current kernel thread: "); strThread[0]=' '; strThread[1]=' '; strThread[2]=' '; strThread[3]=' '; dwThread=(DWORD)KernelThreadManager.lpCurrentKernelThread; Hex2Str(dwThread,&strThread[4]); PrintLine(strThread); return; } lpContext=&lpKernelThread->KernelThreadContext; //ENTER_CRITICAL_SECTION(); //Here,the interrupt must be disabled. __ENTER_CRITICAL_SECTION(NULL,dwFlags) lpKernelThread->dwThreadStatus=KERNEL_THREAD_STATUS_RUNNING; KernelThreadManager.lpCurrentKernelThread=lpKernelThread; SwitchTo(lpContext); }
该函数十分简单,直接从就绪队列中,提取第一个(优先级最高的)线程,修改其状态,并把当前线程设置为选择的线程,然后调用SwitchTo函数,切换到目标线程。需要注意的是,该函数也执行了一个错误检查,即若从就绪队列提取线程失败(就绪队列中无任何线程),则打印一个错误信息,然后死机。就绪队列中没有任何线程,是一种严重的系统错误,一般情况下是不可能发生的,只有因为编程错误、堆栈溢出等发生的情况下才可能发生。
至此,从系统调用上下文中切换线程的过程就介绍完了。