Windows线程的调度和运行2

下面的指令“movl KTHREAD_APCSTATE_PROCESS(%ebx), %edi”使寄存器EDI指向了目标线程所属进程的KPROCESS数据结构。这个数据结构中有个ULONG数组LdtDescriptor[2],如果 其第一个元素的低16位非0就是一个有效的LDT段描述项,那就说明目标线程使用了LDT,因此就要把这个指向其LDT段的描述项设置到GDT中,其下标 为LDT_SELECTOR。同时还要通过指令lldt把这个下标作为段选择项装入到寄存器LDTR中。如前所述,LDTR实质上是个段寄存器,其结构与 普通的段寄存器相同。这就是说,它在CPU中有个16位的可见部分,还有个64位的隐藏部分。而lldt指令,则一方面把16位的段选择项置入LDTR的 可见部分,同时也从GDT中把相应的表项装入了LDTR的隐藏部分。这样:
l 当用户程序把FS、GS等段寄存器设置成使用LDT时(选择项中的表选择位为0表示使用GDT,为1表示使用LDT),根据LDTR就可以找到LDT,而不用再去访问GDT中的描述项。
l 根据置入FS、GS等段寄存器的选择项和LDT的内容,把LDT中的相应表项装入FS、GS等段寄存器的隐藏部分
l 于是,当用户程序通过FS、GS等段寄存器访问内存时,就无需再去访问LDT中的描述项。
注意在不使用LDT时装入LDTR的段选择项是0,而GDT中下标为0处是个非法段描述项,所以要是以后企图访问LDT(例如把段寄存器FS设置成使用LDT)就会导致异常。
我们再往下看。

[Ki386ContextSwitch()]

   /* Get the pointer to the old thread. */
   movl 12(%ebp), %ebx

   /* FIXME: Save debugging state. */

   /* Load up the iomap offset for this thread in preparation for setting it below. */
   movl KPROCESS_IOPM_OFFSET(%edi), %eax

   /* Save the stack pointer in this processors TSS */
   movl %fs:KPCR_TSS, %esi
   pushl KTSS_ESP0(%esi)

   /* Switch stacks */
   movl %esp, KTHREAD_KERNEL_STACK(%ebx)
   movl 8(%ebp), %ebx
   movl KTHREAD_KERNEL_STACK(%ebx), %esp
   movl KTHREAD_STACK_LIMIT(%ebx), %edi
   movl %fs:KPCR_TSS, %esi

   /* Set current IOPM offset in the TSS */
   movw %ax, KTSS_IOMAPBASE(%esi)

这里使EBX指向了老线程,其实要使用这个指针的地方还在后面。此时的EDI仍指向新线程(目标线程)所属进程的KPROCESS数据结构,而 KPROCESS_IOPM_OFFSET(%edi)就是KPROCESS结构中字段IopmOffset的内容。这个字段说明该进程的IO权限位图在 TSS中的位置,这里先把它装入了EAX,后面会把它写入TSS中的IoMapBase字段。
随后的指令“movl %fs:KPCR_TSS, %esi”把KPCR中的指针TSS装入ESI,使其指向了KTSS数据结构,接着就把这个结构中字段Esp0的当前值压入堆栈。如前所述,TSS中的这 个字段总是指向当前线程系统空间堆栈的原点,当一个线程不再成为当前线程时就得把它保存起来。保存在哪里呢?办法当然不止一种,例如保存在KTHREAD 结构中也未尝不可,而这里选择的是保存在堆栈中,那当然也可以。
至此,已经为堆栈的切换作好了准备,下面就是切换堆栈了,注意此时EBX指向老线程的KTHREAD结构。首先把此刻的堆栈指针记录在老线程 KTHREAD结构中的字段KernelStack中。这就是老线程在切换点上的系统空间堆栈指针。然后又使EBX指向新线程的KTHREAD结构,从中 恢复其保存着的堆栈指针,读者不妨回顾一下上一篇漫谈中所讲的这个字段的作用。注意这里的保存堆栈指针和恢复堆栈指针是分别针对两个不同线程、两个不同 KTHREAD数据结构的操作。
从现在起,程序就开始使用另一个线程的系统空间堆栈了。读者也许心中疑虑,就这么把堆栈换了,会不会给程序的运行带来断裂?不会的。对于老线程,已经在堆 栈上的内容或者是要到返回的时候才会用到,或者是在程序中需要这些数据时才会用到,但是那都发生在下一次当这个线程又被调度运行的时候。对于新线程,则下 面要用到的堆栈内容都是在上一次当这个线程被调度停止运行时压入堆栈的。
下一条mov指令把目标进程KTHREAD结构中字段StackLimit的值置入EDI,但是这似乎是多余的。注意此前EDI指向新线程的KPROCESS数据结构,现在则变成了新线程的StackLimit,可是后面没有看到此项数据的使用。
再下一条mov指令把KPCR结构中的指针TSS置入ESI,使其指向本CPU的KTSS数据结构。但是这似乎又是多余的,因为在切换堆栈的那几条指令的 前后FS的内容并未改变,GDT中的相应表项也未改变,又是在同一个CPU上,所以前后两条以ESI为目标的mov指令应该有着相同的效果。话虽如此,读 者要是看出这里面有甚么奥妙就请发个Email给作者。
下面是把当前进程的IopmOffset位移写入KTSS数据结构中的IoMapBase字段。这里寄存器EAX的内容在切换堆栈之前来自目标进程的 KPROCESS结构,现在则把它写入KTSS结构的IoMapBase字段中。这个字段的值是个16位的位移量,说明IO权限位图在TSS中的位置。这 样,KTSS数据结构中的IoMapBase就总是指向当前进程的IO权限位图,或者说明当前进程没有IO权限位图(如果IoMapBase为 0xffff)。需要说明的是,大部分Windows进程都没有IO权限位图,因而只有在内核中才能进行I/O操作;有IO权限位图的只是特殊的进程,一 般是运行于V86模式的进程。
系统空间堆栈的切换意味着CPU的执行已由老线程转到新进程,已经恢复了新线程在系统空间的运行。但是,除非是内核线程,一般而言新线程最后还得回到用户空间,而此刻的用户空间映射还是老线程的,所以还得切换用户空间的映射。继续往下看:

[Ki386ContextSwitch()]

   /* Change the address space */
   movl KTHREAD_APCSTATE_PROCESS(%ebx), %eax
   movl KPROCESS_DIRECTORY_TABLE_BASE(%eax), %eax
   movl %eax, %cr3

   /* Restore the stack pointer in this processors TSS*/
   popl KTSS_ESP0(%esi)

   /* Set TS in cr0 to catch FPU code and load the FPU state when needed
* For uni-processor we do this only if NewThread != KPCR->NpxThread */
#ifndef CONFIG_SMP
   cmpl %ebx, %fs:KPCR_NPX_THREAD
   je 4f
#endif /* !CONFIG_SMP */
   movl %cr0, %eax
   orl $X86_CR0_TS, %eax
   movl %eax, %cr0
4:
   /* FIXME: Restore debugging state */
   /* Exit the critical section */
   sti

   call @KeReleaseDispatcherDatabaseLock FromDpcLevel@0

   cmpl $0, _PiNrThreadsAwaitingReaping
   je 5f
   call _PiWakeupReaperThread@0
5:

   /* Restore the saved register and exit */
   popl %edi
   popl %esi
   popl %ebx

   popl %ebp
   ret

除非内核线程,每个线程都在其所属进程的空间中运行,因而使用某个特定的页面目录。不同页面目录中系统空间页面的映射都是相同的,所不同的是用户空间页面 的映射。至于内核线程则只有系统空间页面的映射,而没有用户空间页面的映射。所以,切换线程的时候也要切换页面目录。而页面目录属于进程,这里 KTHREAD_APCSTATE_PROCESS(%ebx)实际上是KTHREAD数据结构内部KAPC_STATE结构中的字段Process的 值。这是个指针,指向其所属进程的KPROCESS数据结构。把这个指针赋给EAX以后,KPROCESS_DIRECTORY_TABLE_BASE (%eax)就是KPROCESS数据结构中字段DirectoryTableBase的值,这又是个指针(物理地址),指向该进程的页面目录。把这个值 写入控制寄存器CR3,就引起了地址映射的切换,不过此刻的程序是在系统空间执行,而所有进程的系统空间都是相同的,所以这种切换并不影响程序的继续执 行,其作用要倒CPU回到用户空间时才表现出来。
下面从堆栈恢复TSS中的ESP0,这条pop指令与切换堆栈之前的push指令相对应,但却是针对不同的堆栈。前面的push指令是针对老线程的系统空 间堆栈,而后面的pop指令则是针对新线程的系统空间堆栈。而所谓“新线程”,很可能是从前的某一次线程切换中的“老线程”。也就是说,当一个线程不被执 行时,其ESP0存放在它的系统空间堆栈上,到被调度运行并切换时再把ESP0写入TSS。这样,CPU在需要从用户空间进入系统空间时才能知道当前线程 的系统空间堆栈在哪里。注意在修改了KTSS的某些内容后并不需要重新装入段寄存器TR,TR还是指向原来的地方,因为TSS还是原来的TSS,而所改变 的内容都是在实际用到时才由CPU到TSS中获取,例如Esp0就是要到CPU从用户空间进入系统空间时才会用到的。
此后的几条指令与浮点运算有关。控制寄存器CR0的一些标志位控制着CPU许多方面的运行状态,例如是否开启页面映射、是否启用高速缓存等等都是由CR0控制的。但是这里所关心的是其中与浮点运算有关的标志位X86_CR0_TS:

#define X86_CR0_TS   0x00000008   /* enable exception on FPU instruction for task switch */

这是一个控制/标志位,TS表示“Task Switched”,其作用是使浮点处理器FPU的上下文不必立即加以保存,因为新的目标线程在运行中未必会用到FPU,每次切换时都加以保存/恢复就造 成浪费。这里的程序中把这一位设成1以后,如果新的线程真的用到FPU,就会在首次使用FPU时导致一次异常,在相应的异常处理程序中再来处理FPU的切 换就可以了(详见Intel的软件开发手册第三卷)。
至此,线程切换的关键操作都已完成,可以打开中断了。后面的KeReleaseDispatcherDatabaseLockF romDpcLevel()显然是解锁,与其相对应的加锁操作在上一层的程序中,所以在这里看不到。至于PiWakeupReaperThread(), 则是唤醒一个内核线程,让它来“收割”那些已经退出运行的线程,实际上就是释放它们的数据结构。

我们不妨考察一下在这整个过程中的堆栈操作。首先所有的push操作和pop操作显然是平衡的、即数量相等,这在任何函数中都是一样。进一步,除少数例 外,绝大多数的push操作和pop操作也是配对的,例如前面有“pushl KTSS_ESP0(%esi)”,后面就有“popl KTSS_ESP0(%esi)”。最后,至关重要的是,在这个函数内部,这些push操作和pop操作实际上作用于两个不同的堆栈,即两个不同线程的系 统空间堆栈。所以,与前面的push操作配对的确实就是后面的那些pop操作,但是这些pop指令的执行却是“老线程”在下一次被调度运行、并因此而而执 行Ki386ContextSwitch()、变成了“新线程”时的时候。
新创建的线程是个特例。新建线程系统空间堆栈上的这些数据当然不可能是在切换线程时保存进去的,而是预先安排好的。我们再回顾一下Ke386InitThreadWithContext()中的这几行代码:

   KernelStack[0] = (ULONG)Thread->InitialStack - sizeof(FX_SAVE_AREA); /* TSS->Esp0 */
   KernelStack[1] = 0;    /* EDI */
   KernelStack[2] = 0;    /* ESI */
   KernelStack[3] = 0;    /* EBX */
   KernelStack[4] = 0;    /* EBP */
   KernelStack[5] = (ULONG)&PsBeginThreadWithContextInternal ; /* EIP */

比较一下前面的那些pop语句,就可以知道当新建线程被调度运行时被恢复到KTSS_ESP0(%esi)、即TSS中ESP0字段的是系统空间堆栈的原 点,即堆栈区间顶部减去一个FX_SAVE_AREA数据结构以后的边界上。而寄存器EDI、ESI、EBX、和EBP的初值则为0。
新建线程开始运行时TSS中的ESP0字段被设置成系统空间堆栈原点。这样,在CPU回到用户空间之后,如果发生中断、异常、或者系统调用,CPU就会把 TSS中的ESP0装入堆栈指针寄存器ESP,使其指向系统空间堆栈的原点。系统空间堆栈的SS也取自TSS,但是那实际上一经初始化以后便不再改变,所 有线程在系统空间都使用同一个堆栈段。从此以后,这个线程的系统空间堆栈原点就会永远保持下去,作为当前线程运行时这个数值在TSS的ESP0中,不运行 时则保存在其自己的系统空间堆栈中。
值得注意的还有KernelStack[5]、即返回地址,这是PsBeginThreadWithContextInternal 。本来,调用Ki386ContextSwitch()的地方是PsDispatchThreadNoLock(),从 Ki386ContextSwitch()返回时应该返回到那里去,但是那样就得在新建线程的堆栈上构建出包含多个函数调用框架的整个上下文,因为 PsDispatchThreadNoLock()又是受别的函数调用的。对于新建的线程,那样做既麻烦又无必要,所以这里让它抄近路“返回”到 PsBeginThreadWithContextInternal 。在那里,读者在前一篇漫谈中已经看到,稍作处理就直接跳转到了_KiServiceExit。
读者也许要问,这里保存在堆栈上的寄存器才那么几个,这就够了吗?是的。须知线程切换一定是在Ki386ContextSwitch()进行的,首先这是 在系统空间,所有寄存器在用户空间的内容都已经在进入系统空间时保存在陷阱框架中。而这些寄存器在线程切换前夕的内容,如果需要的话,也已经保存在系统空 间堆栈上,或者本来就在系统空间堆栈上(作为局部变量的值),或者保存在有关的数据结构中。所以到进入Ki386ContextSwitch()的时候实 际上已经没有什么需要保存的了,这里之所以要保存EDI、ESI、EBX、和EBP的值,只是因为在切换的过程中需要用到这几个寄存器。除此以外,真正需 要保存/恢复的数据其实只有一项,那就是KTSS_ESP0(%esi),因为每个线程的系统空间堆栈的位置是不同的。

明白了线程切换的过程,剩下来的问题是什么时侯切换。这不用说当然是调度的时侯切换,于是问题变成了什么时候调度。事实上,很多情况都会引起线程调度:
l 当前线程通过NtYieldExecution()系统调用自愿礼让。
l 当前线程在别的系统调用中因操作受阻而半自愿地交出运行权。
l 当前线程通过NtSetInformationThread()等系统调用改变了自身或其它线程/进程的优先级,使得自己可能不再具有最高的运行优先级。
l 当前线程通过NtSuspendThread()挂起其自身的运行。
l 当前线程通过NtResumeThread()恢复了其它线程的运行,使得自己可能不再具有最高的运行优先级。
l 当前线程通过进程间通信/线程间通信唤醒了别的进程,使得自己可能不再具有最高的运行优先级。
l 对时钟中断的处理发现当前线程已经用完时间配额,因而调度其它线程运行。
l 其它中断的发生导致某个/某些线程被唤醒,从而使得当前线程可能不再具有最高的运行优先级。
明白了在什么时侯调度,剩下的就是怎样调度、特别是根据什么准则调度的问题了。
下面我们通过一个实际的情景来解答这个问题。为简单起见,我们从系统调用NtYieldExecution()着手来看这整个过程。这个系统调用的作用是 使当前线程暂时放弃运行,但又不进入睡眠,实际上就是为别的线程让一下路,相当与Linux中的yield()。

NTSTATUS STDCALL
NtYieldExecution(VOID)
{
   PsDispatchThread(THREAD_STATE_READY);
   return(STATUS_SUCCESS);
}

由于只是暂时退让,当前线程并不被阻塞,其运行状态仍为THREAD_STATE_READY、仍处于就绪状态,而只是通过PsDispatchThread()启动一次线程调度,但是当前线程自己并不参与竞争。

[NtYieldExecution() > PsDispatchThread()]

VOID STDCALL PsDispatchThread(ULONG NewThreadStatus)
{
KIRQL oldIrql;

if (!DoneInitYet || KeGetCurrentPrcb()->IdleThread == NULL)
{
   return;
}
oldIrql = KeAcquireDispatcherDatabaseLock();
PsDispatchThreadNoLock(NewThreadStatus);
KeLowerIrql(oldIrql);
}

实际的调度是由PsDispatchThreadNoLock()完成的。调度的过程需要排它地进行,不能在中途又因为别的原因而再次进入调度的过程,所 以要对这整个进程加上锁。特别地,调度的过程中涉及许多队列操作,而队列操作是必须排它进行的。至于所谓Database,实际上就是指这些队列。要不 然,如果不加锁的话,例如要是中途发生了一次时钟中断,而时钟中断服务程序发现当前进程已经用完了时间配额,就又会启动线程调度,这就乱了套。而 PsDispatchThreadNoLock(),正如其函数名所示,是不管加锁这事的,所以这里要先加上锁。
但是读者也许会问,既然在调用PsDispatchThreadNoLock()之前先上了锁,那么理应在从这个函数返回以后开锁,怎么这里看不到呢?这 是因为当前线程对PsDispatchThreadNoLock()的调用并不是在完成了调度以后就立即返回,而要到它下一次又被调度运行时才会返回,中 间还夹着其它线程的运行。所以,到从PsDispatchThreadNoLock()返回的时侯才开锁,那就错了。

[NtYieldExecution() > PsDispatchThread() > PsDispatchThreadNoLock()]

VOID PsDispatchThreadNoLock (ULONG NewThreadStatus)
{
KPRIORITY CurrentPriority;
PETHREAD Candidate;
ULONG Affinity;
PKTHREAD KCurrentThread = KeGetCurrentThread();
PETHREAD CurrentThread =
                     CONTAINING_RECORD(KCurrentThread, ETHREAD, Tcb);

. . . . . .

CurrentThread->Tcb.State = (UCHAR)NewThreadStatus;
switch(NewThreadStatus)
{
    case THREAD_STATE_READY:
       PsInsertIntoThreadList(CurrentThread->Tcb.Priority, CurrentThread);
       break;
    case THREAD_STATE_TERMINATED_1:
       PsQueueThreadReap(CurrentThread);
       break;
}

Affinity = 1 << KeGetCurrentProcessorNumber();
for (CurrentPriority = HIGH_PRIORITY;
   CurrentPriority >= LOW_PRIORITY; CurrentPriority--)
{
    Candidate = PsScanThreadList(CurrentPriority, Affinity);
    if (Candidate == CurrentThread)
    {
       Candidate->Tcb.State = THREAD_STATE_RUNNING;
       KeReleaseDispatcherDatabaseLockF romDpcLevel(); 
       return;
    }
    if (Candidate != NULL)
    {
       PETHREAD OldThread;
       PKTHREAD IdleThread;

       DPRINT("Scheduling %x(%d)\n",Candidate, CurrentPriority);

       Candidate->Tcb.State = THREAD_STATE_RUNNING;

       OldThread = CurrentThread;
       CurrentThread = Candidate;
       IdleThread = KeGetCurrentPrcb()->IdleThread;

       if (&OldThread->Tcb == IdleThread)
       {
          IdleProcessorMask &= ~Affinity;
       }
       else if (&CurrentThread->Tcb == IdleThread)
       {
          IdleProcessorMask |= Affinity;
       }

       MmUpdatePageDir(PsGetCurrentProcess(),
            (PVOID)CurrentThread->ThreadsProcess, sizeof(EPROCESS));

       KiArchContextSwitch(&CurrentThread->Tcb, &OldThread->Tcb);
       return;
   }
}
CPRINT("CRITICAL: No threads are ready (CPU%d)\n", KeGetCurrentProcessorNumber());
PsDumpThreads(TRUE);
KEBUGCHECK(0);
}

一进入这个函数,就使局部量KcurrentThread指向当前线程的KTHREAD数据结构,并使CurrentThread指向相应的 ETHREAD数据结构。宏定义CONTAINING_RECORD根据指向数据结构内部成分的指针和外层结构的类型推算出指向外层结构的指针。实际上, KTHREAD数据结构是ETHREAD结构内部的第一个成分,所以这两个指针其实是一样的,不过这样做更为安全(万一有谁修改了ETHREAD的定 义)。
作为参数传下来的是当前线程的新的状态。如果当前线程被阻塞,那就是THREAD_STATE_BLOCKED,但是在我们这个情景中是THREAD_STATE_READY。
状态为THREAD_STATE_READY的线程应该挂入系统的就绪队列。所谓就绪队列,实际上是一组队列,每一种线程优先级都有一个队列。为此,内核 中有个LIST_ENTRY结构数组PriorityListHead[MAXIMUM_PRIORITY],数组的大小 MAXIMUM_PRIORITY定义为32,对应着32种不同的线程优先级。

[NtYieldExecution() > PsDispatchThread() > PsDispatchThreadNoLock()
> PsInsertIntoThreadList ()]

static VOID
PsInsertIntoThreadList(KPRIORITY Priority, PETHREAD Thread)
{
. . . . . .
InsertTailList(&PriorityListHead
, &Thread->Tcb.QueueListEntry);
PriorityListMask |= (1 << Priority);
}

显然,这是通过KTHREAD结构中的QueueListEntry将其挂入给定优先级的就绪队列。
与32个就绪队列相对应,内核中还有个位图PriorityListMask,只要某个优先级的就绪队列非空,这个位图中相应的标志位就设置成1。这样,只要看一下位图,就知道有没有某个优先级的线程在等待被调度运行了。
那么,不在就绪状态的线程怎么办呢?不在就绪状态的线程当然不在就绪队列中,但是仍通过ETHREAD结构中的ThreadListEntry链接在其所 属进程的线程队列中,这是在创建之初通过PsInitializeThread()中挂入这个队列的。所以,一个线程,不管是否就绪,其ETHREAD数 据结构总是挂在其所属进程的线程队列中。不过,到一个线程结束了运行、状态变成THREAD_STATE_TERMINATED_1的时候则又是特例,对 于这样的线程要通过PsQueueThreadReap()“收割”这个线程的数据结构。
还有个问题,挂入就绪队列的线程什么时候从队列中脱离出来呢?下面就会看到,当调度一个线程运行时,就把它的ETHREAD结构从就绪队列中摘除下来。正因为这样,前面才把状态为就绪的线程又挂回就绪队列。
回到PsDispatchThreadNoLock()的代码,下面就是调度了。在多处理器SMP结构的系统中,有些线程是指定只能在某个或某几个CPU 上运行的,这种关系叫做Affinity,即“亲和”,或者也可以说“绑定”。有关的信息以位图的形式记录在具体线程的KTHREAD数据结构中,所以在 调度时得要明确这是为哪一个CPU在做调度。
具体的调度就是按从高到低的次序扫描各个优先级的就绪队列,从中找出第一个愿意在目标CPU上运行的线程。这就是程序中那个for循环的作用。对于具体优先级的就绪队列,则通过PsScanThreadList()加以扫描。

[NtYieldExecution() > PsDispatchThread() > PsDispatchThreadNoLock() > PsScanThreadList()]

static PETHREAD PsScanThreadList(KPRIORITY Priority, ULONG Affinity)
{
PLIST_ENTRY current_entry;
PETHREAD current;
ULONG Mask;

Mask = (1 << Priority);
if (PriorityListMask & Mask)
{
   current_entry = PriorityListHead
.Flink;
   while (current_entry != &PriorityListHead
)
   {
      current = CONTAINING_RECORD(current_entry, ETHREAD,
                                                    Tcb.QueueListEntry);
      if (current->Tcb.State != THREAD_STATE_READY)
      {
         DPRINT1("%d/%d\n", current->Cid.UniqueThread, current->Tcb.State);
      }
      . . . . . .
      if (current->Tcb.Affinity & Affinity)
      {
          PsRemoveFromThreadList(current);
          return(current);
      }
      current_entry = current_entry->Flink;
   }
}
return(NULL);
}

就这样,按优先级从高到底的次序对每个就绪队列执行PsScanThreadList();一找到合适的线程,循环就结束了。
回到前面PsDispatchThreadNoLock()的代码,调度的结果无非是这么几种:
l 目标线程Candidate就是当前线程本身,这就不需要切换了。将其状态改回THREAD_STATE_RUNNING,就可以返回了。注意返回前的解锁操作。
l 目标线程Candidate就是空转线程IdleThread,而当前线程不是空转线程。此时将位图IdleProcessorMask中对应于当前CPU的标志位设成1,表示空转线程在本CPU上运行。
l 目标线程Candidate不是空转线程,而当前线程是空转线程。此时此时将位图IdleProcessorMask中对应于当前CPU的标志位清0,表示空转线程已不在本CPU上运行。
l 目标线程Candidate和当前线程都不是空转线程,这是常规的线程调度。
l 没有找到任何可以在此CPU上执行的线程,这一定是程序中发生了什么严重的错误,因为空转线程永远都是就绪的,而且每个CPU都有一个(每个CPU的 KRCB数据结构中的指针IdleThread指向其空转线程)。系统在这种情况下不能继续运行了,所以执行KEBUGCHECK()。
当然,在正常的情况下总是有线程可以运行的,所以下面就是切换的事了。这里有两步操作,第一步是对MmUpdatePageDir()的调用;第二步是宏 操作KiArchContextSwitch(),对于x386处理器这就是前面的Ki386ContextSwitch()。
先看对MmUpdatePageDir()的调用。其第一个参数是PsGetCurrentProcess(),注意这是指切换前的当前线程所属的进程。 第二个参数是CurrentThread->ThreadsProcess,这却是目标线程所属进程的EPROCESS结构指针。第三个参数则是 sizeof(EPROCESS)。调用这个函数的目的是要确保目标线程所属进程的ETHREAD数据结构在当前进程的页面映射表中有映射。每个进程都有 个页面映射表,而其所在的地址则在该进程的EPROCESS结构中。每个进程的EPROCESS结构都在物理页面中。但是,在ReactOS内核中(估计 在Windows内核中也是一样),一个进程的EPROCESS结构所占的物理页面却未必映射到别的进程的系统(虚存)空间。换言之,从一个进程的虚存空 间可能访问不到别的进程的EPROCESS结构。这在平时没有什么问题,但是在一些需要跨进程访问的特殊场合下就有问题了。需要跨进程访问的场合主要就在 线程切换的过程中以及需要进程挂靠的时候。
其实,在线程切换的过程中需要访问的并非“新”线程所属进程的整个EPROCESS结构,而是作为其一部分的KPROCESS结构。回顾一下前面Ki386ContextSwitch()的代码,就可以看到:
l 为设置LDTR,需要访问KPROCESS结构中的映像LdtDescriptor[2]。
l KPROCESS结构中的IopmOffset。
l 为设置CR3,需要访问KPROCESS结构中的字段DirectoryTableBase。
目标进程的EPROCESS结构所在页面在当前进程的页面映射表中未必有映射,如果不补上这些页面的映射,就会在访问这些数据的时候发生页面异常,所以需要先通过MmUpdatePageDir()补上这些映射。
注意这里说的是EPROCESS结构,而不是ETHREAD结构,后者无论在哪一个进程的页面目录中都是有映射的,要不然线程调度就没法做了。
现在已是万事俱备了,下面就是Ki386ContextSwitch(),这前面已经看过了。

从代码中可以看出,把线程挂入就绪队列的过程和从就绪队列中选择线程的过程都是很简单而直截了当的,实际上就是根据优先级、即KTHREAD结构中 Priority字段的值。有多个相同优先级的就绪线程时,则轮流(Roubd Robin)执行,因为把就绪线程挂入队列时总是挂在尾部。
线程的运行优先级可以通过系统调用加以改变,也可能因为别的原因而受到改变,但是那又属于另一个话题了
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
## Features ### Anti-debugging attacks - IsDebuggerPresent - CheckRemoteDebuggerPresent - Process Environement Block (BeingDebugged) - Process Environement Block (NtGlobalFlag) - ProcessHeap (Flags) - ProcessHeap (ForceFlags) - NtQueryInformationProcess (ProcessDebugPort) - NtQueryInformationProcess (ProcessDebugFlags) - NtQueryInformationProcess (ProcessDebugObject) - NtSetInformationThread (HideThreadFromDebugger) - NtQueryObject (ObjectTypeInformation) - NtQueryObject (ObjectAllTypesInformation) - CloseHanlde (NtClose) Invalide Handle - SetHandleInformation (Protected Handle) - UnhandledExceptionFilter - OutputDebugString (GetLastError()) - Hardware Breakpoints (SEH / GetThreadContext) - Software Breakpoints (INT3 / 0xCC) - Memory Breakpoints (PAGE_GUARD) - Interrupt 0x2d - Interrupt 1 - Parent Process (Explorer.exe) - SeDebugPrivilege (Csrss.exe) - NtYieldExecution / SwitchToThread - TLS callbacks ### Anti-Dumping - Erase PE header from memory - SizeOfImage ### Timing Attacks [Anti-Sandbox] - RDTSC (with CPUID to force a VM Exit) - RDTSC (Locky version with GetProcessHeap & CloseHandle) - Sleep -> SleepEx -> NtDelayExecution - Sleep (in a loop a small delay) - Sleep and check if time was accelerated (GetTickCount) - SetTimer (Standard Windows Timers) - timeSetEvent (Multimedia Timers) - WaitForSingleObject -> WaitForSingleObjectEx -> NtWaitForSingleObject - WaitForMultipleObjects -> WaitForMultipleObjectsEx -> NtWaitForMultipleObjects (todo) - IcmpSendEcho (CCleaner Malware) - CreateWaitableTimer (todo) - CreateTimerQueueTimer (todo) - Big crypto loops (todo) ### Human Interaction / Generic [Anti-Sandbox] - Mouse movement - Total Physical memory (GlobalMemoryStatusEx) - Disk size using DeviceIoControl (IOCTL_DISK_GET_LENGTH_INFO) - Disk size using GetDiskFreeSpaceEx (TotalNumberOfBytes) - Mouse (Single click / Double click) (todo) - DialogBox (todo) - Scrolling (todo) - Execution after reboot (todo) - Count of processors (Win32/Tinba - Win32/Dyre) - Sandbox k

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值