本章介绍系统如何管理所有正在运行的应用程序。首先讲述什么是进程,以及系统如何创建进程内核对象,以便管理每个进程。然后将说明如何使用相关的内核对象来对进程进行操作。接着,要介绍进程的各种不同的属性,以及查询和修改这些属性所用的若干个函数。还要讲述创建或生成系统中的辅助进程所用的函数。当然,如果不深入说明如何来结束进程的运行,那么这样的介绍肯定是不完整的。现在就来介绍进程的有关内容。
进程通常被定义为一个正在运行的程序的实例,它由两个部分组成:
• 一个是操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程的统计信息的地方。
• 另一个是地址空间,它包含所有可执行模块或 D L L模块的代码和数据。它还包含动态内存分配的空间。如线程堆栈和堆分配空间。
进程是不活泼的。若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,该线程负责执行包含在进程的地址空间中的代码。实际上,单个进程可能包含若干个线程,所有这些线程都“同时”执行进程地址空间中的代码。为此,每个线程都有它自己的一组C P U寄存器和它自己的堆栈。每个进程至少拥有一个线程,来执行进程的地址空间中的代码。如果没有线程来执行进程的地址空间中的代码,那么进程就没有存在的理由了,系统就将自动撤消该进程和它的地址空间。
若要使所有这些线程都能运行,操作系统就要为每个线程安排一定的 C P U时间。它通过以一种循环方式为线程提供时间片(称为量程),造成一种假象,仿佛所有线程都是同时运行的一样。图 4 - 1显示了在单个C PU的计算机上是如何实现这种运行方式的。如果计算机拥有多个 C P U,那么操作系统就要使用复杂得多的算法来实现 C P U上线程负载的平衡。
当创建一个进程时,系统会自动创建它的第一个线程,称为主线程。然后,该线程可以创建其他的线程,而这些线程又能创建更多的线程。
Wi n d o w s 2 0 0 0 Micorsoft Windows 2000能够在拥有多个C P U的计算机上运行。 例如,我用来撰写本书的计算机就包含两个处理器。 Windows 2000可以在每个C P U上运行不同的线程,这样,多个线程就真的在同时运行了。 Windows 2000的内核能够在这种类型的系统上进行所有线程的管理和调度。不必在代码中进行任何特定的设置就能利用多处理器提供的各种优点。
Windows 98只能在单处理器计算机上运行。即使计算机配有多个处理器, Wi n d o w s每次只能安排一个线程运行,而其他的处理器则处于空闲状态。
4.1 编写第一个Wi n d o w s应用程序
Wi n d o w s支持两种类型的应用程序。一种是基于图形用户界面(G U I)的应用程序,另一种是基于控制台用户界面(C U I)的应用程序。基于G U I的应用程序有一个图形前端程序。它能创建窗口,拥有菜单,可以通过对话框与用户打交道,并可使用所有的标准“Wi n d o w s”组件。Wi n d o w s配备的所有应用程序附件(如N o t e p a d、 C a l c u l a t o r和Wo r d P a d),几乎都是基于G U I的应用程序。基于控制台的应用程序属于文本操作的应用程序。它们通常不能用于创建窗口或处理消息,并且它们不需要图形用户界面。虽然基于 C U I的应用程序包含在屏幕上的窗口中,但是窗口只包含文本。命令外壳程序 C M D . E X E(用于Windows 2000)和COMMAND.COM (用于Windows 98)都是典型的基于C U I的应用程序。
这两种类型的应用程序之间的界限是非常模糊的。可以创建用于显示对话框的 C U I应用程序。例如,命令外壳程序可能拥有一个特殊的命令,使它能够显示一个图形对话框,在这个对话框中,可以选定你要执行的命令,而不必记住该外壳程序支持的各个不同的命令。也可以创建一个基于G U I的应用程序,它能将文本字符串输出到一个控制台窗口。我常常创建用于建立控制台窗口的G U I应用程序,在这个窗口中,我可以查看应用程序执行时的调试信息。当然你也可以在应用程序中使用图形用户界面,而不是老式的字符界面,因为字符界面使用起来不太方便。
当使用Microsoft Visual C++来创建应用程序时,这种集成式环境安装了许多不同的链接程序开关,这样,链接程序就可以将相应的子系统嵌入产生的可执行程序。用于 C U I应用程序的链接程序开关是 / S U B S Y S T E M : C O N D O L E,而用于 G U I 应用程序的链接程序开关是S U B S Y S T E M : W I N D O W S。当用户运行一个应用程序时,操作系统的加载程序就会查看可执行图形程序的标题,并抓取该子系统的值。如果该值指明一个 C U I应用程序,那么加载程序就会自动保证为该应用程序创建文本控制台窗口。
如果该值指明这是个 G U I应用程序,那么加载程序不创建控制台窗口,而只是加载应用程序。一旦应用程序启动运行,操作系统就不再考虑应用程序拥有什么类型的用户界面。Wi n d o w s应用程序必须拥有一个在应用程序启动运行时调用的进入点函数。可以使用的进入点函数有4个:
int WINAPI WinMain(
HINSTANCE hinstExe,
HINSTANCE,
PSTR pszCmdLine,
int nCmdShow
);
int WINAPI wWinMain(
HINSTANCE hinstExe,
HINSTANCE,
PWSTR pszCmdLine,
int nCmdShow
);
int __cdecl main(
int argc,
char *argv[],
char *envp[]
);
int __cdecl wmain(
int argc,
wchar_t *argv[],
wchar_t *envp[]
);
操作系统实际上并不调用你编写的进入点函数。它调用的是 C / C + +运行期启动函数。该函数负责对C / C + +运行期库进行初始化,这样,就可以调用 m a l l o c和f r e e之类的函数。它还能够确保已经声明的任何全局对象和静态 C + +对象能够在代码执行以前正确地创建。下面说明源代码中可以实现哪个进入点以及何时使用该进入点 (见表4 - 1 )。
表4-1 应用程序的进入点
应用程序类型 | 进 入 点 | 嵌入可执行文件的启动函数 |
需要A N S I字符和字符串的G U I应用程序 | Wi n M a i n | Wi n M a i n C RT S t a r t u p |
需要U n i c o d e字符和字符串的G U I应用程序 | w Wi n M a i n | w Wi n M a i n C RT S t a r t u p |
需要A N S I字符和字符串的C U I应用程序 | m a i n | m a i n C RT S t a r t u p |
需要U n i c o d e字符和字符串的C U I应用程序 | w m a i n | w m a i n C RT S t a r t u p |
链接程序负责在它连接可执行文件时选择相应的 C / C + +运行期启动函数。如果设定了/ S U B S Y S T E M : W I N D O W S链接程序开关,那么该链接程序期望找到一个 Wi n M a i n或w Wi n m a i n函数。如果这两个函数都不存在,链接程序便返回一个“未转换的外部符号”的错误消息。否则,它可以分别选择Wi n M a i n C RT S t a r t u p函数或w Wi n M a i n C RT S t a r t u p函数。
同样,如果设定了 / S U B S Y S T E M : C O N S O L E链接程序开关,那么该链接程序便期望找到m a i n或w m a i n函数,并且可以分别选择 m a i n C RT S t a r t u p函数或w m a i n C RT S t a r t u p函数。同样,如果m a i n或w m a i n都不存在,那么链接程序返回一条“未转换外部符号”的消息。
但是,人们很少知道这样一个情况,即可以从应用程序中全部删除 / S U B S Y S T E M链接程序开关。当这样做的候,链接程序能够自动确定应用程序应该连接到哪个子系统。当进行链接时,链接程序要查看代码中存在 4个函数(Wi n M a i n、 w Wi n M a i n、 m a i n或w m a i n)中的哪一个。然后确定可执行程序应该是哪一个子系统,并且确定可执行程序中应该嵌入哪个 C / C + +启动函数。
Wi n d o w s / Visual C++编程新手常犯的错误之一是,当创建新的应用程序时,不小心选择了错误的应用程序类型。例如,编程员可能创建一个新的 Wi n 3 2应用程序项目,但是创建了一个进入点函数m a i n。(错误)当创建应用程序时,编程员会看到一个链接程序错误消息,因为 w i n 3 2应用程序项目设置了/ S U B S Y S T E M : W I N D O W S链接程序开关,但是不存在Wi n M a i n或w Wi n M a i n函数。这时,编程员可以有4个选择:
• 将m a i n函数改为Wi n M a i n。通常这不是最佳的选择,因为编程员可能想要创建一个控制台应用程序。
• 用Visual C++创建一个新的Win32 控制台应用程序,并将现有的源代码添加给新应用程序项目。这个选项冗长而乏味,因为它好像是从头开始创建应用程序,而且必须删除原始的应用程序文件。
• 单击 Project Settings 对话框的 L i n k选项卡,将 / S U B S Y S T E M : W I N D O W S开关改为/ S U B S Y S T E M : C O N S O L E。这是解决问题的一种比较容易的方法,很少有人知道他们只需要进行这项操作就行了。
• 单击Project Settings对话框的L i n k选项卡,然后全部删除/ S U B S Y S T E M : W I N D O W S开关。
这是我喜欢选择的方法,因为它提供了最大的灵活性。现在,连接程序将根据源代码中实现的函数进行正确的操作。当用Visual C++ Developer Studio创建新Wi n 3 2应用程序或Wi n 3 2控制台应用程序项目时,我不知道为什么这没有成为默认设置。所有的C / C + +运行期启动函数的作用基本上都是相同的。它们的差别在于,它们究竟是处理A N S I字符串还是U n i c o d e字符串,以及它们在对 C运行期库进行初始化后它们调用哪个进入点函数。 Visual C++配有C运行期库的源代码。可以在CR t0.c文件中找到这4个启动函数的代码。
现在将启动函数的功能归纳如下:
• 检索指向新进程的完整命令行的指针。
• 检索指向新进程的环境变量的指针。
• 对C / C + +运行期的全局变量进行初始化。如果包含了 S t d L i b . h文件,代码就能访问这些变量。表4 - 1列出了这些变量。
• 对C运行期内存单元分配函数(m a l l o c和c a l l o c)和其他低层输入/输出例程使用的内存栈进行初始化。
• 为所有全局和静态 C + +类对象调用构造函数。
当所有这些初始化操作完成后, C / C + +启动函数就调用应用程序的进入点函数。如果编写了一个w Wi n M a i n函数,它将以下面的形式被调用:
当进入点函数返回时,启动函数便调用 C运行期的 e x i t函数,将返回值(n M a i n R e t Va l)传递给它。 E x i t函数负责下面的操作:
• 调用由_ o n e x i t函数的调用而注册的任何函数。
• 为所有全局的和静态的C + +类对象调用析构函数。
• 调用操作系统的E x i t P r o c e s s函数,将n M a i n R e t Va l传递给它。这使得该操作系统能够撤消进程并设置它的e x i t代码。
4.1.1 进程的实例句柄
加载到进程地址空间的每个可执行文件或 D L L文件均被赋予一个独一无二的实例句柄。([重要])可执行文件的实例作为( w ) Wi n M a i n的第一个参数h i n s t E x e来传递。对于加载资源的函数调用来说,通常都需要该句柄的值。例如,若要从可执行文件的映象来加载图标资源,需要调用下面这个函数:
HICON LoadIcon(
HINSTANCE hinst,
PCTSTR pszIcon
);
L o a d I c o n的第一个参数用于指明哪个文件(可执行文件或 D L L文件)包含你想加载的资源。[重要]许多应用程序在全局变量中保存 ( w ) Wi n M a i n的h i n s t E x e参数,这样,它就很容易被所有可执行文件的代码访问。
Platform SDK文档中说,有些函数需要H M O D U L E类型的一个参数。它的例子是下面所示的G e t M o d u l e F i l e N a m e函数:
DWORD GetMoudleFileName(
HMODULE hinstModule,
PTSTR pszPath,
DWORD cchPath
);
注意 实际情况说明, H M O D U L E与H I N S TA N C E是完全相同的对象。如果函数的文档指明需要一个H M O D U L E,那么可以传递一个 H I N S TA N C E,反过来,如果需要一个H I N S TA N C E,也可以传递一个 H M O D U L E。之所以存在两个数据类型,原因是在1 6位Wi n d o w s中, H M O D U L E和H I N S TA N C E用于标识不同的东西。
( w ) Wi n M a i n的h i n s t E x e参数的实际值是系统将可执行文件的映象加载到进程的地址空间时使用的基本地址空间。例如,如果系统打开了可执行文件并且将它的内容加载到地址0 x 0 0 4 0 0 0 0 0中,那么( w ) Wi n M a i n的h i n s t E x e参数的值就是0 x 0 0 4 0 0 0 0 0。
可执行文件的映像加载到的基地址是由链接程序决定的。不同的链接程序可以使用不同的默认基地址。 Visual C++链接程序使用的默认基地址是 0 x 0 0 4 0 0 0 0 0,因为这是在运行Wi n d o w s9 8时可执行文件的映象可以加载到的最低地址。可以改变应用程序加载到的基地址,方法是使用M i c r o s o f t的链接程序中的/ B A S E : a d d r e s s链接程序开关。
如果你想在Wi n d o w s上加载的可执行文件的基地址小于 0 x 0 0 4 0 0 0 0 0,那么Windows 98加载程序必须将可执行文件重新加载到另一个地址。这会增加加载应用程序所需的时间,不过,这样一来,至少该应用程序能够运行。如果开发的应用程序将要同时在 Windows 98和Wi n d o w s2 0 0 0上运行,应该确保应用程序的基地址是 0 x 0 0 4 0 0 0 0 0或者大于这个地址。
下面的Ge t M o d u l e H a n d l e函数返回可执行文件或 D L L文件加载到进程的地址空间时所用的句柄/基地址:
HMODULE GetMoDuleHandle(
PCTSTR pszMoDule
);
当调用该函数时,你传递一个以 0结尾的字符串,用于设定加载到调用进程的地址空间的可执行文件或 D L L 文件的名字。如果系统找到了指定的可执行文件或 D L L 文件名,G e t M o d u l e H a n d l e便返回该可执行文件或 D L L文件映象加载到的基地址。如果系统没有找到该文件,则返回 N U L L。也可以调用 G e t M o d u l e H a n d l e ,为 p s z M o d u l e 参数传递 N U L L,G e t M o d u l e H a n d l e返回调用的可执行文件的基地址。这正是 C运行期启动代码调用 ( w ) Wi n M a i n函数时该代码执行的操作。
请记住G e t M o d u l e H a n d l e函数的两个重要特性。首先,它只查看调用进程的地址空间。如果调用进程不使用常用的对话框函数,那么调用G e t M o d u l e H a n d l e并为它传递“C o m D l g 3 2”后,就会返回 N U L L ,尽管 C o m D l g 3 2 . d l l可能加载到了其他进程的地址空间。第二,调用G e t M o d u l e H a n d l e并传递N U L L值,就会返回进程的地址空间中可执行文件的基地址。因此,即使通过包含在 D L L中的代码来调用(N U L L),返回的值也是可执行文件的基地址,而不是D L L文件的基地址。
4.1.2 进程的前一个实例句柄
如前所述, C / C + +运行期启动代码总是将N U L L传递给( w ) Wi n M a i n的h i n s t E x e P r e v参数。该参数用在1 6位Wi n d o w s中,并且保留了( w ) Wi n M a i n的一个参数,目的仅仅是为了能够容易地转用1 6位Wi n d o w s应用程序。决不应该在代码中引用该参数。由于这个原因,我总是像下面这样编写( w ) Wi n M a i n函数:
int WINAPI WinMain(
HINSTANCE hinstExe,
HINSTANCE,
PSTR pszCmdLine,
int nCmdShow
);
由于没有为第二个参数提供参数名,因此编译器不会发出“没有引用参数”的警告。
4.1.3 进程的命令行
当一个新进程创建时,它要传递一个命令行。该命令行几乎永远不会是空的,至少用于创建新进程的可执行文件的名字是命令行上的第一个标记。但是在后面介绍 C r e a t e P r o c e s s函数时我们将会看到,进程能够接收由单个字符组成的命令行,即字符串结尾处的零。当 C运行期的启动代码开始运行的时候,它要检索进程的命令行,跳过可执行文件的名字,并将指向命令行其余部分的指针传递给Wi n M a i n的p s z C m d L i n e参数。
值得注意的是, p s z C m d L i n e参数总是指向一个 A N S I字符串。但是,如果将 Wi n M a i n改为w Wi n M a i n,就能够访问进程的 U n i c o d e版本命令行。应用程序可以按照它选择的方法来分析和转换命令行字符串。实际上可以写入 p s z C m d L i n e参数指向的内存缓存,但是在任何情况下都不应该写到缓存的外面去。我总是将它视为只读缓存。如果我想修改命令行,首先我要将命令行拷贝到应用程序的本地缓存中,然后再修改本地缓存。
也可以获得一个指向进程的完整命令行的指针,方法是调用 G e t C o m m a n d L i n e函数:
PTSTR GetCommandLine();
该函数返回一个指向包含完整命令行的缓存的指针,该命令行包括执行文件的完整路径名。
许多应用程序常常拥有转换成它的各个标记的命令行。使用全局性 _ _ a rg c(或_ _ w a rg v)变量,应用程序就能访问命令行的各个组成部分。下面这个函数 C o m m a n d L i n e To A rg v W将U n i c o d e字符串分割成它的各个标记:
PWSTR CommandLineToArgvW(
PWSTR pszCmdLine,
int* pNymArgs
);
正如该函数名的结尾处的 W所暗示的那样,该函数只存在于 U n i c o d e版本中(W是英文单词‘Wi d e’的缩写)。第一个参数 p s z C m d L i n e指向一个命令行字符串。这通常是较早时调用G e t C o m m a n d L i n e W而返回的值。 P N u m A rg s参数是个整数地址,该整数被设置为命令行中的参数的数目。 C o m m a n d L i n e To A rg v W将地址返回给一个U n i c o d e字符串指针的数组。
C o m m a n e L i n e To A rg v W负责在内部分配内存。大多数应用程序不释放该内存,它们在进程运行终止时依靠操作系统来释放内存。这是完全可行的。但是如果想要自己来释放内存,正确的方法是像下面这样调用H e a p F r e e函数:
4.1.4 进程的环境变量
每个进程都有一个与它相关的环境块。环境块是进程的地址空间中分配的一个内存块。每个环境块都包含一组字符串,其形式如下:
每个字符串的第一部分是环境变量的名字,后跟一个等号,等号后面是要赋予变量的值。环境块中的所有字符串都必须按环境变量名的字母顺序进行排序。
由于等号用于将变量名与变量的值分开,因此等号不能是变量名的一部分。另外,变量中的空格是有意义的。例如,如果声明下面两个变量,然后将 X Y Z的值与A B C的值进行比较,那么系统将报告称,这两个变量是不同的,因为紧靠着等号的前面或后面的任何空格均作为比较时的条件被考虑在内。
例如,如果将下面两个字符串添加给环境块,后面带有空格的环境变量 X Y Z包含H o m e,
而没有空格的环境变量X Y Z则包含Wo r k。
最后,必须将一个0字符置于所有环境变量的结尾处,以表示环境块的结束。
Wi n d o w s 9 8 若要为 Windows 98 创建一组初始环境变量,必须修改系统的
A u t o E x e c . b a t文件,将一系列S E T行放入该文件。每个S E T行都必须采用下面的形式:
当重新引导系统时, A u t o E x e c . b a t文件的内容被分析,设置的任何环境变量均可
供在Windows 98会话期间启动的任何进程使用。
Windows 2000 当用户登录到Windows 2000中时,系统创建一个外壳进程并将
一组环境字符串与它相关联。通过查看注册表中的两个关键字,系统可以获得一组初
始环境字符串。
第一个关键字包含一个适用于系统的所有环境变量的列表:
第二个关键字包含适用于当前登录的用户的所有环境变量的列表:
用户可以对这些项目进行增加、删除或修改,方法是选定控制面板的S y s t e m小应用
程序,单击A d v a n c e d选项卡,再单击Environment Va r i a b l e s按钮,打开图4 - 2所示的对话框:
图4-2 使用Environment Variables 对话框修改变量
52计计第二部分 编程的具体方法
下载
只有拥有管理员权限的用户才能修改系统变量列表中的变量。
应用程序也可以使用各种注册表函数来修改这些注册表项目。但是,若要使这些
修改在所有应用程序中生效,用户必须退出系统,然后再次登录。有些应用程序,如
E x p l o r e r、 Task Manager和 Control Panel等 , 在 它 们 的 主 窗 口 收 到 W M _
S E T T I N G C H A N G E消息时,用新注册表项目来更新它们的环境块。例如,如果要更新
注册表项目,并且想让有关的应用程序更新它们的环境块,可以调用下面的代码:
通常,子进程可以继承一组与父进程相同的环境变量。但是,父进程能够控制子进程继承
什么环境变量,后面介绍 C r e a t e P r o c e s s函数时就会看到这个情况。所谓继承,指的是子进程获
得它自己的父进程的环境块拷贝,子进程与父进程并不共享相同的环境块。这意味着子进程能
够添加、删除或修改它的环境块中的变量,而这个变化在父进程的环境块中却得不到反映。
应用程序通常使用环境变量来使用户能够调整它的行为特性。用户创建一个环境变量并对
它进行初始化。然后,当用户启动应用程序运行时,该应用程序要查看环境块,找出该变量。
如果找到了变量,它就分析变量的值,调整自己的行为特性。
环境变量存在的问题是,用户难以设置或理解这些变量。用户必须正确地拼写变量的名字,
而且必须知道变量值期望的准确句法。另一方面,大多数图形应用程序允许用户使用对话框来
调整应用程序的行为特性。这种方法对用户来说更加友好。
如果仍然想要使用环境变量,那么有几个函数可供应用程序调用。使用 G e t E n v i r o n m e n t
Va r i a b l e函数,就能够确定某个环境变量是否存在以及它的值:
当调用G e t E n v i r o n m e n t Va r i a b l e时, p s z N a m e指向需要的变量名, p s z Va l u e指向用于存放变
量值的缓存, c c h Va l u e用于指明缓存的大小(用字符数来表示)。该函数可以返回拷贝到缓存
的字符数,如果在环境中找不到该变量名,也可以返回 0。
许多字符串包含了里面可取代的字符串。例如,我在注册表中的某个地方找到了下面的字
符串:
百分数符号之间的部分表示一个可取代的字符串。在这个例子中,环境变量的值
USERPROFILE应该被放入该字符串中。在我的计算机中,我的USERPROFILE环境变量的值是:
因此,在执行字符串替换后,产生的字符串就成为:
由于这种类型的字符串替换是很常用的,因此 Wi n d o w s提供了E x p a n d E n v i r o n m e n t S t r i n g s函
数:
当调用该函数时, p s z S r c参数是包含可替换的环境变量字符串的这个字符串的地址。 p s z D s t
第 4章 进 程计计53
下载
参数是接收已展开字符串的缓存的地址, n S i z e参数是该缓存的最大值(用字符数来表示)。
最后,可以使用S e t E n v i r o n m e n t Va r i a b l e函数来添加变量、删除变量或者修改变量的值:
该函数用于将p s z N a m e参数标识的变量设置为 p s z Va l u e参数标识的值。如果带有指定名字
的变量已经存在, S e t E n v i r o n m e n t Va r i a b l e就修改该值。如果指定的变量不存在,便添加该变量,
如果p s z Va l u e是N U L L,便从环境块中删除该变量。
应该始终使用这些函数来操作进程的环境块。前面讲过,环境块中的字符串必须按变量名
的字母顺序来存放,这样, S e t E n v i r o n m e n t Va r i a b l e就会很容易地找到它们。 S e t E n v i r o n m e n t
Va r i a b l e函数具有足够的智能,使环境变量保持有序排列。
4.1.5 进程的亲缘性
一般来说,进程中的线程可以在主计算机中的任何一个 C P U上执行。但是一个进程的线程
可能被强制在可用 C P U的子集上运行。这称为进程的亲缘性,将在第 7章详细介绍。子进程继
承了父进程的亲缘性。
4.1.6 进程的错误模式
与每个进程相关联的是一组标志,用于告诉系统,进程对严重的错误应该如何作出反映,
这包括磁盘介质故障、未处理的异常情况、文件查找失败和数据没有对齐等。进程可以告诉系
统如何处理每一种错误。方法是调用 S e t E r r o r M o d e函数:
f u E r r o r M o d e参数是表4 - 3的任何标志按位用O R连接在一起的组合。
表4-3 fuError Mode 参数的标志
标 志 说 明
S E M _ FA I L C R I T I C A L E R R O R S 系统不显示关键错误句柄消息框,并将错误返回给调用进程
S E M _ N O G O FA U LT E R R O R B O X 系统不显示一般保护故障消息框。本标志只应该由采用异常情
况处理程序来处理一般保护(G P)故障的调试应用程序来设定
SEM_NOOPENFILEERRORBOX 当系统找不到文件时,它不显示消息框。
S E M _ N O A L I G N M E N T FA U LT E X C E P T 系统自动排除内存没有对齐的故障,并使应用程序看不到这些
故障。本标志对x 8 6处理器不起作用
默认情况下,子进程继承父进程的错误模式标志。换句话说,如果一个进程的
S E M _ N O G P FA U LT E R R O R B O X标志已经打开,并且生成了一个子进程,该子进程也拥有这个
打开的标志。但是,子进程并没有得到这一情况的通知,它可能尚未编写以便处理 G P故障的
错误。如果 G P故障发生在子进程的某个线程中,该子进程就会终止运行,而不通知用户。父
进 程 可 以 防 止 子 进 程 继 承 它 的 错 误 模 式 , 方 法 是 在 调 用 C r e a t e P r o c e s s 时 设 定
C R E AT E _ D E FA U LT _ E R R O R _ M O D E标志(本章后面部分的内容将要介绍 C r e a t e P r o c e s s函数)。
4.1.7 进程的当前驱动器和目录
当不提供全路径名时, Wi n d o w s的各个函数就会在当前驱动器的当前目录中查找文件和目
54计计第二部分 编程的具体方法
下载
录。例如,如果进程中的一个线程调用 C r e a t e F i l e来打开一个文件(不设定全路径名),那么系
统就会在当前驱动器和目录中查找该文件。
系统将在内部保持对进程的当前驱动器和目录的跟踪。 由于该信息是按每个进程来维护的,
因此改变当前驱动器或目录的进程中的线程,就可以为该进程中的所有线程改变这些信息。
通过调用下面两个函数,线程能够获得和设置它的进程的当前驱动器和目录:
4.1.8 进程的当前目录
系统将对进程的当前驱动器和目录保持跟踪,但是它不跟踪每个驱动器的当前目录。不过,
有些操作系统支持对多个驱动器的当前目录的处理。这种支持是通过进程的环境字符串来提供
的。例如,进程能够拥有下面所示的两个环境变量:
这些变量表示驱动器C的进程的当前目录是\ U t i l i t y \ B i n,并且指明驱动器D的进程的当前目
录是\Program Files。
如果调用一个函数,传递一个驱动器全限定名,以表示一个驱动器不是当前驱动器,那么
系统就会查看进程的环境块,找出与指定驱动器名相关的变量。如果该驱动器的变量存在,系
统将该变量的值用作当前驱动器。如果该变量不存在,系统将假设指定驱动器的当前目录是它
的根目录。
例如,如果进程的当前目录是 C : \ U t i l i t y | B i n,并且你调用C r e a t e F i l e来打开D : R e a d M e . T x t,
那么系统查看环境变量 = D。因为= D变量存在,因此系统试图从 D:\Program Files目录打开该
R e a d M e . T x t文件。如果 = D变量不存在,系统将试图从驱动器 D的根目录来打开 R e a d M e . T x t。
Wi n d o w s的文件函数决不会添加或修改驱动器名的环境变量,它们只是读取这些变量。
注意 可以使用C运行期函数_ c h d i r,而不是使用Wi n d o w s的S e t C u r r e n t D i r e c t o r y函数来
变更当前目录。 _ c h d i r函数从内部调用S e t C u r r e n t D i r e c t o r y,但是_chdir 也能够添加或
修改该环境变量,这样,不同驱动器的当前目录就可以保留。
如果父进程创建了一个它想传递给子进程的环境块,子进程的环境块不会自动继承父进程
的当前目录。相反,子进程的当前目录将默认为每个驱动器的根目录。如果想要让子进程继承
父进程的当前目录,该父进程必须创建这些驱动器名的环境变量。并在生成子进程前将它们添
加给环境块。通过调用G e t F u l l P a t h N a m e,父进程可以获得它的当前目录:
例如,若要获得驱动器C的当前目录,可以像下面这样调用 G e t F u l l P a t h N a m e:
记住,进程的环境变量必须始终按字母顺序来排序。因此驱动器名的环境变量通常必须置
第 4章 进 程计计55
下载
于环境块的开始处。
4.1.9 系统版本
应用程序常常需要确定用户运行的是哪个 Wi n d o w s版本。例如,通过调用安全性函数,应
用程序就能利用它的安全特性。但是这些函数只有在 Windows 2000上才能得到全面的实现。
Windows API拥有下面的G e t Ve r s i o n函数:
该函数已经有相当长的历史了。最初它是为 1 6位Wi n d o w s设计的。它的作用很简单,在高
位字中返回M S - D O S版本号,在低位字中返回 Wi n d o w s版本号。对于每个字来说,高位字节代
表主要版本号,低位字节代表次要版本号。
但是,编写该代码的程序员犯了一个小小的错误,函数的编码结果使得 Wi n d o w s的版本号
颠倒了,即主要版本号位于低位字节,而次要版本号位于高位字节。由于许多程序员已经开始
使用该函数, M i c r o s o f t不得不保持函数的原样,并修改了文档,以说明这个错误。
由于围绕着 G e t Ve r s i o n 函数存在着各种混乱,因此 M i c r o s o f t 增加了一个新函数
G e t Ve r s i o n E x :
该函数要求在应用程序中指定一个 O S V E R S I O N I N F O E X结构,并将该结构的地址传递给
G e t Ve r s i o n E x。 O S V E R S I O N I N F O E X的结构如下所示:
O S V E R S I O N I N F O E X结构在Windows 2000中是个新结构。 Wi n d o w s的其他版本使用较老
的O S V E R S I O N I N F O结构,它没有服务程序包、程序组屏蔽、产品类型和保留成员。
注意,对于系统的版本号中的每个成分来说,该结构拥有不同的成员。这样做的目的是,
程序员不必提取低位字、高位字、低位字节和高位字节,因此应用程序能够更加容易地对它们
期望的版本号与主机系统的版本号进行比较。表 4 - 4描述了O S V E R S I O N I N F O E X结构的成员。
表4-4 OSVERSIONINFOEX结构的成员
成 员 描 述
d w O S Ve r s i o n I n f o S i z e 在调用 G e t Ve r s i o n E x函数之前 ,必须置为 s i z e o f ( O S V E R S I O N I N F O )或
s i z e o f ( O S V E R S I O N I N F O E X )。
d w M a j o r Ve r s i o n 主系统的主要版本号
d w M i n o r Ve r s i o n 主系统的次要版本号
d w B u i l d N u m b e r 当前系统的构建号
56计计第二部分 编程的具体方法
下载
(续)
成 员 描 述
dw Platform Id 用于标识当前系统支持的平台。它可以是 V E R _ P L AT F O R M _ W I N 3 2
(Wi n 3 2), V E R _ P L AT F O R M _ W I N 3 2 _ W I N D O W S(Windows 95/Wi n d o w s
9 8), V E R _ P L AT F O R M _ W I N 3 2 _ N T(Windows NT/Windows 2000)或
V E R _ P L AT F O R M _ W I N 3 2 _ C E H H(Windows CE)
s z C S D Ve r s i o n 本域包含了附加文本,用于提供关于已经安装的操作系统的详细信息
w S e r v i c e P a c k M a j o r 最新安装的服务程序包的主要版本号
w S e r v i c e P a c k M i n o r 最新安装的服务程序包的次要版本号
w S u i t e M a s k 用于标识系统上存在哪个程序组( V E R _ S U I T E _ S M A L L B U S I N E S S ,
V E R _ S U I T E _ E N T E R P R I S E , V E R _ S U I T E _ B A C K O F F I C E , V E R _ S U I T E _
C O M M U N I C ATIONS,VER_SUITE_TERMINAL,VER_SUITE_ SMALLBUSINESS_
RESTRICTED, VER_SUITE_EMBEDDEDNT和VER_SUITE_ DATA C E N T E R)
w P r o d u c t Ty p e 用于标识安装了下面的哪个操作系统: V E R _ N T _ W O R K S TAT I O N ,
V E R _ N T _ S E RV E R或V E R _ N T _ D O M A I N _ C O N T R O L L E R
w R e s e r v e d 留作将来使用
为了使操作更加容易, Windows 2000提供了一个新的函数,即 Ve r i f y Ve r s i o n I n f o,用于对
主机系统的版本与你的应用程序需要的版本进行比较:
若要使用该函数,必须指定一个O S V E R S I O N I N F O E X结构,将它的d w O S Ve r s i o n I n f o S i z e成
员初始化为该结构的大小,然后对该结构中的其他成员(这些成员对你的应用程序来说很重要)
进行初始化。当调用Ve r i f y Ve r s i o n I n f o时, d w Ty p e M a s k参数用于指明该结构的哪些成员已经进
行了初始化。 d w Ty p e M a s k 参数是用 O R连接在一起的下列标志中的任何一个标志:
V E R _ M I N O RV E R S I O N, V E R _ M A J O RV E R S I O N, V E R _ B U I L D N U M B E R, V E R _ P L AT F O R M I D,
VER_ SERV I C E PA C K M I N O R, V E R _ S E RV I C E PA C K M A J O R, V E R _ S U I T E N A M E,
VER_PRODUCT_ TYPE。最后一个参数d w l C o n d i t i o n M a s k是个6 4位值,用于控制该函数如何将
系统的版本信息与需要的信息进行比较。
d w l C o n d i t i o n M a s k描述了如何使用一组复杂的位组合进行的比较。若要创建需要的位组合,
可以使用V E R _ S E T _ C O N D I T I O N宏:
第一个参数d w l C o n d i t i o n M a s k用于标识一个变量,该变量的位是要操作的那些位。请注意,
不必传递该变量的地址,因为 V E R _ S E T _ C O N D I T I O N 是个宏,不是一个函数。
d w Ty p e B i t M a s k参数用于指明想要比较的O S V E R S I O N I N F O E X结构中的单个成员。若要比较多
个成员,必须多次调用 V E R _ S E T _ C O N D I T I O N 宏,每个成员都要调用一次。传递给
Ve r i f y Ve r s i o n I n f o的d w Ty p e M a s k参数(V E R _ M I N O RV E R S I O N, V E R _ B U I L D N U M B E R等)的
标志与用于V E R _ S E T _ C O N D I T I O N的d w Ty p e B i t M a s k参数的标志是相同的。
V E R _ S E T _ C O N D I T I O N的最后一个参数d w C o n d i t i o n M a s k用于指明想如何进行比较。它可
以是下列值之一: V E R _ E Q U A L, V E R _ G R E AT E R, V E R _ G R E AT E R _ E Q U A L, V E R _ L E S S或
V E R _ L E S S _ E Q U A L。请注意,当比较V E R _ P R O D U C T _ T Y P E信息时,可以使用这些值。例如,
第 4章 进 程计计57
下载
V E R _ N T _ W O R K S TAT I O N小于V E R _ N T _ S E RV E R。但是对于V E R _ S U I T E N A M E信息来说,不
能使用这些测试值。相反,必须使用 V E R _ A N D(所有程序组都必须安装)或 V E R _ O R(至少
必须安装程序组产品中的一个产品)。
当建立一组条件后,可以调用 Ve r i f y Ve r s i o n I n f o函数,如果调用成功(如果主机系统符合
应用程序的所有要求),则返回非零值。如果 Ve r i f y Ve r s i o n I n f o返回0,那么主机系统不符合要
求,或者表示对该函数的调用不正确。通过调用 G e t L a s t E r r o r函数,就能确定该函数为什么返
回0。如果G e t L a s t E r r o r返回E R R O R _ O L D _ W I N _ V E R S I O N,那么对该函数的调用是正确的,
但是系统没有满足要求。
下面是如何测试主机系统是否正是 Windows 2000的一个例子:
4.2 CreateProcess函数
可以用C r e a t e P r o c e s s函数创建一个进程:
WIN32API函数CreateProcess用来创建一个新的进程和它的主线程,这个新进程运行指定的可执行文件。
函数原型:
BOOL CreateProcess
(
LPCTSTR lpApplicationName,
LPTSTR lpCommandLine,
LPSECURITY_ATTRIBUTES lpProcessAttributes。
LPSECURITY_ATTRIBUTES lpThreadAttributes,
BOOL bInheritHandles,
DWORD dwCreationFlags,
LPVOID lpEnvironment,
LPCTSTR lpCurrentDirectory,
LPSTARTUPINFO lpStartupInfo,
LPPROCESS_INFORMATION lpProcessInformation
);
lpApplicationName:指向一个NULL结尾的、用来指定可执行模块的字符串。 这个字符串可以使可执行模块的绝对路径,也可以是相对路径,在后一种情况下,函数使用当前驱动器和目录建立可执行模块的路径。
这个参数可以被设为NULL,在这种情况下,可执行模块的名字必须处于 lpCommandLine 参数的最前面并由空格符与后面的字符分开。
lpCommandLine:指向一个NULL结尾的、用来指定要运行的命令行。
这个参数可以为空,那么函数将使用参数指定的字符串当作要运行的程序的命令行。
如果lpApplicationName和lpCommandLine参数都不为空,那么lpApplicationName参数指定将要被运行的模块,lpCommandLine参数指定将被运行的模块的命令行
。新运行的进程可以使用GetCommandLine函数获得整个命令行。
lpProcessAttributes:指向一个SECURITY_ATTRIBUTES结构体,这个结构体决定是否返回的句柄可以被子进程继承。如果lpProcessAttributes参数为空(NULL)
,那么句柄不能被继承。
lpThreadAttributes:指向一个SECURITY_ATTRIBUTES结构体,这个结构体决定是否返回的句柄可以被子进程继承。如果lpThreadAttributes参数为空(NULL),
那么句柄不能被继承。
bInheritHandles:指示新进程是否从调用进程处继承了句柄。如果参数的值为真,调用进程中的每一个可继承的打开句柄都将被子进程继承。被继承的句柄与原
进程拥有完全相同的值和访问权限。
dwCreationFlags:指定附加的、用来控制优先类和进程的创建的标志。
lpEnvironment:指向一个新进程的环境块。如果此参数为空,新进程使用调用进程的环境。
一个环境块存在于一个由以NULL结尾的字符串组成的块中,这个块也是以NULL结尾的。每个字符串都是name=value的形式。
lpCurrentDirectory:指向一个以NULL结尾的字符串,这个字符串用来指定子进程的工作路径。这个字符串必须是一个包含驱动器名的绝对路径。如果这个参数
为空,新进程将使用与调用进程相同的驱动器和目录。这个选项是一个需要启动启动应用程序并指定它们的驱动器和工作目录的外壳程序的主要条件。
lpStartupInfo:指向一个用于决定新进程的主窗体如何显示的STARTUPINFO结构体。
lpProcessInformation:指向一个用来接收新进程的识别信息的PROCESS_INFORMATION结构体。
STARTUPINFO结构体
typedef struct _STARTUPINFO
{
DWORD cb; //包含STARTUPINFO结构中的字节数.如果Microsoft将来扩展该结构,它可用作版本控制手段.
应用程序必须将cb初始化为sizeof(STARTUPINFO)
PSTR lpReserved; //保留。必须初始化为N U L L
PSTR lpDesktop; //用于标识启动应用程序所在的桌面的名字。如果该桌面存在,新进程便与指定的桌面相关联。
如果桌面不存在,便创建一个带有默认属性的桌面,并使用为新进程指定的名字。
如果lpDesktop是NULL(这是最常见的情况),那么该进程将与当前桌面相关联
PSTR lpTitle; //用于设定控制台窗口的名称。如果l p Ti t l e 是N U L L ,则可执行文件的名字将用作窗口名
DWORD dwX; //用于设定应用程序窗口在屏幕上应该放置的位置的x 和y 坐标(以像素为单位)。
DWORD dwY; 只有当子进程用CW_USEDEFAULT作为CreateWindow的x参数来创建它的第一个重叠窗口时,
才使用这两个坐标。若是创建控制台窗口的应用程序,这些成员用于指明控制台窗口的左上角
DWORD dwXSize; //用于设定应用程序窗口的宽度和长度(以像素为单位)只有dwYsize
DWORD dwYSize; 当子进程将C W _ U S E D E FA U LT 用作C r e a t e Wi n d o w 的
n Wi d t h参数来创建它的第一个重叠窗口时,才使用这些值。
若是创建控制台窗口的应用程序,这些成员将用于指明控制台窗口的宽度
DWORD dwXCountChars; //用于设定子应用程序的控制台窗口的宽度和高度(以字符为单位)
DWORD dwYCountChars;
DWORD dwFillAttribute; //用于设定子应用程序的控制台窗口使用的文本和背景颜色
DWORD dwFlags; //请参见下一段和表4 - 7 的说明
WORD wShowWindow; //用于设定如果子应用程序初次调用的S h o w Wi n d o w 将S W _ S H O W D E FA U LT 作为
n C m d S h o w 参数传递时,该应用程序的第一个重叠窗口应该如何出现。
本成员可以是通常用于Show Wi n d o w 函数的任何一个S W _ *标识符
WORD cbReserved2; //保留。必须被初始化为0
PBYTE lpReserved2; //保留。必须被初始化为N U L L
HANDLE hStdInput; //用于设定供控制台输入和输出用的缓存的句柄。
按照默认设置,h S t d I n p u t 用于标识键盘缓存,
h S t d O u t p u t 和h S t d E r r o r用于标识控制台窗口的缓存
HANDLE hStdOutput;
HANDLE hStdError;
} STARTUPINFO, *LPSTARTUPINFO;
当Wi n d o w s 创建新进程时,它将使用该结构的有关成员。大多数应用程序将要求生成的应用程序仅仅使用默认值。至少应该将该结构中的所有成员初始化为
零,然后将c b 成员设置为该结构的大小:
STARTUPINFO si = { sizeof(si) };
CreateProcess(...,&si,...);
dwFlags 使用标志及含义
标志 含义
STARTF_USESIZE // 使用d w X S i z e 和d w Y S i z e 成员
STARTF_USESHOWWINDOW //使用w S h o w Wi n d o w 成员
STARTF_USEPOSITION //使用d w X 和d w Y 成员
STARTF_USECOUNTCHARS //使用d w X C o u n t C h a r s 和dwYCount Chars 成员
STARTF_USEFILLATTRIBUTE //使用d w F i l l A t t r i b u t e 成员
STARTF_USESTDHANDLES //使用h S t d I n p u t 、h S t d O u t p u t 和h S t d E r r o r 成员
STARTF_RUN_FULLSCREEN //强制在x 8 6 计算机上运行的控制台应用程序以全屏幕方式启动运行
PROCESS_INFORMATION结构体
typedef struct _PROCESS_INFORMATION { HANDLE hProcess; HANDLE hThread; DWORD dwProcessId; DWORD dwThreadId;
} PROCESS_INFORMATION, *LPPROCESS_INFORMATION;
当一个线程调用C r e a t e P r o c e s s时,系统就会创建一个进程内核对象,其初始使用计数是 1。该进程内核对象不是进程本身,而是操作系统管理进程时使用的一个较小的数据结构。可以将进程内核对象视为由进程的统计信息组成的一个较小的数据结构。然后,系统为新进程创建一个虚拟地址空间,并将可执行文件或任何必要的 D L L文件的代码和数据加载到该进程的地址空间中。
然后,系统为新进程的主线程创建一个线程内核对象(其使用计数为 1)。与进程内核对象一样,线程内核对象也是操作系统用来管理线程的小型数据结构。通过执行 C / C + +运行期启动代码,该主线程便开始运行,它最终调用 Wi n M a i n、 w Wi n M a i n、 m a i n或w m a i n函数。如果系统成功地创建了新进程和主线程, C r e a t e P r o c e s s便返回T R U E。
注意 在进程被完全初始化之前, C r e a t e P r o c e s s返回T R U E。这意味着操作系统加载程序尚未试图找出所有需要的 D L L。如果一个 D L L无法找到,或者未能正确地初始化,那么该进程就终止运行。由于 C r e a t e P r o c e s s返回T R U E,因此父进程不知道出现的任何初始化问题。
这就是总的概述。下面各节将分别介绍 C r e a t e P r o c e s s的各个参数。
4.2.1 pszApplicationName和p s z C o m m a n d L i n e
p s z A p p l i c a t i o n N a m e和p s z C o m m a n d L i n e参数分别用于设定新进程将要使用的可执行文件的名字和传递给新进程的命令行字符串。下面首先让我们谈一谈 p s z C o m m a n d L i n e参数。
注意 请注意, p s z C o m m a n d L i n e参数的原型是P T S T R。这意味着C r e a t e P r o c e s s期望你将传递一个非常量字符串的地址。从内部来讲, C r e a t e P r o c e s s实际上并不修改你传递给它的命令行字符串。不过,在 C r e a t e P r o c e s s返回之前,它将该字符串恢复为它的原始形式。
这个问题很重要,因为如果命令行字符串不包含在文件映象的只读部分中,就会出现违规访问的问题。例如,下面的代码就会导致违规访问的问题,因为 Visual C++将“N O T E PA D”字符串放入了只读内存:
当C r e a t e P r o c e s s试图修改该字符串时,就会发生违规访问(较早的 Visual C++版本将该字符串放入读/写内存,因此调用C r e a t e P r o c e s s不会导致违规访问的问题)。
解决这个问题的最好办法是在调用 C r e a t e P r o c e s s之前像下面这样将常量字符串拷贝到临时缓存中:
也可以考虑使用Visual C++的/ G f和/ G F编译器开关,这些开关用于控制重复字符串的删除和确定这些字符串是否被放入只读内存部分(另外请注意, / Z I开关允许使用Visual Studio的Edit &Continue调试特性,它包含了/ G F开关的功能)。能做的最好工作是使用/ G F编译器开关和临时缓存。 M i c r o s o f t能做的最好事情是安装好C r e a t e - P r o c e s s,使它能够制作一个该字符串的临时拷贝,这样我们就不必进行这项操作。也许将来的Wi n d o w s版本能够做到这一点。
另外,如果调用Windows 2000上的C r e a t e P r o c e s s的A N S I版本,就不会违规访问,因为系统已经制作了一个命令行字符串的临时拷贝(详细信息请见第 2章)。
可以使用 p s z C o m m a n d L i n e参数设定一个完整的命令行,以便 C r e a t e P r o c e s s用来创建新进程。当C r e a t e P r o c e s s分析p s z C o m m a n d L i n e字符串时,它将查看字符串中的第一个标记,并假设该标记是想运行的可执行文件的名字。如果可执行文件的文件名没有扩展名,便假设它的扩展名为. e x e。 C r e a t e P r o c e s s也按下面的顺序搜索该可执行文件:
1) 包含调用进程的. e x e文件的目录。
2) 调用进程的当前目录。
3) Wi n d o w s的系统目录。
4) Wi n d o w s目录。
5) PAT H环境变量中列出的目录。
当然,如果文件名包含全路径,系统将使用全路径来查看可执行文件,并且不再搜索这些目录。如果系统找到了可执行文件,那么它就创建一个新进程,并将可执行文件的代码和数据映射到新进程的地址空间中。然后系统将调用 C / C + +运行期启动例程。正如前面我们讲过的那样, C / C + +运行期启动例程要查看进程的命令行,并将地址作为 ( w ) Wi n M a i n的p s z C m d L i n e参数传递给可执行文件的名字后面的第一个参数。
这一切都是在p s z A p p l i c a t i o n N a m e参数是N U L L(9 9 %以上的时候都应该属于这种情况)时发生的。如果不传递N U L L,可以将地址传递给p s z A p p l i c a t i o n N a m e参数中包含想运行的可执行文件的名字的字符串。请注意,必须设定文件的扩展名,系统将不会自动假设文件名有一个. e x e扩展名。 C r e a t e P r o c e s s假设该文件位于当前目录中,除非文件名前面有一个路径。如果在
当前目录中找不到该文件, C r e a t e P r o c e s s将不会在任何其他目录中查找该文件,它运行失败了。但是,即使在 p s z A p p l i c a t i o n N a m e 参数中设定了文件名, C r e a t e P r o c e s s 也会将p s z C o m m a n d L i n e参数的内容作为它的命令行传递给新进程。例如,可以像下面这样调用
C r e a t e P r o c e s s :
系统启动N o t e p a d应用程序,但是N o t e p a d的命令行是W O R D PAD README.TXT。这种变异情况当然有些奇怪,不过这正是 C r e a t e P r o c e s s运行的样子。这个由 p s z A p p l i c a t i o n N a m e参数提供的能力实际上被添加给了C r e a t e P r o c e s s,以支持Windows 2000的P O S I X子系统。
4.2.2 psaProcess、 p s a T h r e a d和b i n h e r i t H a n d l e s
若要创建一个新进程,系统必须创建一个进程内核对象和一个线程内核对象(用于进程的主线程),由于这些都是内核对象,因此父进程可以得到机会将安全属性与这两个对象关联起来。可以使用p s a P r o c e s s和p s a T h r e a d参数分别设定进程对象和线程对象需要的安全性。可以为
这些参数传递N U L L,在这种情况下,系统为这些对象赋予默认安全性描述符。也可以指定两个S E C U R I T Y _ AT T R I B U T E S结构,并对它们进行初始化,以便创建自己的安全性权限,并将它们赋予进程对象和线程对象。
将S E C U R I T Y _ AT T R I B U T E S结构用于p s a P r o c e s s和p s a T h r e a d参数的另一个原因是,父进程将来生成的任何子进程都可以继承这两个对象句柄中的任何一个(第 3章已经介绍了内核对象句柄的继承性的有关理论)。
清单4 - 1显示了一个说明内核对象继承性的简单程序。假设 Process A创建了Process B,方法法是调用C r e a t e P r o c e s s,为p s a P r o c e s s参数传递一个S E C U R I T Y _ AT T R I B U T E S结构的地址,在这个结构中, b I n h e r i t H a n d l e s成员被置为 T R U E。在同样这个函数调用中, p s a T h r e a d参数指向另一个S E C U R I T Y _ AT T R I B U T E S结构,在这个结构中, b I n h e r i t H a n d l e s成员被置为FA L S E。
当系统创建Process B时,它同时指定一个进程内核对象和一个线程内核对象,并且将句柄返回给p p i P r o c I n f o参数(很快将介绍该参数)指向的结构中的Process A 。这时,使用这些句柄,Process A就能够对新创建的进程对象和线程对象进行操作。
现在,假设Process A第二次调用C r e a t e P r o c e s s函数,以便创建Process C。 Process A可以决定是否为Process C赋予对Process A能够访问的某些内核对象进行操作的能力。 B I n h e r i t H a n d l e s参数可以用于这个目的。如果 b I n h e r i t H a n d l e s被置为T R U E,系统就使Process C继承Process A中的任何可继承句柄。在这种情况下, Process B 的进程对象的句柄是可继承的。无论C r e a t e P r o c e s s的b I n h e r i t H a n d l e s参数的值是什么, Process B的主线程对象的句柄均不能继承。同样,如果Process A调用C r e a t e P r o c e s s,为b I n h e r i t H a n d l e s传递FA L S E,那么Process C将不能继承Process A 目前使用的任何句柄。
清单4-1 内核对象句柄继承性的一个示例
4.2.3 fdwCreate
f d w C r e a t e参数用于标识标志,以便用于规定如何来创建新进程。如果将标志逐位用 O R操
作符组合起来的话,就可以设定多个标志。
• E B U G _ P R O C E S S标志用于告诉系统,父进程想要调试子进程和子进程将来生成的任何
进程。本标志还告诉系统,当任何子进程(被调试进程)中发生某些事件时,将情况通
知父进程(这时是调试程序)。
• D E B U G _ O N LY _ T H I S _ P R O C E S S标志与D E B U G _ P R O C E S S标志相类似,差别在于,调
试程序只被告知紧靠父进程的子进程中发生的特定事件。如果子进程生成了别的进程,
那么将不通知调试程序在这些别的进程中发生的事件。
• C R E AT E _ S U S P E N D E D标志可导致新进程被创建,但是,它的主线程则被挂起。这使得
父进程能够修改子进程的地址空间中的内存,改变子进程的主线程的优先级,或者在进
程有机会执行任何代码之前将进程添加给一个作业。一旦父进程修改了子进程,父进程
将允许子进程通过调用R e s u m e T h r e a d函数来执行代码(第7章将作详细介绍)。
• D E TA C H E D _ P R O C E S S标志用于阻止基于C U I的进程对它的父进程的控制台窗口的访问,
并告诉系统将它的输出发送到新的控制台窗口。如果基于 C U I的进程是由另一个基于C U I
的进程创建的,那么按照默认设置,新进程将使用父进程的控制台窗口(当通过命令外
壳程序来运行 C编译器时,新控制台窗口并不创建,它的输出将被附加在现有控制台窗
口的底部)。通过设定本标志,新进程将把它的输出发送到一个新控制台窗口。
• C R E AT E _ N E W _ C O N S O L E标志负责告诉系统,为新进程创建一个新控制台窗口。如果
同时设定C R E AT E _ N E W _ C O N S O L E和D E TA C H E D _ P R O C E S S标志,就会产生一个错误。
62计计第二部分 编程的具体方法
下载
• C R E AT E _ N O _ W I N D O W标志用于告诉系统不要为应用程序创建任何控制台窗口。可以
使用本标志运行一个没有用户界面的控制台应用程序。
• C R E AT E _ N E W _ P R O C E S S _ G R O U P标志用于修改用户在按下 C t r l + C或C t r l + B r e a k键时得
到通知的进程列表。如果在用户按下其中的一个组合键时,你拥有若干个正在运行的
C U I进程,那么系统将通知进程组中的所有进程说,用户想要终止当前的操作。当创建
一个新的C U I进程时,如果设定本标志,可以创建一个新进程组。如果该进程组中的一
个进程处于活动状态时用户按下 C t r l + C或C t r l _ B r e a k键,那么系统只通知用户需要这个进
程组中的进程。
• C R E AT E _ D E FA U LT _ E R R O R _ M O D E标志用于告诉系统,新进程不应该继承父进程使用
的错误模式(参见本章前面部分中介绍的 S e t E r r o r M o d e函数)。
• C R E AT E _ S E PA R AT E _ W O W _ V D M标志只能当你在Windows 2000上运行1 6位Wi n d o w s应
用程序时使用。它告诉系统创建一个单独的 D O S虚拟机(V D M),并且在该V D M中运行
1 6位Wi n d o w s应用程序。按照默认设置,所有 1 6位Wi n d o w s应用程序都在单个共享的
V D M中运行。在单独的 VDM 中运行应用程序的优点是,如果应用程序崩溃,它只会使
单个V D M停止工作,而在别的 V D M中运行的其他程序仍然可以继续正常运行。另外,
在单独的V D M中运行的1 6位Wi n d o w s应用程序有它单独的输入队列。这意味着如果一个
应用程序临时挂起,在各个 V D M中的其他应用程序仍然可以继续接收输入信息。运行多
个V D M的缺点是,每个V D M都要消耗大量的物理存储器。 Windows 98在单个V D M中运
行所有的1 6位Wi n d o w s应用程序,不能改变这种情况。
• C R E AT E _ S H A R E D _ W O W _ V D M标志只能当你在Windows 2000上运行1 6位Wi n d o w s应用
程序时使用。按照默认设置,除非设定了 C R E AT E _ S E PA R AT E _ W O W _ V D M标志,否则
所有 1 6位Wi n d o w s 应用程序都必须在单个 V D M中运行。但是,通过在注册表中将
H K E Y _ L O C A L _ M A C H I N E \ s y s t e m \ C u r r e n t C o n t r o l S e t \ C o n t r o l \ W O W下的D e f a u l t S e p a r a t e
V D M 设置为“y e s ”,就可以改变该默认行为特性。这时, C R E AT E _ S H A R E D _
W O W _ V D M标志就在系统的共享V D M中运行1 6位Wi n d o w s应用程序。
• C R E AT E _ U N I C O D E _ E N V I R O N M E N T标志用于告诉系统,子进程的环境块应该包含
U n i c o d e字符。按照默认设置,进程的环境块包含的是 A N S I字符串。
• C R E AT E _ F O R C E D O S标志用于强制系统运行嵌入 1 6位O S / 2应用程序的M O S - D O S应用程
序。
• C R E AT E _ B R E A K AWAY _ F R O M _ J O B标志用于使作业中的进程生成一个与作业相关联的
新进程(详细信息见第5章)。
f d w C r e a t e参数也可以用来设定优先级类。不过用不着这样做,并且对于大多数应用程序来说
不应该这样做,因为系统会为新进程赋予一个默认优先级。表4 - 5显示了各种可能的优先级类别。
表4-5 优先级类别
优先级类别 标志的标识符
空闲 I D L E _ P R I O R I T Y _ C L A S S
低于正常 B E L O W _ N O R M A L _ P R I O R I T Y _ C L A S S
正常 N O R M A L _ P R I O R I T Y _ C L A S S
高于正常 A B O V E _ N O R M A L _ P R I O R I T Y _ C L A S S
高 H I G H _ P R I O R I T Y _ C L A S S
实时 R E A LT I M E _ P R I O R I T Y _ C L A S S
第 4章 进 程计计63
下载
这些优先级类将会影响进程中包含的线程如何相对于其他进程的线程来进行调度。详细说
明请见第7章。
注意 B E L O W _ N O R M A L _ P R I O R I T Y _ C L A S S和A B O V E _ N O R M A L _ P R I O R I T Y _ C L A S S
这两个优先级类在Windows 2000中是新类, Windows NT 4(或更早的版本)、 Windows 95
或Windows 98均不支持这两个类。
4.2.4 pvEnvironment
p v E n v i r o n m e n t参数用于指向包含新进程将要使用的环境字符串的内存块。在大多数情况
下,为该参数传递 N U L L,使子进程能够继承它的父进程正在使用的一组环境字符串。也可以
使用G e t E n v i r o n m e n t S t r i n g s函数:
该函数用于获得调用进程正在使用的环境字符串数据块的地址。可以使用该函数返回的地
址,作为C r e a t e P r o c e s s的p v E n v i r o n m e n t参数。如果为p v E n v i r o n m e n t参数传递N U L L,那么这正
是C r e a t e P r o c e s s函数所做的操作。当不再需要该内存块时,应该调用 F r e e E n v i r o n m e n t S t r i n g s函
数将内存块释放:
4.2.5 pszCurDir
p s z C u r D i r参数允许父进程设置子进程的当前驱动器和目录。如果本参数是 N U L L,则新进
程的工作目录将与生成新进程的应用程序的目录相同。如果本参数不是 N U L L,那么p s z C u r D i r
必须指向包含需要的工作驱动器和工作目录的以 0结尾的字符串。注意,必须设定路径中的驱
动器名。
4.2.6 psiStartInfo
p s i S t a r t I n f o参数用于指向一个S TA RT U P I N F O结构:
64计计第二部分 编程的具体方法
下载
当Wi n d o w s创建新进程时,它将使用该结构的有关成员。大多数应用程序将要求生成的应
用程序仅仅使用默认值。至少应该将该结构中的所有成员初始化为零,然后将 c b成员设置为该
结构的大小:
如果未能将该结构的内容初始化为零,那么该结构的成员将包含调用线程的堆栈上的任何
无用信息。将该无用信息传递给 C r e a t e P r o c e s s,将意味着有时会创建新进程,有时则不能创建
新进程,完全取决于该无用信息。有一点很重要,那就是将该结构的未用成员设置为零,这样,
C r e a t e P r o c e s s就能连贯一致地运行。不这样做是开发人员最常见的错误。
这时,如果想要对该结构的某些成员进行初始化,只需要在调用 C r e a t e P r o c e s s之前进行这
项操作即可。我们将依次介绍每个成员。有些成员只有在子应用程序创建一个重叠窗口时才有
意义,而另一些成员则只有在子应用程序执行基于 C U I的输入和输出时才有意义。表 4 - 6描述了
每个成员的作用。
表4-6 STA RT U P I N F O结构的成员
成 员 窗口,控制台 作 用
还是两者兼有
c b 两者兼有 包含S TA RT U P I N F O结构中的字节数。如果 M i c r o s o f t将来
扩展该结构,它可用作版本控制手段。应用程序必须将 c b初
始化为s i z e o f ( S TA RT U P I N F O )
l p R e s e r v e d 两者兼有 保留。必须初始化为 N U L L
l p D e s k t o p 两者兼有 用于标识启动应用程序所在的桌面的名字。如果该桌面存
在,新进程便与指定的桌面相关联。如果桌面不存在,便创
建一个带有默认属性的桌面,并使用为新进程指定的名字。
如果l p D e s k t o p是N U L L(这是最常见的情况),那么该进程
将与当前桌面相关联
l p Ti t l e 控制台 用于设定控制台窗口的名称。如果 l p Ti t l e是N U L L,则可
执行文件的名字将用作窗口名
d w X 两者兼有 用于设定应用程序窗口在屏幕上应该放置的位置的 x和y坐
d w Y 标(以像素为单位)。只有当子进程用C W _ U S E D E FA U LT作
为C r e a t e Wi n d o w的x参数来创建它的第一个重叠窗口时,才
使用这两个坐标。若是创建控制台窗口的应用程序,这些成
员用于指明控制台窗口的左上角
d w X S i z e 两者兼有 用于设定应用程序窗口的宽度和长度(以像素为单位)只有
d wYsize 当子进程将 C W _ U S E D E FA U LT用作C r e a t e Wi n d o w的n Wi d t h
参数来创建它的第一个重叠窗口时,才使用这些值。若是创
建控制台窗口的应用程序,这些成员将用于指明控制台窗口
的宽度
d w X C o u n t C h a r s 控制台 用于设定子应用程序的控制台窗口的宽度和高度(以字符
d w Y C o u n t C h a r s 为单位)
d w F i l l A t t r i b u t e 控制台 用于设定子应用程序的控制台窗口使用的文本和背景颜色
d w F l a g s 两者兼有 请参见下一段和表4 - 7的说明
w S h o w Wi n d o w 窗口 用于设定如果子应用程序初次调用的 S h o w Wi n d o w 将
S W _ S H O W D E FA U LT作为n C m d S h o w参数传递时,该应用程
序的第一个重叠窗口应该如何出现。本成员可以是通常用于
Show Wi n d o w函数的任何一个S W _ *标识符
第 4章 进 程计计65
下载
(续)
成 员 窗口,控制台 作 用
还是两者兼有
c b R e s e r v e d 2 两者兼有 保留。必须被初始化为0
l p R e s e r v e d 2 两者兼有 保留。必须被初始化为N U L L
h S t d I n p u t 控制台 用于设定供控制台输入和输出用的缓存的句柄。按照默认
h S t d O u t p u t 设置, h S t d I n p u t用于标识键盘缓存, h S t d O u t p u t和h S t d E r r o r
h S t d E r r o r 用于标识控制台窗口的缓存
现在介绍d w F l a g s的成员。该成员包含一组标志,用于修改如何来创建子进程。大多数标
志只是告诉C r e a t e P r o c e s s, S TA RT U P I N F O结构的其他成员是否包含有用的信息,或者某些成
员是否应该忽略。表4 - 7标出可以使用的标志及其含义。
表4-7 使用标志及含义
标 志 含 义
S TA RT F _ U S E S I Z E 使用d w X S i z e和d w Y S i z e成员
S TA RT F _ U S E S H O W W I N D O W 使用w S h o w Wi n d o w成员
S TA RT F _ U S E P O S I T I O N 使用d w X和d w Y成员
S TA RT F _ U S E C O U N T C H A R S 使用d w X C o u n t C h a r s和dwYCount Chars成员
S TA RT F _ U S E F I L L AT T R I B U T E 使用d w F i l l A t t r i b u t e成员
S TA RT F _ U S E S T D H A N D L E S 使用h S t d I n p u t、 h S t d O u t p u t和h S t d E r r o r成员
S TA RT F _ R U N _ F U L L S C R E E N 强制在x 8 6计算机上运行的控制台应用程序以全屏幕方式启动运行
另外还有两个标志,即 S TA RT F _ F O R C E O N F E E D B A C K和S TA RT F _+F O R C E O F F F -
E E D B A C K,当启动一个新进程时,它们可以用来控制鼠标的光标。由于 Wi n d o w s支持真正的
多任务抢占式运行方式,因此可以启动一个应用程序,然后在进程初始化时,使用另一个程序。
为了向用户提供直观的反馈信息, C r e a t e P r o c e s s能够临时将系统的箭头光标改为一个新光标,
即沙漏箭头光标:
该光标表示可以等待出现某种情况,也可以继续使用系统。当启动另一个进程时,
C r e a t e P r o c e s s函数使你能够更好地控制光标。当设定 S TA RT F _ F O R C E O F F F E E D B A C K标志时,
C r e a t e P r o c e s s并不将光标改为沙漏。
S TA RT F _ F O R C E O N F E E D B A C K可使C r e a t e P r o c e s s能够监控新进程的初始化,并可根据结
果来改变光标。当使用该标志来调用 C r e a t e P r o c e s s时,光标改为沙漏。过 2 s后,如果新进程没
有调用G U I, CreateProcess 将光标恢复为箭头。
如果该进程在2 s内调用了 G U I, C r e a t e P r o c e s s将等待该应用程序显示一个窗口。这必须在
进程调用G U I后5 s内发生。如果没有显示窗口, C r e a t e P r o c e s s就会恢复原来的光标。如果显示
了一个窗口, C r e a t e P r o c e s s将使沙漏光标继续保留 5 s。如果某个时候该应用程序调用了
G e t M e s s a g e函数,指明它完成了初始化,那么 C r e a t e P r o c e s s就会立即恢复原来的光标,并且停
止监控新进程。
在结束这一节内容的介绍之前,我想讲一讲S TA RT U P I N F O的w S h o w Wi n d o w成员。你将该成
员初始化为传递给( w ) Wi n M a i n的最后一个参数n C m d S h o w的值。该成员显示你想要传递给新进程
的( w ) Wi n M a i n函数的最后一个参数n C m d S h o w的值。它是可以传递给S h o w Wi n d o w函数的标识符之
一。通常, n C m d S h o w的值既可以是S W _ S H O W N O R M A L,也可以是SW_ SHOWMINNOACTIVE。
66计计第二部分 编程的具体方法
下载
但是,它有时可以是S W _ S H O W D E FA U LT。
当在E x p l o r e r中启动一个应用程序时,该
应用程序的 ( w ) Wi n M a i n 函数被调用,而
S W _ S H O W N O R M A L则作为n C m d S h o w参数来
传递。如果为该应用程序创建了一个快捷方式,
可以使用快捷方式的属性页来告诉系统,应用
程序的窗口最初应该如何显示。图 4 - 3显示了运
行N o t e p a d的快捷方式的属性页。注意,使用
R u n 选项的组合框,就能够设定如何显示
N o t e p a d的窗口。
当使用 E x p l o r e r 来启动该快捷方式时,
E x p l o r e r会正确地准备S TA RT U P I N F O结构并调
用C r e a t e P r o c e s s。这时N o t e p a d开始运行,并且
为n C m d S h o w参数将S W _ S H O W M I N N O A C T I V E
传递给它的( w ) Wi n M a i n函数。
运用这样的方法,用户能够很容易地启动
一个应用程序,其主窗口可以用正常状态、最
小或最大状态进行显示。
最后,应用程序可以调用下面的函数,以便获取由父进程初始化的 S TA RT U P I N F O结构的
拷贝。子进程可以查看该结构,并根据该结构的成员的值来改变它的行为特性。
注意 虽然Wi n d o w s文档没有明确地说明,但是在调用 G e t S t a r t I n f o函数之前,必须像
下面这样对该结构的c b成员进行初始化:
4.2.7 ppiProcInfo
p p i P r o c I n f o参数用于指向你必须指定的 P R O C E S S _ I N F O R M AT I O N结构。 C r e a t e P r o c e s s在
返回之前要对该结构的成员进行初始化。该结构的形式如下面所示:
如前所述,创建新进程可使系统建立一个进程内核对象和一个线程内核对象。在创建进程
的时候,系统为每个对象赋予一个初始使用计数值 1。然后,在c r e a t e P r o c e s s返回之前,该函数
打开进程对象和线程对象,并将每个对象的与进程相关的句柄放入 P R O C E S S _ I N F O R M AT I O N
结构的h P r o c e s s和h T h r e a d成员中。当C r e a t e P r o c e s s在内部打开这些对象时,每个对象的使用计
数就变为2。
第 4章 进 程计计67
下载
图4-3 运行N o t e p a d的快捷方式的属性页
这意味着在系统能够释放进程对象前,该进程必须终止运行(将使用计数递减为 1),并且
父进程必须调用 C l o s e H a n d l e(再将使用计数递减 1,使之变为0)。同样,若要释放线程对象,
该线程必须终止运行,父进程必须关闭线程对象的句柄(关于释放线程对象的详细说明,请参
见本章后面“子进程”一节的内容)。
注意 必须关闭子进程和它的主线程的句柄,以避免在应用程序运行时泄漏资源。当
然,当进程终止运行时,系统会自动消除这些泄漏现象,但是,当进程不再需要访问
子进程和它的线程时,编写得较好的软件能够显式关闭这些句柄(通过调用
C l o s e H a n d l e函数来关闭)。不能关闭这些句柄是开发人员最常犯的错误之一。
由于某些原因,许多开发人员认为,关闭进程或线程的句柄,会促使系统撤消该
进程或线程。实际情况并非如此。关闭句柄只是告诉系统,你对进程或线程的统计数
据不感兴趣。进程或线程将继续运行,直到它自己终止运行。
当进程内核对象创建后,系统赋予该对象一个独一无二的标识号,系统中的其他任何进程
内核对象都不能使用这个相同的 I D号。线程内核对象的情况也一样。当一个线程内核对象创建
时,该对象被赋予一个独一无二的、系统范围的 I D号。进程 I D和线程 I D共享相同的号码池。
这意味着进程和线程不可能拥有相同的 I D 。另外,对象决不会被赋予 0 作为其 I D 。在
C r e a t e P r o c e s s返回之前,它要用这些 I D填入P R O C E S S _ I N F O R M AT I O N结构的d w P r o c e s s I d和
d w T h r e a d I d成员中。 I D使你能够非常容易地识别系统中的进程和线程。一些实用工具(如 Ta s k
M a n a g e r)对I D使用得最多,而高效率的应用程序则使用得很少。由于这个原因,大多数应用
程序完全忽略I D。
如果应用程序使用 I D来跟踪进程和线程,必须懂得系统会立即复用进程 I D和线程I D。例
如,当一个进程被创建时,系统为它指定一个进程对象,并为它赋予 I D值1 2 2。如果创建了一
个新进程对象,系统不会将相同的 I D赋予给它。但是,如果第一个进程对象被释放,系统就可
以将1 2 2赋予创建的下一个进程对象。记住这一点后,就能避免编写引用不正确的进程对象或
线程对象的代码。获取进程 I D是很容易的,保存该 I D也不难,但是,接下来你应该知道,该
I D标识的进程已被释放,新进程被创建并被赋予相同的 I D。当使用已经保存的进程 I D时,最
终操作的是新进程,而不是原先获得 I D的进程。
有时,运行的应用程序想要确定它的父进程。首先应该知道只有在生成子进程时,才存在
进程之间的父子关系。在子进程开始执行代码前, Wi n d o w s不再考虑存在什么父子关系。较早
的 W i n d o w s 版本没有提供让进程查询其父进程的函数。现在, To o l H e l p 函数通过
P R O C E S S E N T RY 3 2结构使得这种查询成为可能。在这个结构中有一个 t h 3 2 P a r e n t P r o c e s s I D成
员,根据文档的说明,它能返回进程的父进程的 I D。
系统无法记住每个进程的父进程的 I D,但是,由于I D是被立即重复使用的,因此,等到获
得父进程的I D时,该I D可能标识了系统中一个完全不同的进程。父进程可能已经终止运行。如
果应用程序想要与它的“创建者”进行通信,最好不要使用 I D。应该定义一个持久性更好的机
制,比如内核对象和窗口句柄等。
若要确保进程 I D或线程I D不被重复使用,唯一的方法是保证进程或线程的内核对象不会
被撤消。如果刚刚创建了一个新进程或线程,只要不关闭这些对象的句柄,就能够保证进程
对象不被撤消。一旦应用程序结束使用该 I D,那么调用C l o s e H a n d l e就可以释放内核对象,要
记住,这时使用或依赖进程 I D,对来说将不再安全。如果使用的是子进程,将无法保证父进
程或父线程的有效性,除非父进程复制了它自己的进程对象或线程对象的句柄,并让子进程
继承这些句柄。
4.3 终止进程的运行
若要终止进程的运行,可以使用下面四种方法:
• 主线程的进入点函数返回(最好使用这个方法)。
• 进程中的一个线程调用E x i t P r o c e s s函数(应该避免使用这种方法)。
• 另一个进程中的线程调用Te r m i n a t e P r o c e s s函数(应该避免使用这种方法)。
• 进程中的所有线程自行终止运行(这种情况几乎从未发生)。
这一节将介绍所有这四种方法,并且说明进程结束时将会发生什么情况。
4.3.1 主线程的进入点函数返回
始终都应该这样来设计应用程序,即只有当主线程的进入点函数返回时,它的进程才终止
运行。这是保证所有线程资源能够得到正确清除的唯一办法。
让主线程的进入点函数返回,可以确保下列操作的实现:
• 该线程创建的任何C + +对象将能使用它们的析构函数正确地撤消。
• 操作系统将能正确地释放该线程的堆栈使用的内存。
• 系统将进程的退出代码(在进程的内核对象中维护)设置为进入点函数的返回值。
• 系统将进程内核对象的返回值递减 1。
4.3.2 ExitProcess函数
当进程中的一个线程调用E x i t P r o c e s s函数时,进程便终止运行:
该函数用于终止进程的运行,并将进程的退出代码设置为 f u E x i t C o d e。 E x i t P r o c e s s函数并
不返回任何值,因为进程已经终止运行。如果在调用 E x i t P r o c e s s之后又增加了什么代码,那么
该代码将永远不会运行。
当主线程的进入点函数(Wi n M a i n、 w Wi n M a i n、 m a i n或w m a i n)返回时,它将返回给
C / C + +运行期启动代码,它能正确地清除该进程使用的所有的 C运行期资源。当C运行期资源被
释放之后, C运行期启动代码就显式调用 E x i t P r o c e s s,并将进入点函数返回的值传递给它。这
解释了为什么只需要主线程的进入点函数返回,就能够终止整个进程的运行。请注意,进程中
运行的任何其他线程都随着进程而一道终止运行。
Windows Platform SDK文档声明,进程要等到所有线程终止运行之后才终止运行。就操作
系统而言,这种说法是对的。但是, C / C + +运行期对应用程序采用了不同的规则,通过调用
E x i t P r o c e s s,使得C / C + +运行期启动代码能够确保主线程从它的进入点函数返回时,进程便终
止运行,而不管进程中是否还有其他线程在运行。不过,如果在进入点函数中调用 E x i t T h r e a d,
而不是调用E x t i P r o c e s s或者仅仅是返回,那么应用程序的主线程将停止运行,但是,如果进程
中至少有一个线程还在运行,该进程将不会终止运行。
注意,调用E x i t P r o c e s s或E x i t T h r e a d可使进程或线程在函数中就终止运行。就操作系统而
言,这很好,进程或线程的所有操作系统资源都将被全部清除。但是, C / C + +应用程序应该避
免调用这些函数,因为C / C + +运行期也许无法正确地清除。请看下面的代码:
当上面的代码运行时,将会看到:
它创建了两个对象,一个是全局对象,另一个是局部对象。不过决不会看到 D e s t r u c t o r这
个单词出现, C + +对象没有被正确地撤消,因为 E x i t P r o c e s s函数强制进程在现场终止运行,
C / C + +运行期没有机会进行清除。
如前所述,决不应该显式调用 E x i t P r o c e s s函数。如果在上面的代码中删除了对 E x i t P r o c e s s
的调用,那么运行该程序产生的结果如下:
只要让主线程的进入点函数返回, C / C + +运行期就能够执行它的清除操作,并且正确地撤
消任何或所有的 C + +对象。顺便讲一下,这个说明不仅仅适用于 C + +对象。 C + +运行期能够代
表进程执行许多操作,最好允许运行期正确地将它清除。
注意 显式调用E x i t P r o c e s s和E x i t T h r e a d是导致应用程序不能正确地将自己清除的常见
原因。在调用E x i t T h r e a d时,进程将继续运行,但是可能会泄漏内存或其他资源。
4.3.3 Te r m i n a t e P r o c e s s函数
调用Te r m i n a t e P r o c e s s函数也能够终止进程的运行:
该函数与E x i t P r o c e s s有一个很大的差别,那就是任何线程都可以调用 Te r m i n a t e P r o c e s s来终
止另一个进程或它自己的进程的运行。 h P r o c e s s参数用于标识要终止运行的进程的句柄。当进
程终止运行时,它的退出代码将成为你作为 f u E x i t C o d e参数来传递的值。
只有当无法用另一种方法来迫使进程退出时,才应该使用 Te r m i n a t e P r o c e s s。终止运行的
进程绝对得不到关于它将终止运行的任何通知,因为应用程序无法正确地清除,并且不能避免
自己被撤消(除非通过正常的安全机制)。例如,进程无法将内存中它拥有的任何信息迅速送
往磁盘。
70计计第二部分 编程的具体方法
下载
虽然进程确实没有机会执行自己的清除操作,但是操作系统可以在进程之后进行全面的清
除,使得所有操作系统资源都不会保留下来。这意味着进程使用的所有内存均被释放,所有打
开的文件全部关闭,所有内核对象的使用计数均被递减,同时所有的用户对象和 G D I对象均被
撤消。
一旦进程终止运行(无论采用何种方法),系统将确保该进程不会将它的任何部分遗留下
来。绝对没有办法知道该进程是否曾经运行过。进程一旦终止运行,它绝对不会留下任何蛛丝
马迹。希望这是很清楚的。
注意 Te r m i n a t e P r o c e s s函数是个异步运行的函数,也就是说,它会告诉系统,你想要
进程终止运行,但是当函数返回时,你无法保证该进程已经终止运行。因此,如果想
要确切地了解进程是否已经终止运行,必须调用Wa i t F o r S i n g l e O b j e c t函数(第9章介绍)
或者类似的函数,并传递进程的句柄。
进程中的线程何时全部终止运行
如果进程中的所有线程全部终止运行(因为它们调用了 E x i t T h r e a d函数,或者因为它们已
经用Te r m i n a t e P r o c e s s函数终止运行),操作系统就认为没有理由继续保留进程的地址空间。这
很好,因为在地址空间中没有任何线程执行任何代码。当系统发现没有任何线程仍在运行时,
它就终止进程的运行。出现这种情况时,进程的退出代码被设置为与终止运行的最后一个线程
相同的退出代码。
4.3.4 进程终止运行时出现的情况
当进程终止运行时,下列操作将启动运行:
1) 进程中剩余的所有线程全部终止运行。
2) 进程指定的所有用户对象和G D I对象均被释放,所有内核对象均被关闭(如果没有其他
进程打开它们的句柄,那么这些内核对象将被撤消。但是,如果其他进程打开了它们的句柄,
内核对象将不会撤消)。
3) 进程的退出代码将从S T I L L _ A C T I V E改为传递给E x i t P r o c e s s或Te r m i n a t e P r o c e s s的代码。
4) 进程内核对象的状态变成收到通知的状态(关于传送通知的详细说明,参见第 9章)。系
统中的其他线程可以挂起,直到进程终止运行。
5) 进程内核对象的使用计数递减1。
注意,进程的内核对象的寿命至少可以达到进程本身那么长,但是进程内核对象的寿命可
能大大超过它的进程寿命。当进程终止运行时,系统能够自动确定它的内核对象的使用计数。
如果使用计数降为 0,那么没有其他进程拥有该对象打开的句柄,当进程被撤消时,对象也被
撤消。
不过,如果系统中的另一个进程拥有正在被撤消的进程的内核对象的打开句柄,那么该进
程内核对象的使用计数不会降为 0。当父进程忘记关闭子进程的句柄时,往往就会发生这样的
情况。这是个特性,而不是错误。记住,进程内核对象维护关于进程的统计信息。即使进程已
经终止运行,该信息也是有用的。例如,你可能想要知道进程需要多少 C P U时间,或者,你想
通过调用G e t E x i t C o d e P r o c e s s来获得目前已经撤消的进程的退出代码:
该函数查看进程的内核对象(由 h P r o c e s s参数来标识),取出内核对象的数据结构中用于标
识进程的退出代码的成员。该退出代码的值在 p d w E x i t C o d e参数指向的D W O R D中返回。
可以随时调用该函数。如果调用 G e t E x i t C o d e P r o c e s s函数时进程尚未终止运行,那么该函
数就用S T I L L _ A C T I V E标识符(定义为 0 x 1 0 3)填入D W O R D。如果进程已经终止运行,便返
回数据的退出代码值。
也许你会认为,你可以编写代码,通过定期调用 G e t E x i t C o d e P r o c e s s函数并且检查退出代
码来确定进程是否已经终止运行。大多数情况下,这是可行的,但是效率不高。下一段将介绍
用什么正确的方法来确定进程何时终止运行。
再一次提醒你,应该通过调用 C l o s e H a n d l e函数,告诉系统你对进程的统计数据已经不再
感兴趣。如果进程已经终止运行, C l o s e H a n d l e将递减内核对象的使用计数,并将它释放。
4.4 子进程
当你设计应用程序时,可能会遇到这样的情况,即想要另一个代码块来执行操作。通过调
用函数或子例程,你可以一直象这样分配工作。当调用一个函数时,在函数返回之前,代码将
无法继续进行操作。大多数情况下,需要实施这种单任务同步。让另一个代码块来执行操作的
另一种方法是在进程中创建一个新线程,并让它帮助进行操作。这样,当其他线程在执行需要
的操作时,代码就能继续进行它的处理。这种方法很有用,不过,当线程需要查看新线程的结
果时,它会产生同步问题。
另一个解决办法是生成一个新进程,即子进程,以便帮助你进行操作。比如说,需要进行
的操作非常复杂。若要处理该操作,只需要在同一个进程中创建一个新线程。你编写一些代码,
对它进行测试,但是得到一些不正确的结果。也许你的算法存在错误,也可能间接引用的对象
不正确,并且不小心改写了地址空间中的某些重要内容。进行操作处理时,如果要保护地址空
间,方法之一是让一个新进程来执行这项操作。然后,在继续进行工作之前,可以等待新进程
终止运行,或者可以在新进程工作时,继续进行工作。
不过,新进程可能需要对地址空间中包含的数据进行操作。这时最好让进程在它自己的地
址空间中运行,并且只让它访问父进程地址空间中的相关数据,这样就能保护与手头正在执行
的任务无关的全部数据。 Wi n d o w s提供了若干种方法,以便在不同的进程中间传送数据,比如
动态数据交换(D D E)、 O L E、管道和邮箱等。共享数据最方便的方法之一是,使用内存映射
文件(关于内存映射文件的详细说明请参见第 1 7章)。
如果想创建新进程,让它进行一些操作,并且等待结果,可以使用类似下面的代码:
72计计第二部分 编程的具体方法
下载
在上面的代码段中,你创建了一个新进程,如果创建成功,可以调用 Wa i t F o r S i n g l e O b j e c t
函数:
第9章将全面介绍 Wa i t F o r S i n g l e O b j e c t函数。现在,必须知道的情况是,它会一直等到
h O b j e c t参数标识的对象得到通知的时候。当进程对象终止运行时,它们才会得到通知。因此
对 Wa i t F o r S i n g l e O b j e c t 的调用会将父进程的线程挂起,直到子进程终止运行。当
Wa i t F o r S i n g l e O b j e c t返回时,通过调用 G e t E x i t C o d e P r o c e s s函数,就可以获得子进程的退出代
码。
在上面的代码段中调用C l o s e H a n d l e函数,可使系统为线程和进程对象的使用计数递减为 0,
从而使对象的内存得以释放。
你会发现,在这个代码段中,在 C r e a t e P r o c e s s返回后,立即关闭了子进程的主线程内核对
象的句柄。这并不会导致子进程的主线程终止运行,它只是递减子进程的主线程对象的使用计
数。这种做法的优点是,假设子进程的主线程生成了另一个线程,然后主线程终止运行,这时,
如果父进程不拥有子进程的主线程对象的句柄,那么系统就可以从内存中释放子进程的主线程
对象。但是,如果父进程拥有子进程的线程对象的句柄,那么在父进程关闭句柄前,系统将不
能释放该对象。
运行独立的子进程
大多数情况下,应用程序将另一个进程作为独立的进程来启动。这意味着进程创建和开始
运行后,父进程并不需要与新进程进行通信,也不需要在完成它的工作后父进程才能继续运行。
这就是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 l o s e H a n d l e来关闭它与新进程及它
的主线程之间的句柄。下面的代码示例显示了如何创建新进程以及如何让它以独立方式来运
行:
4.5 枚举系统中运行的进程
许多软件开发人员都试图为 Wi n d o w s编写需要枚举正在运行的一组进程的工具或实用程
序。 Windows API原先没有用于枚举正在运行的进程的函数。不过, Windows NT一直在不断
第 4章 进 程计计73
下载
更新称为Performance Data的数据库。该数据库包含大量的信息,并且可以通过注册表函数来
访问(比如以H K E Y _ P E R F O R M A N C E _ D ATA为根关键字的 R e g Q u e r y Va l u e E x函数)。由于下列
原因,很少有Wi n d o w s程序员知道性能数据库的情况:
• 它没有自己特定的函数,它只是使用现有的注册表函数。
• Windows 95和Windows 98没有配备该数据库。
• 该数据库中的信息布局比较复杂,许多软件开发人员都不愿使用它。这妨碍了人们通过
言传口说来传播它的存在。
为了使该数据库的使用变得更加容易, M i c r o s o f t开发了一组Performance Data Helper函数
(包含在P D H . d l l文件中)。若要了解它的详细信息,请查看 Platform SDK文档中的P e r f o r m a n c e
Data Helper的内容。
如前所述, Windows 95和Windows 98没有配备该数据库。不过它们有自己的一组函数,
可以用于枚举关于它们的进程和信息。这些函数均在 ToolHelp API 中。详细信息请参见
Platform SDK文档中的P r o c e s s 3 2 F i r s t和P r o c e s s 3 2 N e x t函数。
更加有趣的是, M i c r o s o f t的Windows NT开发小组因为不喜欢 To o l H e l p函数,所以没有将
这些函数添加给 Windows NT。相反,他们开发了自己的 Process Status函数,用于枚举进程
(这些函数包含在P S A P I . d l l文件中)。关于这些函数的详细说明,请参见 Platform SDK文档中的
E n u m P r o c e s s e s函数。
M i c r o s o f t似乎使得工具和实用程序开发人员的日子很不好过,不过我高兴地告诉他们,
M i c r o s o f t已经将To o l H e l p函数添加给Windows 2000。最后,开发人员终于有了一种方法,可以
为Windows 95 、 Windows 98和Windows 2000编写具有公用源代码的工具和实用程序。
进程信息示例应用程序
P r o c e s s I n f o应用程序“04 ProcessInfo.exe”(本章结尾处的清单 4 - 2列出了该文件)显示了
如何使用To o l H e l p函数来开发非常有用的实用程序。用于应用程序的源代码和资源文件均放在
本书所附光盘上0 4 - P r o c e s s I n f o目录中。当启动该程序时,便会出现图 4 - 4所示的窗口。
P r o c e s s I n f o首先枚举目前正在运行的一组进程,并在顶部的组合框中列出每个进程的名字
和I D。然后,第一个进程被选定,并在较大的编辑控件中显示关于该进程的信息。可以看到,
与该进程的I D一道显示的还有它的父进程的I D,进程的优先级类,以及该进程环境中当前正在
运行的线程数目。这些信息中的大多数不在本章介绍的范围之内,将在本章后面的内容中加以
说明。
当查看这个进程列表时,可以使用 V M M a p菜单项(当查看模块信息时,该菜单项禁用)。
如果选定V M M a p菜单项,可使V M M a p示例应用程序(参见第1 4章)启动运行。该应用程序将
在指定进程的地址空间中运行。
模块信息部分显示了映射到进程的地址空间中的模块的列表(可执行文件和 D L L文件)。
固定模块是指进程初始化时隐含加载的模块。如果是显式加载的 D L L模块,则显示 D L L的使
用计数。第二个域显示映射模块的地址。如果模块不是在它的首选基地址上映射的,那么首
选基地址显示在括号中。第三个域显示模块的大小(用字节数表示)。最后显示的是模块的全
路径名。线程信息部分显示了该进程中当前运行的一组线程。每个线程 I D和优先级均被显示。
除了进程信息外,可以选择 M o d u l e s !菜单项。这将使得P r o c e s s I n f o能够枚举当前通过系统
加载的模块,并将每个模块的名字放入顶部的组合框。然后 P r o c e s s I n f o可以选定第一个模块,
并显示关于它的信息,如图4 - 5所示。
74计计第二部分 编程的具体方法
下载
图4-4 运行中的P r o c e s s I n f o
图4-5 ProcessInfo显示U s e r 3 2 . d l l加载到它们的地址空间的所有进程
当以这种方法使用 P r o c e s s I n f o实用程序时,能够方便地确定哪些进程正在使用某个模块。
如你所见,模块的全路径名显示在顶部。然后,进程信息部分显示包含该模块的进程列表。除
了每个进程的I D和名字外,还显示每个进程中模块加载到的地址。
P r o c e s s I n f o应用程序显示的所有信息基本上都是通过调用 To o l H e l p的各个函数而产生的。
为了使To o l H e l p函数的使用更加容易,我创建了一个 C + +类(包含在To o l H e l p . h文件中)。这个
C + +类封装了一个To o l H e l p快照,使得调用其他To o l H e l p函数稍稍容易一些。
P r o c e s s I n f o . c p p中的G e t M o d u l e P r e f e r r e d B a s e A d d r函数是个特别有意思的函数:
该函数接受一个进程I D和该进程中的一个模块的地址。然后它查看该进程的地址空间,找
出该模块,并读取模块的标题信息,以确定该模块首选的基地址,一个模块始终应该加载到它
的首选基地址中,否则,使用该模块的应用程序将需要更多的内存,并且在初始化时会对性能
产生影响。由于这是个非常可怕的情况,因此我增加了这个函数并且显示何时模块没有加载到
它的首选基地址中。第2 0章将要进一步介绍首选基地址和这次/内存性能