博客网 >

再谈Windows NT/2000环境切换
作者:分类:默认分类标签:

               再谈Windows NT/2000环境切换
                           WebCrazy(tsu00@263.net)

    线程是Windows NT/2000环境切换的最基本单位。在<<浅析Windows NT/2000环境切换>>(Nsfocus Magazine 12)一文中,我只对进程CR3切换进行了较详细的讨论,但未涉及线程调度的内容,本文将尽量讲述这些部分内容。在这之前,还是先看看以下的代码:

    //-----------------------------------------------
    //
    // EnumThreads-information from KPEB and KTEB
    // Only test on Windows 2000 Server Chinese Edition
    // Build 2195(Free)!Programmed By WebCrazy
    // (tsu00@263.net) on 5-23-2000!
    //
    //-----------------------------------------------

    #define KTEBListOffsetKPEB         0x50
    #define PIDOffset                  0x9c  
    #define KPEBListOffset             0xa0
    #define ProcessNameOffset          0x1fc    

    #define StackTopOffset             0x18
    #define StackBtmOffset             0x1c
    #define UserTEBOffset              0x20
    #define StackPtrOffset             0x28
    #define KTEBListOffset             0x1a4
    #define KTEBPIDOffset              0x1e0
    #define TIDOffset                  0x1e4

    void DisplayThreadFromKPEB(void *kpeb)
    {

        char ProcessName[16];
        ULONG PID;
        ULONG TID;
        ULONG StackBtm,StackTop,StackPtr,UserTEB;

        PLIST_ENTRY KTEBListHead, KTEBListPtr;

        KTEBListHead=KTEBListPtr=(PLIST_ENTRY)((int)kpeb+KTEBListOffsetKPEB);

        do
          {
           void *kteb;
           kteb=(void *)((*(ULONG *)KTEBListPtr)-KTEBListOffset);
           TID=*(ULONG *)(((char *)kteb)+TIDOffset);
           StackBtm=*(ULONG *)(((char *)kteb)+StackBtmOffset);
           StackTop=*(ULONG *)(((char *)kteb)+StackTopOffset);
           StackPtr=*(ULONG *)(((char *)kteb)+StackPtrOffset);
           UserTEB=*(ULONG *)(((char *)kteb)+UserTEBOffset);
           memset(ProcessName, 0, sizeof(ProcessName));
           memcpy(ProcessName, ((char *)kpeb)+ProcessNameOffset, 16);
           PID=*(ULONG *)(((char *)kpeb)+PIDOffset);
           // or PID=*(ULONG *)(((char *)kteb)+KTEBPIDOffset);
           DbgPrint("  %04X  %08X   %08X  %08X  %08X  %08X  %s(%X)\n",
                    TID,kteb,StackBtm,StackTop,StackPtr,UserTEB,ProcessName,PID);
           KTEBListPtr=KTEBListPtr->Flink;
          }while (KTEBListPtr->Flink!=KTEBListHead);
     }


     void EnumThreads()
     {

        PLIST_ENTRY KPEBListHead, KPEBListPtr;

        if(((USHORT)NtBuildNumber)!=2195){
           DbgPrint("Only test on Windows 2000 Server Build 2195!\n");
           return;
        }

       DbgPrint("\n  TID   KTEB Addr  StackBtm  StackTop  StackPtr  User TEB  Process
                  Name  (PID)");
       DbgPrint("\n  ----  --------   --------  --------  --------  --------  -------
                  ----- -----\n");

       KPEBListHead=KPEBListPtr=(PLIST_ENTRY)(((char *)PsInitialSystemProcess)+KPEBListOffset);
       while (KPEBListPtr->Flink!=KPEBListHead) {
           void *kpeb;
           kpeb=(void *)(((char *)KPEBListPtr)-KPEBListOffset);
           DisplayThreadFromKPEB(kpeb);
           DbgPrint("\n");
           KPEBListPtr=KPEBListPtr->Flink;
       }
    }

    这段代码列出的EnumThreads函数在Windows 2000 Server Build 2195中的输出结果如下:

  TID   KTEB Addr  StackBtm  StackTop  StackPtr  User TEB  Process Name(PID)
  ----  --------   --------  --------  --------  --------  -----------------
  0004  FE4E19E0   F9019000  F901C000  F901B9C4  00000000  System(8)
  000C  FE4E0C80   F9021000  F9024000  F9023D34  00000000  System(8)
  0010  FE4E0A00   F9025000  F9028000  F9027D34  00000000  System(8)
  0014  FE4E0780   F9029000  F902C000  F902BD34  00000000  System(8)
  0018  FE4E0500   F902D000  F9030000  F902FD34  00000000  System(8)
  001C  FE4E0280   F9031000  F9034000  F9033D34  00000000  System(8)
                               .
                               .(略)
                               .

    从运行结果可知EnumThreads主要是实现将系统当前所有的线程列出,上面的输出格式与SoftICE的thread命令一致。代码中使用了一些Undocumented的KPEB/KTEB数据项:

    1.进程的线程链表
      这是一个LIST_ENTRY结构的项(占用两个32位的指针即8字节),位于KPEB后的50h处,上面代码由KTEBListOffsetKPEB表示。
    2.线程链表相对KTEB的偏移
      位于KTEB后1a4h处(KTEBListOffset定义)。

    输出结果中的如StackBtm、StackTop、StackPtr等请参阅<<SOFTICE COMMAND REFERENCE>>,它们在KTEB中的位置请直接看代码前的定义。

    在理解了EnumThreads程序段与我上次给出的实现EnumProcesses的底层代码后,也差不多明白了Windows NT/2000是如何组织、管理进程与线程了,这对理解线程调度可是至关重要的。虽然如此但要讨论线程调度,还是再看看几个重要的数据(我不再具体说明如何取得这些数据具体位置的方法了,如果您很想知道还是建议您再看看<<浅析Windows NT/2000环境切换>>):

    1. 进程状态(Status) //KPEB+65h UCHAR

    典型的进程状态有:Running、Ready与Idle。
    应该说明的是在单处理器的机子中处于Running状态的进程只有一个,且其不受KPEB中的这个值约束,系统通过调用IoGetCurrentProcess内核例程获得。我在<<再谈Windows NT/2000内部数据结构>>对IoGetCurrentProcess进行了比较详细的介绍。

    当KPEB中Status值为0时,进程状态为Ready;为1时进程状态为Idle;为2时进程状态为Transition等等。
    正像前面所提及的线程是Windows NT/2000环境切换的基本单位,实际上系统并不执行进程,进程状态和以下将要提及的进程优先级是很抽象的概念,只是系统调用时对线程的范围限制, Microsoft提出这些概念我想主要是隐藏系统内部Thread调度行为。但在有线程状态的前提下其也不是说就随便附个值即可。曾有次我将 System进程的状态从Ready改为Transition后,只能眼巴巴的看着屏幕上的程序代码,不能存盘。因为此时系统已经变得懒洋洋的,不再响应 我的千呼万唤了。

   2. 线程状态   //KTEB+2dh UCHAR

    在KTEB中有一成员State主要是指出当前线程状态,其位于KTEB+2d处(单字节)。它主要有如下几个值(值取自SoftICE的输出结果):

       0 - Initialized (表示State的值为0时,表示线程状态为Initialized,以下类同)
       1 - Ready
       2 - Running
       3 - StandBy
       4 - Terminated
       5 - Waiting
       6 - Transition

    在David Solomon与Mark Russinovich的<<INSIDE MICROSOFT WINDOWS 2000,THIRD EDITION>>中是如此描述的:

    To quote:
    --------
    The thread states are as follows:

    Ready
    When looking for a thread to execute, the dispatcher considers only the pool of threads in the
 ready state. These threads are simply waiting to execute.

    Standby
    A thread in the standby state has been selected to run next on a particular processor. When
the correct conditions exist,the dispatcher performs a context switch to this thread. Only one
thread can be in the standby state for each processor on the     system.

    Running
    Once the dispatcher performs a context switch to a thread, the thread enters the running
state and executes. The thread's execution continues until the kernel preempts it to run a higher priority thread, its quantum ends, it terminates, or it voluntarily enters the wait state.

    Waiting
    A thread can enter the wait state in several ways: a thread can voluntarily wait on an object to synchronize its execution, the operating system (the I/O system, for example) can wait on the thread's behalf, or an environment subsystem can direct the thread to suspend itself. When the thread's wait ends, depending on the priority, the thread either begins running immediately or is
moved back to the ready state.

    Transition
    A thread enters the transition state if it is ready for execution but its kernel stack is
paged out of memory. For example, the thread's kernel stack might be paged out of memory. Once
its kernel stack is brought back into memory, the thread enters the ready state.

    Terminated
    When a thread finishes executing, it enters the terminated state. Once terminated, a thread
object might or might not be deleted. (The object manager sets policy regarding when to delete
the object.) If the executive has a pointer to the thread object, it can reinitialize the thread
object and use it again.

    Initialized
    Used internally while a thread is being created.
    --------

    我以下主要对waiting的状态进行分析:

    在Windows NT/2000中线程能调用KeWaitForSingleObject、KeWaitForMultipleObjects等自动放弃自己的执行时间总 量(Quantum)。系统当前执行的线程由系统中的Processor Control Block(PRCB,注意与 Processor Control Region区别)中的CurrentThread成员指定。还记得我介绍过的如何得到当前线程吗(取FS:124H中的DWORD值)?其实就是指向这 个CurrentThread成员了。PRCB的定义KPRCB在ntddk.h中。系统通过如下函数获得KPRCB指针:

    _KeGetCurrentPrcb
    0008:80465310  MOV       EAX,[FFDFF020]  取KPCR(Processor Control Region)成员Prcb
    0008:80465315  RET

    系统当前线程状态为Running。其它线程状态由几个(通常最大为THREAD_WAIT_OBJECTS+1个,否则就会出现BSOD,但 Microsoft还定义了个MAXIMUM_WAIT_OBJECTS,这就要看您传递给系统的参数了)KWAIT_BLOCK结构表示,这些值以及以 下将要谈到的表示线程等待理由的KWAIT_REASON也均可从ntddk.h中找到。线程KWAIT_BLOCK结构数据处于KTEB+6ch处。上 次我提到的发生context switch的两种情况,要么可以用event,semphore等同步对象,要么可以用timer内核对象表示,这样可以形成线程等待对列,来表示线程 当前状态。

    由于KWAIT_BLOCK、KWAIT_REASON、还有event、timer等在Windows NT/2000中是少有的几个Documented成员,您在知道KWAIT_BLOCK的具体位置后,大可以自己读出线程等待队列。不过SoftICE已经为你呈现了所有这些内部结构了。

    从上分析对于线程状态,牵涉到比较多的内容,我将一部分分析抄录如下:

    :u _KeReadStateThread
    _KeReadStateThread
    0008:8042F029  MOV       EAX,[ESP+04]
    0008:8042F02D  MOV       AL,[EAX+04]
    0008:8042F030  RET       0004

   :bpx _KeReadStateThread if (tid==_tid)

   :bl  //这命令后退出调试器
   00)   BPX _KeReadStateThread  IF (TID==0x3BC)

   Break due to BPX _KeReadStateThread  IF (TID==0x3BC) (ET=22.28 seconds)

   //分析一下_KeReadStateThread的第一个参数(也是唯一的参数)
   :what dword(@(esp+04)) //您应该理解每个线程,每个时刻线程状态由哪个内核对象确定都是不固定的吧
   The value FF6811E0 is (a) Kernel Timer object (handle=0230) for explorer(398)
                                     |                 |
                                     |_Timer内核对象   |_这个对象在explorer进程中的句柄
  
   :timer dword(@(esp+04)) 
   Timer Object at FF6811E0
   Dispatcher Type: 08
   Dispatcher Size: 000A
   Signal State: Signaled 
         .
         .(略)
         .

   //SoftICE中的timer命令只是读出timer对象数据
   //所以你可以直接读DISPATCHER_HEADER(Common dispatcher object header)中的SignalState成员(见ntddk.h)
   //即下面这个命令

   :? #byte(@(@(esp+04)+4))
   00000001  0000000001  ""    //1代表Signaled,试过将其从1改为0吗?(从Signaled改为Not Signaled)

   好了以上分析的这个线程当前状态取决于timer对象(Object Pointer:0xFF6811E0)的状态(Jeffrey Richter说Signaled表示When time comes due)。我已经是从最简单的方面来分析了,很多线程当前状态往往不仅仅取决于一个对象,SoftICE中Thread Wait List也即是这个概念。

   谈了这么多让线程等待的对象,现在来说说KWAIT_REASON,在KTEB中有专门表示thread wait reason的一个成员,它位于KTEB偏移57h处,占用一个CHAR的空间,严格的说这才是真正表示致使线程处于wait状态的原因,上面的那么多的讨论只不过是解释什么内核对象造成这一wait reason的。DDK Documentation是这样定义wait reason的

   WaitReason
   Specifies the reason for the wait. A driver should set this value to Executive, unless it is doing work on behalf of a user and is running in the context of a user thread, in which case it should set this value to UserRequest.

   其中所提及的是否user thread是由KTEB另一个位于55h偏移处的单字符成员,它由0代表内核模式,1代表用户模式。上面提到了wait reason在驱动程序编程中最常见(并不是系统内核态代码中最多见的)的两个值:Executive与UserRequest,至于其其它值请参阅ntddk.h。

   3.进程优先级(KPEB+62h)、线程基本优先级(BasePriority,KTEB+68h)、线程动态优先级(Dyn Priority,KTEB+33h)

   这三个值各自占用一个字节。其中Thread Dyn Priority在Spy++中显示为Current Priority,而在Microsoft的WinDbg与Windows 2000 Server Resource Kit中的一些小工具,如pstat.exe等中则直接用Priority表示,但在SoftICE中则显示为Dyn Priority。由于直接用Priority又不容易表达这么多的优先级。鉴于我文中所有内容都基于SoftICE的分析,我在本文中均沿用 SoftICE中的名称。其实Microsoft在KTEB结构中还提供PriorityDecrement等其它使系统随时动态更改当前优先级,这也是 我比较喜欢使用Dyn Priority的一个原因之一。至于这些优先级的详细讨论请参阅参考资料中的<<WINDOWS NT DEVICE DRIVER DEVELOPMENT>>,其对核心态的这些值的作用进行了比较多的说明。

   4.线程亲缘性(Affinity)
  
    由于我目前尚未有条件测试多处理机的情况,我也不好在这多说,有条件的朋友我很希望您能说说。

   5.线程拥有的时间总量(Thread Quantum)

    单字节,位于KTEB+6b处。指出CPU可以让线程调度的时间总量(Quantum)。在Processor Control Block中系统存有三个_KTHREAD(KTEB)结构的成员CurrentThread、NextThread与IdleThread,分别代表系 统当前处理器正在执行的线程、将要被调用的线程与系统空闲(Idle)线程,Idle线程通常只是简单的调用KiIdleLoop,直到系统新的中断来 临,以对其它线程进行调用。Windows NT/2000中调用KiQuantumEnd判断当前线程是否使用完自己的时间总量,如果当前线程已执行完Quantum,则在KPRCB中 NextThread非空时返回NextThread,作为系统调用的下一个线程。系统通过调用KiFindReadyThread寻找下一个处于 Ready状态的线程。

    _KiQuantumEnd
    0008:804315B9  PUSH      EBP
    0008:804315BA  MOV       EBP,ESP
    0008:804315BC  PUSH      ECX
    0008:804315BD  PUSH      EBX
    0008:804315BE  PUSH      ESI
    0008:804315BF  PUSH      EDI
    0008:804315C0  MOV       EAX,DS:[FFDFF020]               ;KPRCB->EAX
    0008:804315C6  MOV       EDI,EAX                         ;KPRCB->EDI
    0008:804315C8  MOV       EAX,FS:[00000124]               ;Current Thread's KTEB->EAX
    0008:804315CE  MOV       ESI,EAX                         ;Current Thread's KTEB->ESI
    0008:804315D0  CALL      [__imp__KeRaiseIrqlToDpcLevel]  ;将IRQL提升到DISPATCH_LEVEL,学过
                                                             ;ddk的朋友应该都比较熟悉
    0008:804315D6  XOR       EBX,EBX
    0008:804315D8  MOV       [EBP-01],AL                     ;Save Old IRQL
    0008:804315DB  CMP       [ESI+6B],BL                     ;判断当前线程的Quantum
    0008:804315DE  JG        804315F2                        ;在Quantum小于等于0时获取NextThread
    0008:804315E0  MOV       EAX,[ESI+44]
    0008:804315E3  CMP       [EAX+69],BL
    0008:804315E6  JZ        80431608
    0008:804315E8  CMP       BYTE PTR [ESI+33],10
    0008:804315EC  JL        80431608
    0008:804315EE  MOV       BYTE PTR [ESI+6B],7F
    0008:804315F2  MOV       ESI,[EDI+08]                     ;KPRCB's NextThread->ESI
    0008:804315F5  CMP       ESI,EBX                          ;KPRCB's NextThread是否为空
    0008:804315F7  JNZ       80431601
    0008:804315F9  MOV       CL,[EBP-01]
    0008:804315FC  CALL      @KiUnlockDispatcherDatabase
    0008:80431601  MOV       EAX,ESI                          ;将NextThread返回
    0008:80431603  POP       EDI
    0008:80431604  POP       ESI
    0008:80431605  POP       EBX
    0008:80431606  LEAVE
    0008:80431607  RET
    0008:80431608  MOVSX     EDX,BYTE PTR [ESI+33]
           .
           .(代码很长,牵涉到调度算法,看来只能您自己去认真看看了)
           .            


   6.线程所属进程的KPEB(KTEB+22ch处)

    主要是更容易的在KTEB与KPEB间进行些数据交换。

    其实上面部分内部数据在Linux中也可以找到实现对应功能的体现,如Windows NT/2000中的Thread Quantum对应Linux task_struct中counter成员等等。我在<<浅析Windows NT/2000环境切换>>中就指出过Windows NT/2000实际上发生任务切换的情况只有两种,我也在上次给出了时间中断的部分代码,给出了SwapContext(主要是CR3切换)代码。Windows中的每一个进程都分别拥有私有的内存空间,私有的内核对象(句柄表Handle Table)等等,这些都是在环境切换的基础上实现的,也是一个操作系统Robustness and Reliability的基础。http://www.research.microsoft.com/中有很多文章对这有过验证,大可以翻翻看看。关于这部分的实现您还可以再看看如下的一些例程(限于篇幅我不再列出代码):

    ⊙ KiFindReadyThread
    ⊙ KiReadyThread
    ⊙ KiSwapThread
    ⊙ SwapContext

    其中SwapContext与KiSwapThread是系统真正切换的代码,是不是经常在stack trace中看到这个函数,在此你应该可以比较容易的明白了吧。(关于stack trace请参阅DDK Documentation中的Anatomy of a Stack Trace段或SoftICE Command Reference中的stack与thread命令的解释)

    另一种线程自动放弃执行的情况,跟踪KeWaitForSingleObject、KeSetEvent等就相对比较容易了,拿Event Object举个例子吧,由于知道Event的结构,在您的实验用机上您大可以随便更改DISPATCHER_HEADER中的SignalState成 员更改Object状态,看您要它是Clear还是Signalled了(上面我给出了如何用SoftICE实现)。甚至在您理解了我开头给出的代码(其 实说白了只是读出一双向链表中的数据,如果是在Linux中,我相信你也根本没有耐心看到这儿了),还有理解了KWAIT_BLOCK所定义Thread Wait List后,只要给您一内核调试器,我相信您想阻塞哪个线程就哪个线程了,加上理解了线程优先级后,您想让哪个线程多占用CPU时间都可以。不过我可不保 证您这时候机子是否还Robustness and Reliability了。

    还是顺便提一下,Windows NT/2000中调度代码运行在DISPATCH_LEVEL IRQL上,已防止通常运行在PASSIVE_LEVEL的普通代码对其的中断。

    Jeffrey Richter在<<PROGRAMMING APPLICATIONS for MICROSOFT WINDOWS,FOURTH EDITION>>中曾指出:

      ⊙ Microsoft doesn't fully document the behavior of the scheduler.
      ⊙ Microsoft doesn't let applications take full advantage of the scheduler's features.
      ⊙ Microsoft tells you that the scheduler's algorithm is subject to change so that you
can code defensively.

    从这几点看Microsoft并没将调度行为固定,实际上Windows NT 4.0与Windows 2000在调度算法上就有不同,而本文所提及的所有代码均取自或只在Windows 2000 Server Build 2195中测试过。

    我想对DDK有过学习的朋友应该都知道PsSetCreateProcessNotifyRoutine与 PsSetCreateThreadNotifyRoutine这两个让用户注册系统建立与删除进程或线程时调用回调函数的Fully Documented例程,知道它们就是Mark Russinovich的NTPMON的实现的最主要的两个函数吧。其实在Windows 2000中Microsoft还提供实现类似功能的与环境切换有关的函数,即KeSetSwapContextNotifyRoutine和 KeSetThreadSelectNotifyRoutine,不过这两个函数却是Undocumented的,并且只能在Windows 2000的特定版本上运行(请查阅MSDN Magazine)。

    在Linux中所有的代码都是公开的,但真正要理解调度代码还是很困难的,何况本身就比Linux复杂(我尚未浏览过Linux 2.4.X源码,我这样说纯系个人目前感觉)而且只能在一大堆汇编代码中搜寻的Windows NT/2000,其分析的难度真的是可想而知的。所以我希望您能将文中错误或遗漏说明的地方告诉我(tsu00@263.net),谢谢!
  
    参考资料:
      1.Jeffrey Richter
          <<PROGRAMMING APPLICATIONS for MICROSOFT WINDOWS,FOURTH EDITION>>
      2.Peter Viscarola and Anthony Mason <<WINDOWS NT DEVICE DRIVER DEVELOPMENT>>
      3.Numega <<SOFTICE COMMAND REFERENCE>>
      4.Windows 2000 DDK Documentation
      5.David Solomon and Mark Russinovich <<INSIDE MICROSOFT WINDOWS 2000,THIRD EDITION>
<< Windows多线程间同步事件的... / 浅析Windows NT/200... >>

专题推荐

不平凡的水果世界

不平凡的水果世界

平凡的水果世界,平凡中的不平凡。 今朝看水果是水果 ,看水果还是水果 ,看水果已不是水果。这境界,谁人可比?在不平凡的水果世界里,仁者见仁,智者见智。

中国春节的那些习俗

中国春节的那些习俗

正月是农历新年的开始,人们往往将它看作是新的一年年运好坏的兆示期。所以,过年的时候“禁忌”特别多。当然,各个地方的风俗习惯不一样,过年的禁忌也是不一样的。

评论
0/200
表情 验证码:

magichere

  • 文章总数0
  • 画报总数0
  • 画报点击数0
  • 文章点击数0
个人排行
        博文分类
        日期归档