嵌入式操作系统
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

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函数,切换到目标线程。需要注意的是,该函数也执行了一个错误检查,即若从就绪队列提取线程失败(就绪队列中无任何线程),则打印一个错误信息,然后死机。就绪队列中没有任何线程,是一种严重的系统错误,一般情况下是不可能发生的,只有因为编程错误、堆栈溢出等发生的情况下才可能发生。

至此,从系统调用上下文中切换线程的过程就介绍完了。