Windows Internals 笔记——线程调度

孤街浪徒 提交于 2020-03-12 03:25:22

1.线程内核对象中的CONTEXT反应了线程上一次执行时CPU寄存器的状态。大约每隔20ms,Windows都会查看所有当前存在的线程内核对象。Windows在可调度的线程内核对象中选择一个,并将上次保存在线程上下文中的值载入CPU寄存器。这一操作被称为上下文切换。Windows实际上会记录每个线程的运行次数。

 

2.调用CreateProcess或者CreateThread时,系统将创建线程内核对象,并把挂起计数初始化为1。这样就不会给这个线程调度CPU了,因为线程初始化需要时间,我们不想再线程准备好之前就开始执行它。在线程初始化之后,CreateProcess或者CreateThread函数将查看是否有CREATE_SUSPENDED标志传入,如果有,函数返回并让新的线程处于挂起状态。如果没有,函数会将线程的挂起计数递减为0,线程就成为可调度的了。

 

3.通过创建一个处于挂起状态的线程,我们可以在线程执行任何代码之前改变它的环境(比如优先级)。之后可以调用ResumeThread函数使其变为可调度的,如果调用成功会返回线程的前一个挂起计数,否则返回0xFFFFFFFF。

 

4.还可以调用SuspendThread来挂起线程,任何线程都可以调用这个函数挂起另外一个线程(只要有线程句柄)。显然线程可以将自己挂起,但是它无法自己恢复。SuspendThread返回线程之前的挂起计数。一个线程最多可以挂起MAXIMUM_SUSPEND_COUNT(WinNT.H中定义为127)次。请注意,就内核模式下面执行情况而言,SuspendThread是异步的,但是在线程恢复之前,它是无法在用户模式下执行的。

 

5.调用SuspendThread时必须小心,因为试图挂起一个线程时,我们不知道线程在做什么,例如线程正在分配堆中的内存,线程将锁定堆,当其他线程要访问堆的时候,它们的执行将被终止,直到第一个线程恢复。所以可能会造成死锁。

 

6.Windows中不存在挂起和恢复进程的概念,因为系统从来不会给进程调度CPU时间。在一个特殊情况下,即调试器处理WaitForDebugEvent返回的调试事件时,Windows将冻结被调试进程中的所有线程。直至调试器调用ContinueDebugEvent。

 

7.调用Sleep函数可以告诉系统,在一定时间内自己不需要调度了。

  • 调用Sleep函数,将使线程资源放弃属于它的时间片中剩下的部分。
  • 系统设置线程不可调度的时间只是“近似于”所设定的毫秒数,实际情况取决于系统中其他线程的运行情况。
  • 给Sleep传递INFINITE参数,告诉系统永远不要调度这个进程,这样做没什么用。
  • 可以给Sleep传入0,这是在告诉系统,主调线程放弃了时间片的剩余部分,它强制系统调度其他线程。但是系统有可能重新调度刚刚调用Sleep的那个线程,如果没有相同或者优先级较高的可调度线程时,就会发生这样的事情。

 

8.调用SwitchToThread函数时,系统查看是否存在正急需CPU时间的饥饿线程,如果没有SwitchToThread立即返回,如果存在将调度该线程(其优先级可能比SwitchToThread的主调线程低)。饥饿线程可以运行一个时间量,然后系统调度程序恢复正常运行。

 

9.超线程处理器芯片有多个“逻辑”CPU,每个都可以运行一个线程。每个线程都有自己的体系结构状态(一组寄存器),但是所有线程共享主要的执行资源,比如CPU高速缓存。当一个线程终止时,CPU自动执行另一个线程,无需操作系统干预,只有在缓存未命中、分支预测错误和需要等待前一个指令的结果等情况下,CPU才会暂停。

 

10.在超线程CPU上执行旋转循环(spin loop)时,需要我们强制当前线程暂停,使另一个线程可以访问芯片的资源。x86体系结构支持一个名为PAUSE的汇编语言指令。PAUSE指令可以确保避免内存顺序违规,从而改进性能。在x86上,PAUSE指令等价于REP NOP指令。PAUSE会导致一定的延时(有些CPU上为0).在Win32 API中,x86 PAUSE指令是通过调用WinNT.h中定义的YieldProcessor宏发出的。

 

11.用GetTickCount()或GetTickCount64()来计算某项任务消耗时间的时候,其前提是该任务执行时不会被中断,但是Windows是抢占式的操作系统。在Windows Vista之前的系统中我们就可以使用GetThreadTimes()函数。GetProcessTimes的返回值是进程中所有线程(即使线程已经终止)的时间综总和。

 

12.在Windows Vista中,系统为线程分配CPU时间的方式发生了改变。操作系统不再依赖约10~15ms的间隔时钟计时器,而是改用处理器的64位时间戳计时器,它计算的是机器启动以来的时钟周期数。QueryThreadCycleTime()和QueryProcessCycleTime()函数分别返回给定线程或进程的所有线程所用的时钟周期数。

 

13.在Windows定义的所有数据结构中,CONTEXT结构是唯一一个特定于CPU的。所以它的成员的具体情况取决于Windows运行在什么CPU上。以下是x86 CPU的完整CONTEXT结构定义:

typedef struct _CONTEXT {

    //
    // The flags values within this flag control the contents of
    // a CONTEXT record.
    //
    // If the context record is used as an input parameter, then
    // for each portion of the context record controlled by a flag
    // whose value is set, it is assumed that that portion of the
    // context record contains valid context. If the context record
    // is being used to modify a threads context, then only that
    // portion of the threads context will be modified.
    //
    // If the context record is used as an IN OUT parameter to capture
    // the context of a thread, then only those portions of the thread's
    // context corresponding to set flags will be returned.
    //
    // The context record is never used as an OUT only parameter.
    //

    DWORD ContextFlags;

    //
    // This section is specified/returned if CONTEXT_DEBUG_REGISTERS is
    // set in ContextFlags.  Note that CONTEXT_DEBUG_REGISTERS is NOT
    // included in CONTEXT_FULL.
    //

    DWORD   Dr0;
    DWORD   Dr1;
    DWORD   Dr2;
    DWORD   Dr3;
    DWORD   Dr6;
    DWORD   Dr7;

    //
    // This section is specified/returned if the
    // ContextFlags word contians the flag CONTEXT_FLOATING_POINT.
    //

    FLOATING_SAVE_AREA FloatSave;

    //
    // This section is specified/returned if the
    // ContextFlags word contians the flag CONTEXT_SEGMENTS.
    //

    DWORD   SegGs;
    DWORD   SegFs;
    DWORD   SegEs;
    DWORD   SegDs;

    //
    // This section is specified/returned if the
    // ContextFlags word contians the flag CONTEXT_INTEGER.
    //

    DWORD   Edi;
    DWORD   Esi;
    DWORD   Ebx;
    DWORD   Edx;
    DWORD   Ecx;
    DWORD   Eax;

    //
    // This section is specified/returned if the
    // ContextFlags word contians the flag CONTEXT_CONTROL.
    //

    DWORD   Ebp;
    DWORD   Eip;
    DWORD   SegCs;              // MUST BE SANITIZED
    DWORD   EFlags;             // MUST BE SANITIZED
    DWORD   Esp;
    DWORD   SegSs;

    //
    // This section is specified/returned if the ContextFlags word
    // contains the flag CONTEXT_EXTENDED_REGISTERS.
    // The format and contexts are processor specific
    //

    BYTE    ExtendedRegisters[MAXIMUM_SUPPORTED_EXTENSION];

} CONTEXT;

CONTEXT结构分为几部分:

  • CONTEXT_CONTROL包含CPU的控制寄存器,比如指令指针、栈指针、标志和函数返回地址。
  • CONTEXT_INTEGER标识CPU的整数寄存器。
  • CONTEXT_FLOATING_POINT标识CPU的浮点寄存器。
  • CONTEXT_SEGMENTS标识CPU的段寄存器。
  • CONTEXT_DEBUG_REGISTERS标识CPU的调试寄存器。
  • CONTEXT_EXTENDED_REGISTERS标识CPU的扩展寄存器。

 

14.Windows实际上允许我们调用GetThreadContext查看线程的内核对象的内部,并获取当前CPU寄存器状态的集合。调用之前应先调用SuspendThread,否则,系统可能正好获得调度此线程,这样线程的上下文与所获取的信息就不一致了。

 

15.一个线程实际上有两个上下文:用户模式和内核模式。GetThreadContext只能返回线程的用户模式的上下文。如果调用SuspendThread暂停一个线程,但是该线程正在内核模式执行,那么它的用户模式上下文保持不变,即使SuspendThread实际上还没有暂停线程。但是,在线程恢复之前,不能再执行任何用户模式的代码,因此,我们完全可以认为线程已经暂停,这时调用GetThreadContext是非常安全的。

 

16.CONTEXT结构的ContextFlags成员与任何CPU寄存器都不对应。这个成员的作用是告诉GetThreadContext函数应该获取哪些寄存器。请注意在调用GetThreadContext之前,必须首先初始化CONTEXT结构的ContextFlags成员。

 

17.Windows还允许我们通过调用SetThreadContext来改变结构中的成员,并把新的寄存器放回线程的内核对象中。同样调用之前要先SuspendThread,否则结果无法预料。也必须再初始化CONTEXT的ContextFlags成员。

 

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!