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

4.5 V1.5核心线程对象(KernelThreadObject)的实现

在Hello China V1.5的实现中,对核心线程对象(__KERNEL_THREAD_OBJECT)进行了优化,删除了其中不必要的数据成员,并对其中的一些成员进行了更改。相应地,为了实现等待多个同步对象等功能,又增加了部分数据成员。优化后的核心线程对象如下:

BEGIN_DEFINE_OBJECT(__KERNEL_THREAD_OBJECT)
    INHERIT_FROM_COMMON_OBJECT
    INHERIT_FROM_COMMON_SYNCHRONIZATION_OBJECT
    //__KERNEL_THREAD_CONTEXT       KernelThreadContext;
    __KERNEL_THREAD_CONTEXT*     lpKernelThreadContext;
    DWORD                         dwThreadID;
    DWORD                        dwThreadStatus;
    __PRIORITY_QUEUE*            lpWaitingQueue;
    DWORD                         dwThreadPriority;
    //DWORD                        dwScheduleCounter;
    DWORD                         dwReturnValue;
    DWORD                            dwTotalRunTime;
    DWORD                            dwTotalMemSize;
    LPVOID                           lpHeapObject;
    LPVOID                           lpDefaultHeap;
    BOOL                             bUsedMath;
    DWORD                            dwStackSize;
    LPVOID                           lpInitStackPointer;
    DWORD                            (*KernelThreadRoutine)(LPVOID);
    LPVOID                           lpRoutineParam;
  //The following four members are used to manage the message queue
  //of the current kernel thread.
    __KERNEL_THREAD_MESSAGE
KernelThreadMsg[MAX_KTHREAD_MSG_NUM];
    UCHAR                         ucMsgQueueHeader;
    UCHAR                         ucMsgQueueTrial;
    UCHAR                         ucCurrentMsgNum;
    UCHAR                         ucAligment;
    __PRIORITY_QUEUE*            lpMsgWaitingQueue;
    __EVENT*                     lpMsgEvent;
    DWORD                            dwLastError;
  END_DEFINE_OBJECT()

相对于V1.0,V1.5主要的修改内容有:

(1)删除了在V1.0中用于调度的dwScheduleCounter成员,因为在V1.5的实现中,是严格按照优先级来进行调度的,无须该成员。

(2)在V1.0的实现中,该对象包含了一个类型为__KERNEL_THREAD_CONTEXT的数据结构,用于保存当前线程的硬件上下文。在V1.5的实现中,则把该变量替换成了一个指针,硬件的上下文直接保存在当前线程的堆栈中,新增加的指针指向核心线程的堆栈中的上下文信息。详细信息,请参考后续描述。

(3)在V1.0的实现中,采用了一个事件对象来同步核心线程的消息队列,而在V1.5的实现中,则直接添加了一个消息队列的等待队列(lpMsgWaitingQueue)对象,当核心线程调用GetMessage试图获得消息的时候,若消息队列为空,则会阻塞在该队列中。

4.5.1 V1.5版本中硬件上下文的保存

在V1.0的实现中,对硬件上下文的保存,是直接保存在核心线程对象的KernelThreadContext成员中的。每当线程切换发生的时候,原来线程的硬件上下文,保存在该结构体中,而新选择投入运行的线程,则从该结构体中获得其硬件上下文。当时的设计,是考虑到线程上下文可能被程序更改的情况,这样单独把线程上下文拿出来,会带来很多的方便。但实际表明,这种需求是非常小的,因此在V1.5中做了更改,线程上下文直接保存在线程的堆栈中,而核心线程对象则保存了一个指向核心线程上下文的指针,这样通过该指针,也可以方便地访问到线程的上下文。

而对线程上下文本身的定义,也做了更改,更改后的定义如下:

BEGIN_DEFINE_OBJECT(__KERNEL_THREAD_CONTEXT)
#ifdef __I386
    DWORD          dwEFlags;
    WORD           wCS;
    WORD           wReserved;
    DWORD          dwEIP;
    DWORD          dwEAX;
    DWORD          dwEBX;
    DWORD          dwECX;
    DWORD          dwEDX;
    DWORD          dwESI;
    DWORD          dwEDI;
    DWORD          dwEBP;
    //DWORD          dwESP;
END_DEFINE_OBJECT()

与V1.0不同的是,删除了硬件上下文中的一个数据成员dwESP。在中断或异常发生后,由汇编语言编写的中断处理程序入口模块,会建立如图4-17所示的堆栈框架。

图4-17 V1.5的核心线程堆栈框架

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

在切换到新的线程时,只需把新线程的lpKernelThreadContext装载到ESP寄存器中,就切换到了新线程的堆栈,然后恢复所有寄存器,并执行iretd指令即可,下面的汇编代码,示意了该过程:

  __declspec(naked) VOID __SwitchTo(__KERNEL_THREAD_CONTEXT* lpContext)
  {
  __asm{
    push ebp
    mov ebp,esp
    mov esp,dword ptr [ebp+0x08]  //Switched to new thread.
    pop ebp
    pop edi
    pop esi
    pop edx
    pop ecx
    pop ebx
    mov al,0x20
    out 0x20,al
    out 0xa0,al
    pop eax
    iretd
}
}

上述切换函数,只能在中断上下文中调用,因为该切换函数解除了8259中断控制器的中断请求。

但在进程上下文中切换的时候,却有些麻烦,因为需要建立上述形式的堆栈框架。在过程上下文中,定义了一个函数__SaveAndSwitch,来完成当前线程上下文的保存,并切换到新选择的核心线程。该函数原型如下:

__declspec(naked) VOID__SaveAndSwitch(__KERNEL_THREAD_CONTEXT**
 lppOldContext,__KERNEL_THREAD_OBJECT** lppNewContext);

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

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

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

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

static DWORD dwTmpEip=0;
static DWORD dwTmpEax=0;
static DWORD dwTmpEbp=0;
__declspec(naked) void __SaveAndSwitch(__KERNEL_THREAD_CONTEXT**
    lppOldContext,__KERNEL_THREAD_CONTEXT** lppNewContext)
{
  __asm{
    mov dwTmpEbp,esp  //Save ESP to global variable.
    pop dwTmpEip  //Save EIP to global variable.
    push eax
    pop dwTmpEax  //Save EAX to global variable.
    pushfd        //Save EFlags.
    xor eax,eax
    mov ax,cs
    push eax      //Save CS.
    push dwTmpEip  //Restore EIP
    push eax
    push ebx
    push ecx
    push edx
    push esi
    push edi
    push ebp
//Now,we have built the target statck frame,save it to *lppOldContext.
    mov ebp,dwTmpEbp
    mov ebx,dword ptr [ebp+0x04]
    mov dword ptr [ebx],esp  //Now,save ESP to *lppOldContext.
    //Restore the new thread's context,and switch to it.
    mov ebx,dword ptr [ebp+0x08]
    mov esp,dword ptr [ebx]  //Restored the ESP register.
    pop ebp
    pop edi
    pop esi
    pop edx
    pop ecx
    pop ebx
    pop eax
    iretd
  }
    }

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

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