读Windows核心编程 - 7

         windows是抢占式的,我们无法保证线程在某个事件的某个时间段内开始运行。系统只调度可以调度的线程,但实际情况是,系统中的大多数线程是不可调度的。除了暂停的线程外,还有其他许多线程也是不可调度的,因为它们正在等待某些事情的发生。例如,如果运行Notepad,但是并不输入任何数据,那么Notepad的线程就没有什么事情要做。系统不给无事可做的线程分配CPU时间。当我们移动Notepad窗口或者输入的时候系统就会自动使Notepad线程成为可调度线程。但这并不意味着Notepad线程立即获得CPU时间。

        当调用CreateProcess或CreateThread后,就创建了线程的内核对象,并且它的暂停计数为1。当线程初始化结束后,CreateProcess或CreateThread要查看是否已经传递了CREATR_SUSPENDED标志,如果没有传递,暂停计数变为0。当暂停计数变为0的时候,除非线程正在等待他们某种事情的发生,否则该线程处于可调度状态。要使线程从不可调度变成可调度状态,可以调用ResumeThread函数,将线程句柄传递给它。它将返回线程前一个暂停计数。注意,如果一个线程暂停了三次,那么必须调用三次ResumeThread函数才可使线程变为可调度状态。除了使用CREATE_SUSPENDED标志外,还可以使用SuspendThread函数来暂停线程的运行。不用说,线程可以自行暂停运行,但是不能自行恢复运行。与ResumeThread一样,SuspendThread也返回前一次暂停计数。在实际环境中,调用SuspendThread需要小心,因为线程可能正试图从堆里分配内存,那么该线程将在该堆上设置一个锁。当其他线程试图访问该堆时,这些线程的访问就被停止,直到第一个线程恢复运行。

        一般情况下,windows不允许一个进程暂停另一个进程所有线程的运行。但是有一种特殊情况,就是从事暂停操作的进程必须是个调试程序。特别是,进程必须调用WaitForDebugEvent和ContinueDebugEvent之类的函数。虽然无法创建绝对完美的SuspendProcess函数,但是可以创建一个不那么完美的SuspendProcess函数:

VOID SuspendProcess(DWORD dwProcessID, BOOL fSuspend) {
   
// Get the list of threads in the system.
   HANDLE hSnapshot = CreateToolhelp32Snapshot(
      TH32CS_SNAPTHREAD, dwProcessID);
   
if (hSnapshot != INVALID_HANDLE_VALUE) {
      
// Walk the list of threads.
      THREADENTRY32 te = sizeof(te) };
      BOOL fOk 
= Thread32First(hSnapshot, &te);
      
for (; fOk; fOk = Thread32Next(hSnapshot, &te)) {
         
// Is this thread in the desired process?
         if (te.th32OwnerProcessID == dwProcessID) {
            
// Attempt to convert the thread ID into a handle.
            HANDLE hThread = OpenThread(THREAD_SUSPEND_RESUME,
               FALSE, te.th32ThreadID);
            
if (hThread != NULL) {
               
// Suspend or resume the thread.
               if (fSuspend)
                  SuspendThread(hThread);
               
else
                  ResumeThread(hThread);
            }

            CloseHandle(hThread);
         }

      }

      CloseHandle(hSnapshot);
   }

}

         OpenThread函数负责找出带有匹配线程ID的线程内核对象,对内核对象的使用计数进行递增。这个函数是windows2000后才引入的。但是SuspendProcess并不总是可行的,当我调用了CreateToolhelp32Snapshot后,一个新进程可能出现在目标进程中,那么我的函数将无法暂停这个线程。更有甚者,如果正好调用了CreateToolhelp32Snapshot之后,一个线程撤销了,又一个线程创建了,而新创建的线程的ID跟被撤销线程的ID一样,那么我们就有可能暂停一个目标进程之外的线程。

我们可以调用Sleep函数使线程暂停自己的运行,调用该函数有几个重要问题值得注意:

1. 调用Sleep,可使线程自愿放弃剩余的时间片。

2. 系统将在大约的指定毫秒数内使线程不可调度。

3. 调用Sleep,并且将参数设为INFINITE,就告诉了系统用于不要调度该线程。这不是一件值得去做的事。

4. 可以将0传递给Sleep。这将告诉系统,调用线程将释放剩余的时间片,并迫使系统调用另外一个线程。

        系统提供了一个称为SwitchToThread函数,使得另一个线程可调度。当调用这个函数的时候,系统要查看是否存在一个迫切需要CPU时间的线程。如果没有线程迫切需要CPU时间,SwitchToThread就会立即返回。如果存在一个迫切需要CPU的线程,SwitchToThread就对该线程进行调度。该函数允许一个需要资源的线程强制另一个优先级较低,而目前却拥有该资源的线程放弃该资源。如果调用SwitchToThread函数时没有其他线程可以运行,那么该函数返回FALSE。它与Sleep函数的差别是,SwitchToThread允许优先级较低的线程运行。

        有时我们需要计算某个任务的执行时间。许多人采取的方法是编写类似下面的代码:

DWORD dwStartTime = GetTickCount(); ......DWORD dwElapsedTime = GetTickCount - dwStartTime;

但是这个代码做了一个假设:即它不会被中断。但是,在抢占式系统中,系统无法知道线程何时被赋予CPU时间。我们需要一个函数,以便返回线程得到的CPU时间的数量。幸运的是,windows提供了一个称为GetThreadTimes的函数:

BOOL GetThreadTimes(
     HANDLE hThread,                 
     PFILETIME pftCreationTime,  
//线程创建时间
     PFILETIME pftExitTime,          //线程退出时间
     PFILETIME pftkKernelTime,    //线程执行的操作系统代码已经经过了多少个100ns的CPU时间
     PFILETIME  fptUserTime);       //线程执行的应用程序代码已经经过了多少个100ns的CPU时间

         同样,还有一个GetProcessTimes函数来获得进程中所有线程的时间信息。例如,返回内核时间是指所有进程中线程在内核代码中经过的全部时间的总和。对于高分辨率的配置文件来说,GetThreadTimes并不完美。windows确实提供了一些高分辨率性能的函数(具体用法参看核心编程page148):

BOOL QueryPerformanceFrequency(LARGE_INTEGER* pliFrequency);

BOOL QueryPerformanceCounter(LARGE_INTEGER* pliCount);

        接下来要说的是前面有一章中提过的线程上下文,也就是CONTEXT结构。CONTEXT结构是windows定义的数据结构中唯一一个特定于CPU的数据结构。比如,如果在x86计算机上,数据成员是Eax,Ebx,Ecx,Edx等等。而在Alpha处理器上,那么数据成员包括IntV0,IntT0,IntT1,IntRa,IntZero等等。CONTEXT结构可以包含若干部分:

CONTEXT_CONTROL:包含CPU控制寄存器,比如指令指针,堆栈指针,标志和函数返回地址。

CONTEXT_INTEGER:用于标识CPU的整数寄存器。CONTEXT_FLOATING_POINT:浮点寄存器。

CONTEXT_SEGMENTS:CPU段寄存器(仅x86)。CONTEXT_DEBUG_REGISTER:CPU调试寄存器。(仅x86)

CONTEXT_EXTENDED_REGISTERS:CPU扩展寄存器。(仅x86)

        windows允许查看线程内核对象的内部情况,以便抓取它当前的一组CPU寄存器。若要执行这项操作,只需调用GetThreadContext(HANDLE hThread, PCONTEXT pContext)函数。在调用CetThreadContext之前,应该调用SuspendThread,否则,线程可能被调度,而且线程的环境可能与你得到的不同。一个线程实际上有两个环境,一个是用户方式,一个是内核方式。GetThreadContext只能返回线程的用户方式环境。如果调用SuspendThread来停止线程的运行,但是该线程目前正在调用内核方式运行,那么,即使SuspendThread实际上尚未暂停该线程的运行,它的用户方式扔处于稳定状态。线程在恢复用户方式之前,它无法执行更多的用户方式代码,因为可以放心的将线程视为暂停状态,GetThreadContext能正常运行。(?)

        调用GetThreadContext之前,必须对CONTEXT结构中的ContextFlags成员进行初始化,指明你想要获得的寄存器信息,比如你想要获得控制寄存器和整数寄存器,应该像下面这样对ContextFlags进程初始化:

Context.ContextFlags = CONTEXT_CONTROL | CONTEXT_INTEGER;然后在调用GetThreadContext(hThread, &Context);另外windows还提供了SetThreadContext函数使你能将修改后的CONTEXT结构放回线程的内核对象中。同样,调用该函数前应该先调用SuspendThread并对ContextFlags进行初始化。虽然这两个函数能使你对线程进程许多方面的控制,但是在使用时应该非常小心,针对不同的CPU可能要写不同的代码。如:#if defind(_ALPHA_) Context.Fir = 0x00010000; #elif defined(_x86_) Context.Eip = 0x00010000 #else...实际上,几乎没有应用程序调用这些函数。增加这些函数是为了增强调试程序和其他工具的功能。

优先级云云:

        如果所有线程的优先级都相同,那么每个线程只运行20ms,然后系统将调度另外一个可调度线程。但是,在现实中,线程被赋予不同的优先级。每个线程都会被赋予从0~30的优先级号码。只要优先级为31的线程是可调度的,那么其他线程绝分不到CPU。这种情况称为渴求调度。但是这并不意味着低于这个优先级的线程将永远得不到调度。比如,当优先级为31的线程调用GetMessage函数而消息队列中又没有消息时,那么系统就会暂停这个线程并调度其他可调度线程。另外,高优先级线程将抢占低优先级线程的运行,例如,如果一个优先级为6的线程正在运行,系统发现一个高优先级的线程准备要运行,那么系统就会立即暂停低优先级的线程的运行,并且将一个完整的时间片赋予高优先级线程。但是有一点需注意,系统可能会根据情况动态对线程的优先级进行改变,以保障低优先级的线程不处于饿死状态。这点稍后会提到。

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

       Windows支持6个优先级类:即空闲(屏保),低于正常,正常,高于正常,高(Task Manager)和实时(极端小心,该优先级甚至会抢占操作系统组件的运行)。当然,正常优先级是最常用的,99%的应用程序都使用这个优先级。只有当绝对必要的时候,才可以使用高优先级。你会惊奇的发现,Windows Explorer是在高优先级上运行的。大多数时间Explorer的线程是暂停的,等待用户按下操作键或者点击鼠标按钮时被唤醒。当Explorer处于暂停状态时,系统不将它的线程分配CPU。这将使低优先级的线程得以运行。但是如下用户按下Ctrl+Esc组合键,即使有低优先级线程在运行,系统也会让Explorer抢在那些线程前面运行。另外,如果没有足够的理由,千万不要使用实时优先级。它会干扰操作系统任务的运行。

        一旦选定了优先级类后,就不必考虑你的应用程序与其他应用程序之间的关系,只需要集中考虑你各个线程的优先级。windows支持7个相对的线程优先级,即空闲、最低、低于正常、正常、高于正常、最高和关键时间优先级。这些优先级是相对于进程优先级的。以行作为进程优先级以列作为相对线程优先级就可以做出一张优先级矩阵表,比如进程优先级高,线程优先级正常对应的优先级就是13。(这个表可以在核心编程page155中找到,优先级从1到31不等,有一些优先级是用户方式的应用程序无法使用的,比如17、18等,但是如果编写一个以内核方式运行的设备驱动程序,可以使用这些优先级),另外需要注意的是,实时优先级类的进程中的线程不能低于优先级16,而非实时优先级进程中的线程优先级不能高于15。

        一般来说,大多数时候高优先级的线程不应该处于可调度状态。相反,低优先级线程可以保持可调度状态。如果按照这个原则来办,这个操作系统就能正确的对用户响应。

        那么,进程是如果赋予不同的优先级的呢?当调用CreateProcess的时候,一共有6种标志符对应了6中进程的优先级。但是比较奇怪的是,创建子进程的进程负责子进程运行的优先级类。让我们用Explorer为例来说明这个问题,当使用Explorer来运行一个应用程序时,新进程按正常优先级运行。而Explorer不知道进程在做什么,也不知道隔多久进程的线程需要调度。但是一旦子进程运行,它就可以调用SetPriorityClass来改变它自己的优先级类。这个函数有一个参数用于设定进程的句柄,也就是说,只要拥有该进程的句柄和足够的访问权,就能够改变系统中运行的任何进程的优先级类,如果要改变自己的优先级类,只需传递GetCurrentProcess()即可。同样,也有GetPriorityClass函数可以获得进程的优先级类。

        当使用Explorer启动一个进程时,该程序的起始优先级是正常优先级类。但是如果使用Start命令来启动该程序,可以用一个设定开关来选择应用程序的优先级类。比如:C:/> START /LOW CALC.EXE,另外还有其他开关:/BELOWNORMAL、/NORMAL、/ABOVENORMAL、/HIGH、/REALTIME等。当然,一旦应该程序启动运行,它就可以调用SetPriorityClass设定自己的优先级。我们还可以使用Task Manager设定已启动进程的优先级,选中进程,右击鼠标即可看到。

        当一个线程刚刚创建的时候,它的相对优先级总是设定为正常优先级。CreateThread没有为我们提供一个参数来设定线程的优先级。但是我们可以调用SetThreadPriority来设定线程的优先级。若要创建一个带有相对优先级为空闲的线程,可以按照以下顺序调用:CreateThread(...CREATE_SUSPENDED)->SetPrioirtyClass->ResumeThread。

        有时候,系统常常要提高线程的优先级等级,以便对窗口消息或读取磁盘等I/O事件作出响应。例如,在高优先级类进程中的一个正常优先级等级的线程的基本优先级等级是13。如果用户按下一个操作键,系统就会将一个WM_KEYDOWN消息放入线程的队列中。由于一个消息已经出现在线程的队列中,因此该线程就是可调度线程。此外,键盘驱动程序也能告诉系统暂时提高线程的优先级等级。该线程的优先级等级可能提高两级,其当前的优先级等级为15。系统在优先级为15时为一个时间片对线程进程调度。一旦时间片结束系统,系统将线程的优先级递减1,使下一个时间的优先级等级为14。该线程的第三个时间片按优先级等级13来执行。如果线程要求执行更多的时间片,均按优先级等级13来执行。注意,线程的当前优先级等级绝不会低于线程的基本优先级等级。另外,系统只能为基本优先级等级在1至15的之间的线程提高其优先级等级,这个范围称为动态优先级范围。此外,系统不会将线程的优先级等级提高到实时范围(高于15),线程也不会动态提高实时范围内的线程优先级等级。系统提高了两个函数,可以让我们自己选择是否需要这种动态提高线程优先级等级的行为,它们是SetProcessPriorityBoost和SetThreadPriorityBoost,分别对进程中的所有线程和某个线程进行控制。当然,也有Get形式的版本。另一种情况也会导致系统动态地提高线程的优先级等级,当一个低优先级等级的线程准备运行,但是却有一个高优先级线程连续被调度时,系统会将该低优先级等级的线程动态提高到15,并让该线程运行两倍于它的时间片。当到了两倍时间片的时候,该线程的优先级等级返回到它的基本优先级。

        另外,系统还会自动提高前台程序的优先级等级,使他们能对用户的输入作出更快的反应。当前台进程返回后台运行时,该线程的优先级等级又恢复到基本优先级。

 亲缘性

         软亲缘性:按照默认设置,当系统将线程分配给处理器时,Windows 2000使用软亲缘性来进行操作。这意味着如果所有其他因素相同的话,它将设法在它上次运行的那个处理器上运行线程。让线程留在单个处理器上,有助于重复使用仍然在处理器的内存高速缓存中的数据。

        硬亲缘性:Windows 2000允许设置进程和线程的亲缘性。换句话说,可以控制哪个CPU能够运行某些线程。这称为硬亲缘性。

        通过调用GetSystemInfo,我们可以得到计算机拥有的CPU数量。为了限制在可用CPU的子集上运行的单个进程中的线程数量,可以调用SetProcessAffinityMask(HANDLE hProcess, DWROD_PRT dwProcessAffinityMask),第一个参数指明要影响的进程。第二个参数是个屏蔽位,用于指明线程可以在哪些CPU上运行。例如,0x00000005表示可以在CPU0和CPU2上运行。子进程可以继承父进程的亲缘性,此外,可以使用作业对象将一组进程限制在要求的一组CPU上运行。当然,也有对应的GetProcessAffinityMask函数。

        有时,我们可能想要将进程中的一个线程限制在一组CPU上运行。同样的,我们还有一个函数:GetThreadAffinityMask,参数跟上面那个相同,第一个用于指定线程句柄,第二个屏蔽位必须是进程的亲缘性屏蔽的子集。返回值是线程的前一个亲缘性屏蔽。若要为线程设置一个理想CPU,可以调用SetThreadIdleProcessor(HANDLE hThread, DWORD dwIdealProcessor)函数,dwIdealProcessor不是屏蔽位,它是个从0到31的整数,用于指明供线程使用的首选CPU。

        也可以在一个可执行文件的头上设置处理器亲缘性。设置代码在核心编程page170中可以找到。另外,可以使用称为ImageCfg.exe的实用程序,以便改变可执行程序模块头上的某些标志。这个不详细介绍了。

发布了25 篇原创文章 · 获赞 0 · 访问量 8万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章