回炉重造之重读Windows核心编程-007-线程的调度、优先级与亲缘性

  Windows被设计成一个抢占式的操作系统,用某种算法来确定哪些线程应该在何时被调度和运行多长时间。每隔20ms左右,Windows就要查看当前所有线程的内核对象,找到可以被调度的一个,将它加载到CPU寄存器中。这个操作成为上下文切换。Windows实际上保存了一个记录,说明每个线程获得了多少次运行的机会。使用MicrosoftSpy++这个工具可以了解这个情况。

  一个线程随时可以停止运行,一个线程可以进行调度。可以对线程进行一定程度的控制,但是不能太多。不能保证一个线程做任何事。

7.1暂停和恢复线程的运行  

  线程内核对象的内部有一个值,用以指明线程的暂停计数。当调用CreateThread创建线程内核对象的时候,暂停初始化为1。这可以防止线程被调度到CPU中,让系统有机会做好充分的准备再执行线程。

  初始化完毕,CreateThread或者CreateProcess就要查看是否传递了CREATE_SUSPEND标志,如果有这个标志函数就返回,同时新线程处于暂停状态;如果没有这个标志,那么函数就将暂停计数递减为0,让线程处于可调度的状态。

7.2暂停和恢复进程的运行  

  对于WIndows来说,不存在暂停或恢复进程的概念,因为进程从来不会获得CPU时间。但是线程是如何被暂停的呢?Windows确实允许一个进程暂停另一个进程的所有线程的运行,但是从事暂停操作的进程必须是个调试程序,必须是调用WaitForDebugEvent和ContinueDebugEvent之类的函数。

  Windows本身是一个抢占式的操作系统,没有提供其他方法来暂停进程中所有线程的执行。

  虽然无法创建完美的SuspendProcess函数,但是可以创建一个该函数的实现代码,在许多条件下出色地运行:

VOID SupendProcess(DWORD dwProcessID, BOOL fSuspend) {
  // Get the list of thread in the process
  HANDLE hSnapShot = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, dwProcessID);
  if (hSnapShot != INVALID_HANDLE_VALUE){
    // Walk the list of thread
    THREADENTRY32 te = {sizeof(te)};
    BOOL fOk = Thread32First(hSnapShot, &te);
    while (fOk) {
      // is this the thread in the desire process
      if (te.th32OwnerProcessID == dwProcessID) {
        // convert id to handle
        HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME, FALSE, te.th32ThreadID);
        if (NULL != hThread) {
          // resume of suspend
          if (fSuspend)
            SuspendThread(hThread);
          else
            ResumeThread(hThread);
        }
        CloseHandle(hThread);
      }
    }
    CloseHandle(hSnapShot);
  }
}

  使用的OpenThread函数能够找出带有匹配线程ID的线程内核对象,递增内核对象的引用计数,返回这个句柄。运用这个句柄,就可以调用ResumeThread和SuspendThread了。

  SuspendProcess不一定总是能运行,原因是线程可以随时被创建和撤销,如果线程已经被撤销,调用CreateToolHelp32Snapshot不能保证找到所有线程;或者在调用函数后创建了一个线程。更坏的情况是枚举线程ID的时候,相继被撤销和创建的两个线程如果拥有同样的ID,这将会导致其中任意一个线程的运行。

7.3睡眠方式  

  当然线程也可以想要在某个时间段内不被调度,通过Sleep函数实现。

VOID Sleep(DWORD dwMillisecond);

  这个函数可以暂停自己的运行,直到dwMillisecond那么多的时间结束。需要注意的是:

  1. Sleep函数可以让当前的线程放弃自己的时间片。
  2. 系统将在大约指定的毫秒数内使得线程不可调度,但这不意味着过了指定时间线程一定被唤醒;
  3. 如果给dwMillisecond参数传递INFINITE,系统将不会再调度这个线程。这不是个好事,会让资源没有办法释放。
  4. 如果给函数传递0,这将告诉系统当前线程将放弃当前的时间片,让系统有机会调度其他的线程。当然没有其他线程的话就重新调度自己。

7.4转换到另一个线程  

  系统提供了另一个函数使得另一个可调度程序能够运行:

BOOL SwitchToThread(();

  当调用这个函数的时候:

  • 系统要查看是否穿在一个迫切需要CPU时间的线程

    • 如果没有线程需要时间片,函数就会立即返回
    • 如果有线程需要时间片,函数就会调度该线程。

  这个函数允许一个需要资源的线程强制另一个优先级较低、目前却拥有资源的线程放弃该资源。

  和Sleep和相似。

7.5线程的运行时间 

  有的时候想要计算一个线程执行一个任务需要多长的时间,常见的方法是这样:

DWORD dwStartTime = GetTickCount();
// Perform complex algorithm here

// Subtract start time from current time to get duration
DWORD dwElapsedTime = GetTickCount - dwStart;

  这个方法有个银行的假设:当前进程不会被中断。但是在抢占式系统中,永远不能保证一个线程何时被调度。我们需要一个函数,用来计算线程运行的CPU时间的数量。幸运的是,Windows确实提供了这样的一个函数:

BOOL GetThreadTimes(
   HANDLE hThread,
   PFILETIME  pftCreationTime;
   PFILETIME  pftExitTime;
   PFILETIME  pftKernelTime;
   PFILETIME  pftUserTime
);

  通过这个函数,就可以用下面的代码确定复杂算法需要的时间量了。

__int64 FileTimeToQuadWord(PFILETIME pft) {
  return (Int64ShllMod32(pft->dwHighDateTime, 32) | pft->dwLowDateTime);
}
void PerformLongOperation() {
  FILETIME ftKernelTimeStart, ftKernelTimeEnd;
  FILETIME ftUserTimeStart, ftUserTimeEnd;
  FILETIME ftDummy;
  __int64 dwKernelTimeElapsed, dwUserTimeElapsed, qwTotalTimeElapsed;
  // Get Start Time
  GetThreadTimes(GetCurrentThread(), &ftDummy, &ftDummy,
    &ftKernelTimeStart, &ftKernelTimeEnd);
  // Perform complex algorithm
  // Get End Time
  GetThreadTimes(GetCurrentThread(), &ftDummy, &ftDummy,
    &ftKernelTimeStart, &ftKernelTimeEnd);
  dwKernelTimeElapsed = FileTimeToQuadWord(&ftKernelTimeEnd) -
    FileTimeToQuadWord(&ftKernelTimeStart);
  dwUserTimeElapsed = FileTimeToQuadWord(&ftUserTimeEnd) -
    FileTimeToQuadWord(&ftUserTimeStart);
  // Get the time duration by the kernel and user
  qwTotalTimeElapsed = dwUserTimeElapsed + dwKernelTimeElapsed;
}

  GetProcessTimes是个类似的函数,使用与进程中的所有线程:

BOOL GetProcessTimes(
    HANDLE hProcess,
    PFILETIME pftCreationTime,
    PFILETIME pftExitTime,
    PFILETIME pftKernelTime,
    PFILETIME pftUserTime
)

  这个函数返回的时间适用于某个进程中的所有线程(包括已经终止的)。

  对于高分辨率的配置文件来说,GetThreadTimes并不完美,Windows提供了另一些特地用于高分辨率性能的函数:

BOOL QueryPerformanceFrequency(LARGE* pliFrequency);
BOOL QueryPerformanceCounter(LARGE* pliCount);

7.6运用环境结构

    环境结构的重要性现在已经不言而喻了,没有它,当然下次线程被调度的时候,就找不到它上次中断的地方。这样底层的数据结构一般不会完整地记录在PlatformSDK文档中。只是文档并没有说明结构的成员,也没有描述成员是谁,毕竟这些取决于系统运行在哪个CPU上。

  这个结构CONTEXT可以分为若干个部分。CONTEXT_CONTROL包含CPU的控制寄存器,CONTEXT_INTERGER用于标识CPU的整数寄存器。CONTEXT_FLOATING_POINT用于标识浮点寄存器,CONTEXT_SEGEMENT用于表示CPU的段寄存器,CONTEXT_DEBUG_REGISTER用于表示调试寄存器,CONTEXT_EXTENDED_REGISTER用于标识扩展寄存器。

  要获取这个结构,只需要调用GetThreadContex函数:

BOOL GetThreadContext(
    HANDLE hThread;
   PCONTEXT pContext
);

  在调用这个函数之前,应该调用SuspendThread,否则线程可能被调度,而且线程的环境可能与你回收的不同。线程其实有两个环境,一个是用户方式,一个是内核方式。GetThreadContext只能返回用户方式环境,如果调用它用来停止线程的运行,而线程又在内核方式运行,那么即使SuspendThread尚未暂停该线程的运行,他的用户方式仍然处于稳定状态。线程在恢复用户方式前,无法执行更多的用户方式代码,因此可以放心地把线程视为处于暂停状态,GetThreadContext可以正常运行。

  ContextFlags成员用于指明你想用函数GetThreadContext获取哪些寄存器。只是注意要初始化ContextFlags。

  GetThreadContext和SetThreadContext函数使你能够对线程进行多方面的控制,只是使用的时候要小心。实际上几乎没有应用程序调用这些函数,增加它们只是为了增强调试程序和其他工具的功能,任何程序都可以使用它们。

7.7线程的优先级 

  为数众多的线程被赋予许多不同的优先级,这影响到调度程序将哪个线程取出来作为下个要运行的线程。

  每个线程都会被赋予一个从0(最低)到31(最高)的优先级号码。这样系统就可以以这个数字从大到小的顺序调度线程。系统会尽量使得计算机保持繁忙的状态。

  那是不是低优先级的线程就永远得不到机会运行了呢?不过在任何一个时段内,系统中的大多数线程是不能调度的,其他空闲线程就有机会被调度。

  假如高优先级线程抢在低优先级线程之前运行,不管低优先级线程正在运行什么。

  还有当系统引导时,它会创建一个特殊的线程,称为0页线程。它的优先级被设置为0,是整个系统中唯一的在优先级0上运行的线程。当系统没有任何线程需要执行操作时,0页线程负责将系统中的所有空闲RAM页面置0。

7.8对优先级的抽象说明

  线程的调度算法对用户运行的应用程序类型有相当大的影响,随着系统用途的变化,调度的算法也不断改变,而Windows又要保证软件在将来的系统版本上运行。那么Microsoft如何改变系统的工作方式而仍然抱着软件能够运行呢?下面是一下方法:

  • 没有将调度程序的行为特性完全固定下来。
  • 没有让应用程序充分利用调度程序的特性。
  • 声称调度程序的算法是变化的,在编写代码时应有所准备。

  因此WindowsAPI展示了系统调度程序上的一个抽象层,这样就永远不会直接与调度程序通信。Windows支持6个优先级类:空闲、低于正常、正常、高于正常、高和实时。当然, 正常优先级是最常用的,99%的应用程序使用它。

  当系统什么都不做的时候,使用空闲优先级是最合适不过的了。只有在绝对必要的时候,才可以使用高优先级,例如Windows Explorer就在按组合键或者操作的时候被唤醒,否则它就处于暂停状态,大部分情况下都会抢在其他线程前运行。

  应该避免使用实时优先级类。如果使用它很有可能会干扰操作系统的运行,毕竟大多数操作系统线程均以较低的优先级在运行,必要的磁盘IO和网络信息的产生可能会别阻止。还有鼠标和键盘的输入不会得到及时的处理。

  一旦选定了优先级类别后,就不必考虑应用程序之间的关系了,只需要集中考虑你的应用程序中的各个线程。Windows支持7个相对的线程优先级:空闲、最低、低于正常、正常、高于正常、最高和关键时间优先级。这些优先级是相对于进程的优先级类而言的,大多数线程都使用正常线程优先级。

  开发人员不必具体设置优先级,相反,系统负责将线程的优先级类和进程的优先级类映射到一个优先级上。

7.9程序的优先级 

  进程是如何被赋予优先级类的呢?当调用CreateProcess时,可以在fdwCreate参数中传递需要的优先级类:

  1. 实时:REALTIME_PRIORITY_CLASS
  2. 高:HIGH_PRIORITY_CLASS
  3. 高于正常:ABOVE_NORMAL_PRIORITY_CLASS
  4. 正常:NORMAL_PRIORITY_CLASS
  5. 低于正常:BELOW_PRIORITY_CLASS
  6. 空闲:IDLE_PRIORITY_CLASS

  创建子进程的进程负责选择子进程运行的优先级别,这看起来有点奇怪。可是一旦子进程被创建完毕,就能用SetPriorityClass函数改变自己的优先级:

BOOL SetPriorityClass(
   HANDLE hProcess,
   DWORD fdwProirity);

  fdwProirity参数可以在进程优先级的类别中选择。再加上hProcess的访问权限,就能改变任何进程的优先级类。把自己变成空闲线程的方式:

SetPRiorityClass(
    GetCurrentProcess(),
    IDLE_PRIORITY_CLASS);

  获取进程的优先级GetPriorityClass,返回进程的优先级类别。

  如果使用命令外壳启动程序,程序的优先级是正常优先级。但是如果使用start命令启动程序,就是空闲优先级别了,除非你不使用/BELOWNORMAL、/NORMAL、/ABOVENORMAL、/HIGH和/REALTIME等开关。当然还可以用SetPriorityClass设定优先级,Windows的任务管理器就有这个功能。

  当线程被创建的时候,它的优先级总是被设置为正常优先级。奇怪的是,CreateThread没有为调用者提供 一个设置新线程的相对优先级的方法,只有设置优先级的。

BOOL SetThreadPriority(
    HANDLE hThread,
    int nPriority
);int GetThreadPriority(  HANDLE hThread);

  优先级别也和进程的类别大致相同。还有就是检索线程的相对优先级别的补充函数GetThreadPriority。如果要创建优先级为空闲的线程,可以执行以下的代码:

DWORD dwThreadID;
HANDLE hThread = CreateThread(
    NULL, 0, ThreadFunc, NULL, CREATE_SUSPEND, &dwThreadID
);ResumeThread(hThread);CloseHandle(hThread);

7.9.1动态提高线程的优先级等级 

  综合考虑进程和线程的优先级后,系统就可以确定线程的优先级别了,这也称为基本优先等级。假定一个线程的基本优先级别是13,然后某种操作导致13升级到了15;当操作结束后,优先级就从15将为14了。下一个时间片按照13来执行。

  现在看来系统动态提高线程优先级的功能对他们的线程性能会产生一种不良的影响,为此Windows增加了以下就个函数,使得系统的动态提高线程有限等级的功能失效了:

BOOL SetProcessPriorityBoost()
    HANDLE hProcess,
    BOOL DisablePriorityBoost
;
BOOL SetThreadPriorityBoost()
    HANDLE hProcess,
    BOOL DisablePriorityBoost
;

BOOL GetProcessPriorityBoost()
    HANDLE hProcess,
    BOOL DisablePriorityBoost
;
BOOL GetThreadPriorityBoost()
    HANDLE hProcess,
    BOOL DisablePriorityBoost
;

7.9.2为前台进程调整调度程序 

  如果需要前台进程比后台线程有更强的相应能力,Windows就要为前台进程中的线程调整其调度算法。

7.9.3SchedulingLab示例应用程序 

  见代码清单。

7.10亲缘性 

  按照默认设置,当系统将线程分配给处理器时,Windows使用软亲缘性进行操作。让现场留在单个处理器上,有助于重复使用仍然在处理器的内存高速缓存中的数据。

  系统在引导时操作系统需要确定有多少个CPU可以是用,通过调用GetSystemInfo函数,应用程序就能查询到机器中的CPU数量按照默认的设置,任何线程都可以调度到这些CPU中的任意一个上去运行。为了限制在可用CPU的子集上的运行当个进程的线程数量,可以使用SetProcessAffinityMask:

BOOL SetProcessAffinityMask(
    HANDLE hProcess,
    DWORD_PTR dwProcessAffinityMask
);

  第一个参数是进程句柄,第二个参数是个位屏蔽,用以指明进程在那个CPU上运行。

  子进程可以继承进程的亲缘性。

  当然,还有一个函数可以返回位屏蔽,那就是GetProcessAffinityMask,它还能返回系统的亲缘性位屏蔽。

原文地址:https://www.cnblogs.com/leoTsou/p/12375618.html

时间: 2024-08-29 08:10:22

回炉重造之重读Windows核心编程-007-线程的调度、优先级与亲缘性的相关文章

回炉重造之重读Windows核心编程-006-线程

线程也是有两部分组成的: 线程的内核对象,操作系统用来管理线程和统计线程信息的地方. 线程堆栈,用于维护现场在执行代码的时候用到的所有函数参数和局部变量. 进程是线程的容器,如果进程中有一个以上的线程,这些线程将共享进程的地址空间,操作空间中的数据,执行相同的代码,对相同的数据操作,甚至内核对象句柄(因为它是依托进程而不是线程存在的). 所以进程使用的系统资源比线程多的多,线程只需要一个内核对象和一个堆栈.既然线程比进程需要的开销少,因此始终都应该设法用增加线程来解决编程问题.当然这也不是一成不

回炉重造之重读Windows核心编程-003-内核对象

内核对象是个比较难理解的概念,问题的根源就在于即使是<核心编程>书中也没有说清楚它的定义,只是不停地举例和描述它的性质,还有如何使用. 盲人摸象,难见全貌.只能尽可能列举它的性质,注意使用了. 引用计数(书中的说法是使用计数)就是内核对象的一个很关键的性质.由于内核对象的拥有者是内核而不是进程,所以只能由内核来做撤销内核对象的操作.而通常一个内核对象不一定只被一个进程使用的,创建或者撤销内核对象,就要看引用计数了.引用计数在内核对象被创建的时候被置为1,被进程访问一次引用计数就递增1.当引用计

回炉重造之重读Windows核心编程-001-错误处理

Windows处理错误靠的是API的返回值,类型不止一种种: VOID,函数不可能失败,Windows API的返回值很少是这个情况. BOOL,如果函数失败,则返回值是0,否则返回是非零值.不要测试返回值是否为TRUE! HANDLE,如果函数失败,则返回值通常是NULL,否则返回一个HANDLE用于操作对象.有的函数是返回INVALID_HANDLE_VALUE的,它被定义为-1,以函数在文档中的说明为标准! PVOID,如果函数失败,则返回NULL,否则返回内存块的地址. LONG/DWO

回炉重造之重读Windows核心编程-011-线程池和其他异步方式

线程池的使用 多线程应用程序很难设计,有两大难点,一是要管理线程的创建和撤销,再就是要对线程访问资源时实施同步.同步的工具有几个了.为了应对线程频繁地创建和撤销,线程池这个方案被放上了台面.Windows2000提供了一些新的线程池函数,使得线程的创建.撤销和基本管理更加容易.线程池的实现不拘一格,只要遵循以下的要点: 异步调用函数. 按照规定的时间间隔调用函数. 当单个内核对象变为已通知状态时调用函数. 当异步IO请求完成时调用函数. 为了完成这些操作,线程池由4个独立的部分组成.下面shix

回炉重造之重读Windows核心编程-014-虚拟内存

14.1 系统信息 GetSystemInfo函数用户检索与主机相关的值,只需要传递SYSTEM_INFO结构体的地址即可. typedef struct _SYSTEM_INFO{ union{ DWORD dwOemId; struct { WORD wProcessorArchiteture; WORD wReserved; }; }; DWORD dwPageSize; LPVOID lpMinimumApplicationAddress; LPVOID lpMaximumApplica

【windows核心编程】线程局部存储TLS

线程局部存储TLS, Thread Local Storage TLS是C/C++运行库的一部分,而非操作系统的一部分. 分为动态TSL 和 静态TLS 一.动态TLS 应用程序通过调用一组4个函数来使用动态TLS, 这些函数实际上最为DLL所使用. 系统中的每个进程都有一组 正在使用标志(in-use flag), 每个标志可被设置为FREE 或者 INUSE, 表示该TLS元素是否正在使用. 微软平台保证至少有TLS_MINUMUM_AVALIABLE个标志位可供使用, TLS_MINUMU

【转】《windows核心编程》读书笔记

这篇笔记是我在读<Windows核心编程>第5版时做的记录和总结(部分章节是第4版的书),没有摘抄原句,包含了很多我个人的思考和对实现的推断,因此不少条款和Windows实际机制可能有出入,但应该是合理的.开头几章由于我追求简洁,往往是很多单独的字句,后面的内容更为连贯. 海量细节. 第1章    错误处理 1.         GetLastError返回的是最后的错误码,即更早的错误码可能被覆盖. 2.         GetLastError可能用于描述成功的原因(CreatEvent)

【windows核心编程】DLL相关(3)

DLL重定向 因为DLL的搜索路径有先后次序,假设有这样的场景:App1.exe使用MyDll1.0.dll, App2.exe使用MyDll2.0.dll, MyDll1.0 和 MyDll2.0是同一个DLL的两个版本,1.0为旧版本,2.0为新版本. 而如果MyDll2.0.dll的存放路径的优先次序比较靠前时,那么App1.exe就会去加载MyDll2.0.dll,这就可能引发 DLL地狱问题,因此DLL重定向可解决这个问题. 加载程序总是先检查应用程序目录,我们所要做的就是如下: ①在

windows核心编程 DLL技术 【转】

注:本文章转载于网络,源地址为:http://blog.csdn.net/ithzhang/article/details/7051558 本篇文章将介绍DLL显式链接的过程和模块基地址重定位及模块绑定的技术. 第一种将DLL映射到进程地址空间的方式是直接在源代码中引用DLL中所包含的函数或是变量,DLL在程序运行后由加载程序隐式的载入,此种方式被称为隐式链接. 第二种方式是在程序运行时,通过调用API显式的载入所需要的DLL,并显式的链接所想要链接的符号.换句话说,程序在运行时,其中的一个线程