第4章 进 程
• 一个是操作系统用来管理进程的内核对象。内核对象也是系统用来存放关于进程的统计
信息的地方。
• 另一个是地址空间,它包含所有可执行模块或 D L L模块的代码和数据。它还包含动态内存分配的空间。如线程堆栈和堆分配空间。
进程是不活泼的。若要使进程完成某项操作,它必须拥有一个在它的环境中运行的线程,该线程负责执行包含在进程的地址空间中的代码。实际上,单个进程可能包含若干个线程,所有这些线程都“同时”执行进程地址空间中的代码。为此,每个线程都有它自己的一组C P U寄存器和它自己的堆栈。每个进程至少拥有一个线程,来执行进程的地址空间中的代码。如果没有线程来执行进程的地址空间中的代码,那么进程就没有存在的理由了,系统就将自动撤消该进程和它的地址空间。若要使所有这些线程都能运行,操作系统就要为每个线程安排一定的 C P U时间。它通过以一种循环方式为线程提供时间片(称为量程) ,造成一种假象,仿佛所有线程都是
同时运行的一样。图4 - 1显示了在单个C P U的计算机上是如何实现这种运行方式的。如果计算机拥有多个 C P U,那么操作系统就要使用复杂得多的算法来实现 C P U上线程负载的平衡。
建其他的线程,而这些线程又能创建更多的线程。
Windows 98 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个:
同样,如果设定了/ 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函数改为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开关。这是我喜欢选择的方法,因为它提供了最大的灵活性。现在,连接程序将根据源代码中实现的函数进行正确的操作。
• 检索指向新进程的环境变量的指针。
• 对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函数,它将以下面的形式被调用:
• 调用由_ 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来传递。对于加载资源的函数调用来说,
通常都需要该句柄的值。例如,若要从可执行文件的映象来加载图标资源,需要调用下面这个
函数:
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函数:
如果你想在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或者大于这个地址。
下面的G e t M o d u l e H a n d l e函数返回可执行文件或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函数: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函数:
许多应用程序常常拥有转换成它的各个标记的命令行。使用全局性 _ _ 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字符串分割成它的各个标记:
4.1.4 进程的环境变量
每个进程都有一个与它相关的环境块。环境块是进程的地址空间中分配的一个内存块。每个环境块都包含一组字符串,其形式如下:
Windows 2000 当用户登录到Windows 2000中时,系统创建一个外壳进程并将一组环境字符串与它相关联。通过查看注册表中的两个关键字,系统可以获得一组初始环境字符串。
第一个关键字包含一个适用于系统的所有环境变量的列表:
应用程序也可以使用各种注册表函数来修改这些注册表项目。但是,若要使这些
修改在所有应用程序中生效,用户必须退出系统,然后再次登录。有些应用程序,如
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消息时,用新注册表项目来更新它们的环境块。例如,如果要更新
注册表项目,并且想让有关的应用程序更新它们的环境块,可以调用下面的代码:
SendMessage(HWND_BROADCAST ,WM_SETTINGCHANGE ,0 ,(LPARAM)TEXT(“Environment”));
通常,子进程可以继承一组与父进程相同的环境变量。但是,父进程能够控制子进程继承什么环境变量,后面介绍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 tVa r i a b l e函数,就能够确定某个环境变量是否存在以及它的值:
DWORD GetEnvironmentVariableW(
_In_opt_ LPCWSTR lpName,
_Out_writes_to_opt_(nSize, return + 1) LPWSTR lpBuffer,
_In_ DWORD nSize
);
TCHAR tcEnviromentVar[MAX_PATH] = {0};
GetEnvironmentVariable(_TEXT("TEMP") ,tcEnviromentVar ,MAX_PATH);
当调用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%My Documents
百分数符号之间的部分表示一个可取代的字符串。在这个例子中,环境变量的值
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函数:
DWORD ExpandEnvironmentStringsW(
_In_ LPCWSTR lpSrc,
_Out_writes_to_opt_(nSize, return) LPWSTR lpDst,
_In_ DWORD nSize
);
TCHAR tcFullEnviromentVar[MAX_PATH] = {0};
ExpandEnvironmentStrings(_TEXT("%TEMP%\A") ,tcFullEnviromentVar ,MAX_PATH);
当调用该函数时,p s z S r c参数是包含可替换的环境变量字符串的这个字符串的地址。p s z D s t参数是接收已展开字符串的缓存的地址,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函数来添加变量、删除变量或者修改变量的值:
BOOL SetEnvironmentVariableW(
_In_ LPCWSTR lpName,
_In_opt_ LPCWSTR lpValue
);
SetEnvironmentVariable(_TEXT("TTT") ,_TEXT("C:"));
该函数用于将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 tVa 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函数:
UINT SetErrorMode(UINT fuErrorMode);
f u E r r o r M o d e参数是下表的任何标志按位用O R连接在一起的组合。
默认情况下,子进程继承父进程的错误模式标志。换句话说,如果一个进程的
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的各个函数就会在当前驱动器的当前目录中查找文件和目录。例如,如果进程中的一个线程调用 C r e a t e F i l e来打开一个文件(不设定全路径名) ,那么系统就会在当前驱动器和目录中查找该文件。
系统将在内部保持对进程的当前驱动器和目录的跟踪。 由于该信息是按每个进程来维护的,因此改变当前驱动器或目录的进程中的线程,就可以为该进程中的所有线程改变这些信息。
通过调用下面两个函数,线程能够获得和设置它的进程的当前驱动器和目录:
DWORD GetCurrentDirectoryW(
_In_ DWORD nBufferLength,
_Out_writes_to_opt_(nBufferLength, return + 1) LPWSTR lpBuffer
);
TCHAR tcLocalAppPath[MAX_PATH] = {0};
GetCurrentDirectory(MAX_PATH ,tcLocalAppPath);
BOOL SetCurrentDirectoryW(
_In_ LPCWSTR lpPathName
);
SetCurrentDirectory(_TEXT("G:\inetpub"));
4.1.8 进程的当前目录
系统将对进程的当前驱动器和目录保持跟踪,但是它不跟踪每个驱动器的当前目录。不过,有些操作系统支持对多个驱动器的当前目录的处理。这种支持是通过进程的环境字符串来提供的。例如,进程能够拥有下面所示的两个环境变量:
=C:=C:UtilityBin
=D:=D:Program FIles
这些变量表示驱动器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,父进程可以获得它的当前目录:
TCHAR szCurDir[MAX_PATH] = {0};
GetFullPathName(_TEXT("C:") ,MAX_PATH ,szCurDir ,NULL);
记住,进程的环境变量必须始终按字母顺序来排序。因此驱动器名的环境变量通常必须置于环境块的开始处。
4.1.9 系统版本
应用程序常常需要确定用户运行的是哪个 Wi n d o w s版本。例如,通过调用安全性函数,应用程序就能利用它的安全特性。但是这些函数只有在Windows 2000上才能得到全面的实现。Windows API拥有下面的G e t Ve r s i o n函数:
DWORD GetVersion();
该函数已经有相当长的历史了。最初它是为 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 :
OSVERSIONINFO osvi;
ZeroMemory(&osvi ,sizeof(OSVERSIONINFO));
osvi.dwOSVersionInfoSize = sizeof(OSVERSIONINFOEX);
GetVersionExW(&osvi);
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结构,它没有服务程序包、程序组屏蔽、产品类型和保留成员。
注意,对于系统的版本号中的每个成分来说,该结构拥有不同的成员。这样做的目的是,
程序员不必提取低位字、高位字、低位字节和高位字节,因此应用程序能够更加容易地对它们期望的版本号与主机系统的版本号进行比较。下表描述了O S V E R S I O N I N F O E X结构的成员。
为了使操作更加容易,Windows 2000提供了一个新的函数,即Ve r i f y Ve r s i o n I n f o,用于对主机系统的版本与你的应用程序需要的版本进行比较:
BOOL VerifyVersionInfoW(
_Inout_ LPOSVERSIONINFOEXW lpVersionInformation,
_In_ DWORD dwTypeMask,
_In_ DWORDLONG dwlConditionMask
);
若要使用该函数,必须指定一个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宏:
VER_SET_CONDITION(
DWORD dwlConditionMask,
ULONG dwTypeBitMask,
ULONG dwConditionMask)
第一个参数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信息时,可以使用这些值。例如,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的一个例子: