7.6 运用结构环境
现在应该懂得环境结构在线程调度中所起的重要作用了。环境结构使得系统能够记住线程的状态,这样,当下次线程拥有可以运行的C P U时,它就能够找到它上次中断运行的地方。
知道这样低层的数据结构也会完整地记录在 Platform SDK文档中确实使人吃惊。不过如果查看该文档中的C O N T E X T结构,会看到下面这段文字:
“C O N T E X T结构包含了特定处理器的寄存器数据。系统使用 C O N T E X T结构执行各种内部操作。目前,已经存在为 I n t e l、M I P S、A l p h a和P o w e r P C处理器定义的C O N T E X T结构。若要了解这些结构的定义,参见头文件Wi n N T. h” 。
该文档并没有说明该结构的成员,也没有描述这些成员是谁,因为这些成员要取决于Windows 2000在哪个C P U上运行。实际上,在Wi n d o w s定义的所有数据结构中,C O N T E X T结构是特定于C P U的唯一数据结构。
那么C O N T E X T结构中究竟存在哪些东西呢?它包含了主机 C P U上的每个寄存器的数据结构。在x 8 6计算机上,数据成员是E a x、E b x、E c x、E d x等等。如果是A l p h a处理器,那么数据成员包括I n t V 0、I n t T 0、I n t T 1、I n t S 0、I n t R a和I n t Z e r o等等。下面这个代码段显示了 x86 CPU的完整的C O N T E X T结构:
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;
typedef CONTEXT *PCONTEXT;
C O N T E X T结构可以分成若干个部分。C O N T E X T _ C O N T R O L包含C P U的控制寄存器,比如指令指针、堆栈指针、标志和函数返回地址(与 x 8 6处理器不同,Alpya CPU在调用函数时,将该函数的返回地址放入一个寄存器中) 。C O N T E X T _ I N T E G E R用于标识C P U的整数寄存器。C O N T E X T _ F L O AT I N G _ P O I N T用于标识C P U的浮点寄存器。C O N T E X T _ S E G M E N T S用于标识C P U的段寄存器(仅为x 8 6处理器) 。CONTEXT_DEBUG_ REGISTER用于标识C P U的调试寄存器(仅为x 8 6处理器) 。CONTEXT_EXTENDED_ REGISTERS用于标识C P U的扩展寄存器(仅为x 8 6处理器) 。
Wi n d o w s实际上允许查看线程内核对象的内部情况,以便抓取它当前的一组 C P U寄存器。若要进行这项操作,只需要调用G e t T h r e a d C o n t e x t函数:
若要调用该函数,只需指定一个C O N T E X T结构,对某些标志(该结构的C o n t e x t F l a g s成员)进行初始化,指明想要收回哪些寄存器,并将该结构的地址传递给 G e t T h r e a d C o n t e x t。然后该函数将数据填入你要求的成员。
在调用G e t T h r e a d C o n t e x t函数之前,应该调用S u s p e n d T h r e a d,否则,线程可能被调度,而且线程的环境可能与你收回的不同。一个线程实际上有两个环境。一个是用户方式,一个是内核方式。G e t T h r e a d C o n t e x t只能返回线程的用户方式环境。如果调用S u s p e n d T h r e a d来停止线程的运行,但是该线程目前正在用内核方式运行,那么,即使 S u s p e n d T h r e a d实际上尚未暂停该线程的运行,它的用户方式仍然处于稳定状态。线程在恢复用户方式之前,它无法执行更多的用户方式代码,因此可以放心地将线程视为处于暂停状态, G e t T h r e a d C o n t e x t函数将能正常运行。
C O N T E X T结构的C o n t e x t F l a g s成员并不与任何C P U寄存器相对应。无论是何种 C P U结构,该成员存在于所有C O N T E X T结构定义中。C o n t e x t F l a g s成员用于向G e t T h r e a d C o n t e x t函数指明你想检索哪些寄存器。例如,如果想要获得线程的控制寄存器,可以编写下面的代码:
注意,在调用G e t T h r e a d C o n t e x t之前,首先必须对C O N T E X T结构中的C o n t e x t F l a g s成员进
行初始化。
Wi n d o w s为编程人员提供了多么强大的功能啊!如果你认为它确实不错,那么你一定会喜欢它的,因为Wi n d o w s使你能够修改C O N T E X T结构中的成员,然后通过调用S e t T h r e a d C o n t e x t将新寄存器值放回线程的内核对象中:
这有可能导致远程线程中的访问违规,向用户显示未处理的异常消息框,同时,远程进程终止运行。你将成功地终止另一个进程的运行,而你的进程则可以继续很好地运行。
G e t T h r e a d C o n t e x t和S e t T h r e a d C o n t e x t函数使你能够对线程进行许多方面的控制,但是在使用它们时应该小心。实际上,几乎没有应用程序调用这些函数。增加这些函数是为了增强调试程序和其他工具的功能。任何应用程序都可以调用它们。
7.7 线程的优先级
本章开头讲述了C P U是如何只使线程运行 2 0 m s,然后调度程序将另一个可调度的线程分配给C P U的。如果所有线程具有相同的优先级,那么就会发生这种情况,但是,在现实环境中,线程被赋予许多不同的优先级,这会影响到调度程序将哪个线程取出来作为下一个要运行的线程。
每个线程都会被赋予一个从 0(最低)到3 1(最高)的优先级号码。当系统确定将哪个线程分配给C P U时,它首先观察优先级为3 1的线程,并以循环方式对它们进行调度。如果优先级为3 1的线程可以调度,那么就将该线程赋予一个C P U。在该线程的时间片结束时,系统要查看是否还有另一个优先级为3 1的线程可以运行,如果有,它将允许该线程被赋予一个C P U。
只要优先级为3 1的线程是可调度的,系统就绝对不会将优先级为 0到3 0的线程分配给C P U。这种情况称为渴求调度(s t a r v a t i o n) 。当高优先级线程使用大量的 C P U时间,从而使得低优先级线程无法运行时,便会出现渴求情况。在多处理器计算机上出现渴求情况的可能性要少得多,因为在这样的计算机上,优先级为3 1和优先级为3 0的线程能够同时运行。系统总是设法使C P U
保持繁忙状态,只有当没有线程可以调度的时候,C P U才处于空闲状态。
人们可能认为,在这样的系统中,低优先级线程永远得不到机会运行。不过正像前面指出的那样,在任何一个时段内,系统中的大多数线程是不能调度的。例如,如果进程的主线程调用G e t M e s s a g e函数,而系统发现没有线程可以供它使用,那么系统就暂停进程的线程运行,释放该线程的剩余时间片,并且立即将C P U分配给另一个等待运行的线程。
如果没有为G e t M e s s a g e函数显示可供检索的消息,那么进程的线程将保持暂停状态,并且决不会被分配给C P U。但是,当消息被置于线程的队列中时,系统就知道该线程不应该再处于暂停状态。此时,如果没有更高优先级的线程需要运行,系统就将该线程分配给一个 C P U。
现在考虑另一个问题。高优先级线程将抢在低优先级线程之前运行,不管低优先级线程正在运行什么。例如,如果一个优先级为 5的线程正在运行,系统发现一个高优先级的线程准备要运行,那么系统就会立即暂停低优先级线程的运行(即使它处于它的时间片中) ,并且将C P U分配给高优先级线程,使它获得一个完整的时间片。
还有,当系统引导时,它会创建一个特殊的线程,称为 0页线程。该线程被赋予优先级 0,它是整个系统中唯一的一个在优先级0上运行的线程。当系统中没有任何线程需要执行操作时,0页线程负责将系统中的所有空闲R A M页面置0。
7.8 对优先级的抽象说明
当M i c r o s o f t的开发人员设计线程调度程序时,他们发现该调度程序无法在所有时间适应所有人的需要。他们还发现,计算机的“作用”是不断变化的。当 Windows NT问世时,对象链接和嵌入(O L E)应用程序还刚刚开始编写。现在,O L E应用程序已经司空见惯。游戏软件已经相当流行。当然,在Windows NT的早期,并没有更多地考虑I n t e r n e t的问题。
调度算法对用户运行的应用程序类型有着相当大的影响。从一开始, M i c r o s o f t的开发人员就认识到,随着系统的用途的变化,他们必须不断修改调度算法。但是,软件开发人员需要在今天编写软件,而M i c r o s o f t则要保证软件能够在将来的系统版本上运行。那么 M i c r o s o f t如何改变系统工作的方式并仍然保证软件能够运行呢?下面是解决这个问题的一些办法:
• Microsoft没有将调度程序的行为特性完全固定下来。
• Microsoft没有让应用程序充分利用调度程序的特性。
• Microsoft声称调度程序的算法是变化的,在编写代码时应有所准备。
Windows API展示了系统的调度程序上的一个抽象层,这样就永远不会直接与调度程序进行通信。相反,要调用Wi n d o w s函数,以便根据运行的系统版本“转换”参数。本章将介绍这个抽象层。
当设计一个应用程序时, 你应该考虑到还有什么别的应用程序会与你的应用程序一道运行。然后,应该根据你的应用程序中的线程应该具备何种响应性,选择一个优先级类。这听起来有些费解,不过情况确实如此。M i c r o s o f t不想作出任何将来可能影响你的代码运行的承诺。
Wi n d o w s支持6个优先级类:即空闲、低于正常、正常、高于正常、高和实时。当然,正常优先级是最常用的优先级类, 9 9 %的应用程序均使用这个优先级类。表 7 - 4描述了这些优先级类。
当系统什么也不做的时候,将空闲优先级类用于应用程序的运行是最恰当不过的。没有用交互方式使用的计算机有可能仍然很繁忙(比如作为文件服务器) ,不应该与屏幕保护程序争用C P U时间。定期更新系统的某些状态的统计信息跟踪应用程序不应该干扰关键任务的运行。
只有当绝对必要的时候,才可以使用高优先级类。你会惊奇地发现, Windows Explorer是在高优先级上运行的。大多数时间 E x p l o r e r的线程是暂停的,等待用户按下操作键或者点击鼠标按钮时被唤醒。当E x p l o r e r的线程处于暂停状态时,系统不将它的线程分配给 C P U。因为这将使低优先级线程得以运行。但是一旦用户按下一个操作键或组合键,如 C t r l + E s c,系统就会唤醒E x p l o r e r的线程(当用户按下C t r l + E s c组合键时,也会出现S t a r t菜单) 。如果低优先级线程正在运行,系统会立即抢在这些线程的前面,让E x p l o r e r的线程优先运行。
M i c r o s o f t就是按这种方法设计E x p l o r e r的,因为用户希望无论系统中正在运行什么,外壳程序都具有极强的响应能力。实际上,即使低优先级线程在无限循环中暂停运行,也能显示E x p l o r e r的窗口。由于E x p l o r e r的线程拥有较高的优先级,因此执行无限循环的线程被抢占,E x p l o r e r让用户终止挂起进程的运行。E x p l o r e r的运行特性非常出色,大部分时间它的线程无事可做,不必占用C P U时间。如果情况不是如此,那么整个系统的运行速度就会慢得多,许多应用程序就不会作出响应。
应该尽可能避免使用实时优先级类。实际上Windows NT 3.1的早期测试版并没有向应用程序展示这个优先级类,尽管该操作系统支持这个类。实时优先级是很高的优先级,它可能干扰操作系统任务的运行,因为大多数操作系统线程均以较低的优先级来运行。因此实时线程可能阻止必要的磁盘I / O信息和网络信息的产生。此外,键盘和鼠标输入将无法及时得到处理,用户可能以为系统已经暂停运行。大体来说,必须有足够的理由才能使用实时优先级,比如需要
以很短的等待时间来响应硬件事件,或者执行某些不能中断的短期任务。
注意 除非用户拥有“提高调度优先级”的权限,否则进程不能用实时优先级类来运行。凡是被指定为管理员或特权用户的用户,均默认拥有该权限。
当然,大多数进程都属于正常优先级类。低于正常和高于正常的优先级类是 Windows 2000中的新增优先级。M i c r o s o f t增加这些优先级类的原因是,有若干家公司抱怨现有的优先级类无法提供足够的灵活性。
一旦选定了优先级类之后,就不必考虑你的应用程序与其他应用程序之间的关系,只需要集中考虑你的应用程序中的各个线程。 Wi n d o w s支持7个相对的线程优先级:即空闲、最低、低于正常、正常、高于正常、最高和关键时间优先级。这些优先级是相对于进程的优先级类而言的。大多数线程都使用正常线程优先级。表7 - 5描述了这些相对的线程优先级。
概括起来说,进程是优先级类的一个组成部分,你为进程中的线程赋予相对线程优先级。这里没有讲到0到3 1的优先级的任何情况。应用程序开发人员从来不必具体设置优先级。相反,系统负责将进程的优先级类和线程的相对优先级映射到一个优先级上。正是这种映射方式,M i c r o s o f t不想拘泥不变。实际上这种映射方式是随着系统的版本的升级而变化的。
表7 - 6显示了这种映射方式是如何用于Windows 2000的,注意,Windows NT的早期版本和某些Windows 95和Windows 98版本采用了不同的映射方式。未来的Wi n d o w s版本中的映射方式也会变化。
例如,正常进程中的正常线程被赋予的优先级是 8。由于大多数进程属于正常优先级类,而大多数线程属于正常线程优先级,因此系统中的大多数线程的优先级是 8。
如果高优先级进程中有一个正常线程,该线程的优先级将是 1 3。如果将进程的优先级类改为8,那么线程的优先级就变为4。如果改变了进程的优先级类,线程的相对优先级不变,但是它的优先级的等级却发生了变化。
注意,表7 - 6并没有显示优先级的等级为0的线程。这是因为0优先级保留供零页线程使用,系统不允许任何其他线程拥有 0优先级。另外,下列优先级等级是无法使用的: 1 7、1 8、1 9、2 0、2 1、2 7、2 8、2 9和3 0。如果编写一个以内核方式运行的设备驱动程序,可以获得这些优先级等级,而用户方式的应用程序则不能。另外还要注意,实时优先级类中的线程不能低于优先级等级1 6。同样,非实时优先级类中的线程的等级不能高于1 5。
注意 有些人常常搞不清进程优先级类的概念。他们认为这可能意味着进程是可以调度的。但是进程是根本不能调度的,只有线程才能被调度。进程优先级类是个抽象概念,M i c r o s o f t提出这个概念的目的,是为了帮助你将它与调度程序的内部运行情况区分开来。它没有其他目的。
注意 一般来说,大多数时候高优先级的线程不应该处于可调度状态。当线程要进行某种操作时,它能迅速获得C P U时间。这时线程应该尽可能少地执行 C P U指令,并返回睡眠状态,等待再次变成可调度状态。相反,低优先级的线程可以保持可调度状态,执行大量的C P U指令来进行它的操作。如果按照这些原则来办,整个操作系统就能正确地对用户作出响应。
7.9 程序的优先级
进程是如何被赋予优先级类的呢?当调用C r e a t e P r o c e s s时,可以在f d w C r e a t e参数中传递需要的优先级类。表7 - 7显示了优先级类的标识符。
创建子进程的进程负责选择子进程运行的优先级类,这看起来有点奇怪。让我们以E x p l o r e r为例来说明这个问题。当使用E x p l o r e r来运行一个应用程序时,新进程按正常优先级运行。E x p l o r e r不知道进程在做什么,也不知道隔多长时间它的线程需要进行调度。但是,一旦子进程运行,它就能够通过调用S e t P r i o r i t y C l a s s来改变它自己的优先级类:
该函数将h P r o c e s s标识的优先级类改为f d w P r i o r i t y参数中设定的值。f d w P r i o r i t y参数可以是表7 - 7显示的标识符之一。由于该函数带有一个进程句柄,因此,只要拥有该进程的句柄和足够的访问权,就能够改变系统中运行的任何进程的优先级类。
一般来说,进程将试图改变它自己的优先级类。下面是如何使一个进程将它自己的优先级类设置为空闲的例子:
SetPriorityClass(GetCurrentProcess() ,HIGH_PRIORITY_CLASS);
下面是用来检索进程的优先级类的补充函数:
DWORD GetPriorityClass(HANDLE hProcess);
正如你所期望的那样,该函数将返回表7 - 7中列出的标识符之一。
当使用命令外壳启动一个程序时,该程序的起始优先级是正常优先级。但是,如果使用S t a r t命令来启动该程序,可以使用一个开关来设定应用程序的起始优先级。例如,在命令外壳输入下面的命令可使系统启动C a l c u l a t o r,并在开始时按空闲优先级来运行它:
S t a r t命令还能识别 / B E L O W N O R M A L、/ N O R M A L、/ A B O V E N O R M A L、/ H I G H和/ R E A LT I M E等开关,以便按它们各自的优先级启动执行一个应用程序。当然,一旦应用程序启动运行,它就可以调用S e t P r i o r i t y C l a s s函数,将它自己的优先级改为它选择的任何优先级。
Windows 98 Windows 98的S t a r t命令并不支持这些开关中的任何一个。Windows 98命令外壳启动的进程总是使用正常优先级类来运行。
Windows 2000的Task Manager使得用户可以改变进程的优先级类。图 7 - 2显示了Ta s kM a n a g e r的P r o c e s s e s选项卡,它显示了当前运行的所有进程。Base Pri列显示了每个进程的优先级类。可以改变进程的优先级类,方法是选定一个进程,然后从上下文菜单的 Set Priority(设置优先级)子菜单中选择一个选项。
当一个线程刚刚创建时,它的相对线程优先级总是设置为正常优先级。我总感到有些奇怪,C r e a t e T h r e a d没有为调用者提供一个设置新线程的相对优先级的方法。若要设置和获得线程的相对优先级,必须调用下面的这些函数:
当然,h T h r e a d参数用于标识想要改变优先级的单个线程, n P r i o r i t y参数是表7 - 8列出的7个标识符之一。
下面是检索线程的相对优先级的补充函数:
int GetThreadPriority(HANDLE hThread);
该函数返回表7 - 8列出的标识符之一。
若要创建一个带有相对优先级为空闲的线程,可以执行类似下面的代码:
注意,C r e a t e T h r e a d函数创建的新函数带有的相对优先级总是正常优先级。若要使线程以空闲优先级来运行,应该将C R E AT E _ S U S P E N D E D标志传递给C r e a t e T h r e a d函数,这可以防止线程执行任何代码。然后可以调用 S e t T h r e a d P r i o r i t y,将线程的优先级改为相对空闲优先级。这时可以调用R e s u m e T h r e a d,使得线程成为可调度的线程。你不知道线程何时能够获得 C P U时间,但是调度程序会考虑这样一个情况,即该线程拥有一个空闲优先级。最后,可以关闭新线程的句柄,一旦线程终止运行,内核对象就能被撤消。
注意 Wi n d o w s没有提供返回线程的优先级的函数。这是故意进行的。记住,M i c r o s o f t保留了随时修改调度算法的权利。你不会设计需要调度算法专门知识的应用程序。如果坚持使用进程优先级类和相对线程优先级,你的应用程序不仅现在能够顺利地运行,而且在系统的将来版本上也能很好地运行。
7.9.1 动态提高线程的优先级等级
通过将线程的相对优先级与线程的进程优先级类综合起来考虑,系统就可以确定线程的优级等级。有时这称为线程的 基本优先级等级。系统常常要提高线程的优先级等级,以便对窗口消息或读取磁盘等I / O事件作出响应。
例如,在高优先级类进程中的一个正常优先级等级的线程的基本优先级等级是 1 3。如果用户按下一个操作键,系统就会将一个 W M _ K E Y D O W N消息放入线程的队列中。由于一个消息已经出现在线程的队列中,因此该线程就是可调度的线程。此外,键盘设备驱动程序也能够告诉系统暂时提高线程的优先级等级。该线程的优先级等级可能提高 2级,其当前优先级等级改为1 5。
系统在优先级为1 5时为一个时间片对该线程进行调度。一旦该时间片结束,系统便将线程的优先级递减1,使下一个时间片的线程优先级降为 1 4。该线程的第三个时间片按优先级等级1 3来执行。如果线程要求执行更多的时间片,均按它的基本优先级等级 1 3来执行。
注意,线程的当前优先级等级决不会低于线程的基本优先级等级。此外,导致线程成为可调度线程的设备驱动程序可以决定优先级等级提高的数量。 M i c r o s o f t并没有规定各个设备驱动程序可以给线程的优先级提高多少个等级。这样就使得 M i c r o s o f t可以不断地调整线程优先级提高的动态等级,以确定最佳的总体响应性能。
系统只能为基本优先级等级在 1至1 5之间的线程提高其优先级等级。实际上这是因为这个范围称为动态优先级范围。此外,系统决不会将线程的优先级等级提高到实时范围(高于 1 5) 。由于实时范围中的线程能够执行大多数操作系统的函数,因此给等级的提高规定一个范围,就可以防止应用程序干扰操作系统的运行。另外,系统决不会动态提高实时范围内的线程优先级等级。
有些编程人员抱怨说,系统动态提高线程优先级等级的功能对他们的线程性能会产生一种不良的影响,为此M i c r o s o f t增加了下面两个函数,这样就能够使系统的动态提高线程优先级等级的功能不起作用:
S e t P r o c e s s P r i o r i t y B o o s t负责告诉系统激活或停用进行中的所有线程的优先级提高功能,而S e t T h r e a d P r i o r i t y B o o s t则让你激活或停用各个线程的优先级提高功能。这两个函数具有许多相似的共性,可以用来确定是激活还是停用优先级提高功能:
对于这两个函数中的每个函数,可以传递想要查询的进程或线程的句柄,以及由函数设置的B O O L的地址。
Windows 98 Windows 98没有提供这4个函数的有用的实现代码。它们全部返回FA L S E,后来对G e t L a s t E r r o r的调用将返回E R R O R _ C A L L _ N O T _ I M P L E M E N T E D。
另一种情况也会导致系统动态地提高线程的优先级等级。比如有一个优先级为 4的线程准备运行但是却不能运行,因为一个优先级为8的线程正连续被调度。在这种情况下,优先级为4的线程就非常渴望得到C P U时间。当系统发现一个线程在大约3至4 s内一直渴望得到C P U时间,它就将这个渴望得到C P U时间的线程的优先级动态提高到1 5,并让该线程运行两倍于它的时间量。当到了两倍时间量的时候,该线程的优先级立即返回到它的基本优先级。
7.9.2 为前台进程调整调度程序
当用户对进程的窗口进行操作时,该进程就称为前台进程,所有其他进程则称为后台进程。当然,用户希望他正在使用的进程比后台进程具有更强的响应性。为了提高前台进程的响应性,Wi n d o w s能够为前台进程中的线程调整其调度算法。对于Windows 2000来说,系统可以为前台进程的线程提供比通常多的C P U时间量。这种调整只能在前台进程属于正常优先级类的进程时才能进行。如果它属于其他任何优先级类,就无法进行任何调整。
Windows 2000实际上允许用户对这种调整进行相应的配置。在 System Properties(系统属性)对话框的A d v a n c e d选项卡上,用户可以单击Performance Options(性能选项)按钮,打开图7 - 3所示的对话框
如果用户选择优化应用程序的性能,系统就执行配置的调整。如果用户选择优化后台服务程序的性能,系统就不进行调整。当安装 Windows 2000的专业版时,A p p l i c a t i o n s就会被默认选定。对于Windows 2000的所有其他版本,则默认选定Background Services,因为计算机将主要由非交互式用户使用。
当进程移到前台时,Windows 98也会对正常优先级类的进程中的线程调度算法进行调整。当一个优先级为正常的进程移到前台时,系统便将最低、低于正常、正常、高于正常和最高等优先级的线程的优先级提高 1,优先级为空闲和关键时间的线程的优先级则不予提高。因此,在正常优先级类的进程中运行的、其相对优先级为正常的线程,它的优先级等级是 9而不是8。当进程返回后台时,进程中的线程便自动返回它们定义好的基本优先级等级。
Windows 98 Windows 98没有提供允许用户配置这种调整手段的任何用户界面,因为Windows 98不是作为专用服务器来运行的。
将进程改为前台进程的原因是,使它们能够对用户的输入更快地作出响应。如果不改为前台进程,那么在后台的正常打印进程与在后台接收用户输入的正常进程就会平等地争用 C P U时间。用户会发现文本无法在前台应用程序中顺利地显示。但是,由于系统改变了前台进程的线程优先级,前台进程的线程就能对用户的输入更好地作出响应。
7.10 亲缘性
按照默认设置,当系统将线程分配给处理器时, Windows 2000使用软亲缘性来进行操作。这意味着如果所有其他因素相同的话,它将设法在它上次运行的那个处理器上运行线程。让线程留在单个处理器上,有助于重复使用仍然在处理器的内存高速缓存中的数据。
有一种新的计算机结构,称为 N U M A(非统一内存访问) ,在该结构中,计算机包含若干块插件板,每个插件板上有 4个C P U和它自己的内存区。图7 - 6显示了一台配有3块插件板的计算机,总共有1 2个C P U,这样,任何一个线程都可以在1 2个C P U中的任何一个上运行。
当C P U访问的内存是它自己的插件板上的内存时, N U M A系统运行的性能最好。如果C P U需要访问位于另一个插件板上的内存时,性能就会大大降低。在这样的环境中,就需要来自一个进程中的线程在CPU 0至3上运行,让另一个进程中的线程在 CPU 4至7上运行,依次类推。为了适应这种计算机结构的需要,Windows 2000允许设置进程和线程的亲缘性。换句话说,可以控制哪个C P U能够运行某些线程。这称为硬亲缘性。
计算机在引导时,系统要确定机器中有多少个C P U可供使用。通过调用G e t S y s t e m I n f o函数(第1 4章介绍) ,应用程序就能查询机器中的C P U数量。按照默认设置,任何线程都可以调度到这些C P U中的任何一个上去运行。 为了限制在可用C P U的子集上运行的单个进程中的线程数量,可以调用S e t P r o c e s s A ff i n i t y M a s k:
第一个参数h P r o c e s s用于指明要影响的是哪个进程。第二个参数 d w P r o c e s s A ff i n i t y M a s k是个位屏蔽,用于指明线程可以在哪些C P U上运行。例如,传递0 x 0 0 0 0 0 0 0 5表示该进程中的线程可以在CPU 0和CPU 2上运行,但是不能在CPU 1和C P U 3至3 1上运行。
注意,子进程可以继承进程的亲缘性。因此,如果一个进程的亲缘性屏蔽是 0 x 0 0 0 0 0 0 0 5,那么它的子进程中的任何线程都拥有相同的位屏蔽,并共享相同的 C P U。此外,可以使用作业内核对象将一组进程限制在要求的一组C P U上运行。
当然,还有一个函数也能够返回进程的亲缘性位屏蔽,它就是 G e t P r o c e s s A ff i n i t y M a s k,如面的代码所示:
这里也可以传递想要亲缘性屏蔽的进程句柄,该函数填入 p d w P r o c e s s A ff i n i t y M a s k指向的变量。该函数还能返回系统的亲缘性屏蔽(在 p d w S y s t e m A ff i n i t y M a s k指向的变量中) 。系统的亲缘性屏蔽用于指明系统的哪个C P U能够处理线程。进程的亲缘性屏蔽始终是一个系统的亲缘性屏蔽的正确子集。
Windows 98 无论计算机中实际拥有多少个C P U,Windows 98只使用一个C P U。因此,G e t P r o c e s s A ff i n i t y M a s k总是用1填入两个变量中。
到现在为止,已经介绍了如何将进程的多个线程限制到一组 C P U上去运行。有时可能想要将进程中的一个线程限制到一组 C P U上去运行。例如,可能有一个包含 4个线程的进程,它们在拥有4个C P U的计算机上运行。如果这些线程中的一个线程正在执行非常重要的操作,而你想增加某个C P U始终可供它使用的可能性,为此你对其他 3个线程进行了限制,使它们不能在CPU 0上运行,而只能在CPU 1、2和3上运行。
通过调用S e t T h r e a d A ff i n i t y M a s k,就能为各个线程设置亲缘性屏蔽:
该函数中的h T h r e a d参数用于指明要限制哪个线程,d w T h r e a d A ff i n i t y M a s k用于指明该线程能够在哪个C P U上运行。d w T h r e a d A ff i n i t y M a s k必须是进程的亲缘性屏蔽的相应子集。返回值是线程的前一个亲缘性屏蔽。因此,若要将 3个线程限制到CPU 1、2和3上去运行,可以这样操作:
Windows 98 由于计算机中无论配有多少个C P U,Windows 98只使用一个C P U,因此d w T h r e a d A ff i n i t y M a s k参数必须始终是1。
当一个x 8 6系统引导时,系统要执行相应的代码,以便测定主机上的哪些 C P U遇到了著名的P e n t i u m浮点错误。系统必须为每个 C P U测试其浮点错误,方法是将线程的亲缘性设置为第一个C P U,执行潜在的故障分割操作,并将结果与已知的正确答案进行比较。然后对下一个C P U进行上述同样的操作,如此等等。
注意 在大多数环境中,改变线程的亲缘性就会影响调度程序有效地在各个 C P U之间移植线程的能力,而这种能力可以最有效地使用C P U时间。表7 - 9显示了一个例子。
当线程A被唤醒时,调度程序发现该线程可以在 CPU 0上运行,因此它被分配给CPU 0。然后线程B被唤醒,调度程序发现该线程可以被分配给 CPU 0或1,但是,由于CPU 0正在使用之中,因此调度程序将线程B分配给了CPU 1。至此,一切进行得都很顺利。
这时线程C被唤醒,调度程序发现它只能在CPU 1上运行。但是CPU 1正在被线程B使用着,它是个优先级为8的线程。由于线程C的优先级为6,因此它不能抢在线程B的前面运行。线程C可以抢在线程A的前面运行,因为线程A的优先级是4,但是调度程序不会使它抢在线程A的前面运行,因为线程C不能在CPU 0上运行。
这显示出为线程设置硬亲缘性将会对调度程序的优先级设置方案产生什么样的影响。
有时强制将一个线程分配给特定的 C P U的做法是不妥当的。例如,有 3个线程都只能在CPU 0上运行,而CPU 1、2和3则闲着无事可做。在这种情况下,如果告诉系统想让一个线程在某个C P U上运行,但是允许该线程在可能的情况下移到另一个 C P U上去运行,那么这种办法会更好些。
若要为线程设置一个理想的C P U,可以调用S e t T h r e a d I d e a l P r o c e s s o r :
h T h r e a d用于指明要为哪个线程设置首选的 C P U。与我们已经介绍的其他函数不同,d w I d e a l P r o c e s s o r函数不是个位屏蔽函数,它是个从0到3 1的整数,用于指明供线程使用的首选C P U。可以传递一个M A X I M U M _ P R O C E S S O R S的值(在Wi n N T. h中定义为3 2) ,用于指明不存在理想的C P U。如果没有为该线程设置理想的 C P U,那么该函数返回前一个理想的 C P U或M A X I M U M _ P R O C E S S O R S。