【Windows via C/C++】第4章 进程 (1)


Part II: Getting Work Done
Chapter 4: Processes


本章讨论系统如何管理所有正在运行的应用程序。

  • 首先,解释什么是进程,以及系统如何创建进程内核对象来管理每个进程。
  • 然后,展示如何使用与进程关联的内核对象来操纵进程。
  • 接下来,讨论进程的各种属性,以及用于查询和更改这些属性的若干个函数。
  • 研究在系统中创建或生成其他进程的函数。
  • 深入了解过程如何终止。

进程通常被定义为一个正在运行的程序的实例,它由两个部分组成:

  • 操作系统用来管理进程的内核对象。内核对象也是系统存放关于进程的统计信息的地方。
  • 包含所有可执行文件或动态链接库 (DLL) 模块的代码和数据的地址空间。它还包含动态内存分配空间,比如线程栈和堆分配空间。

要使一个进程完成某项操作,它必须拥有一个运行在其环境下的线程;该线程负责执行进程地址空间中包含的代码。
实际上,一个进程可能包含多个线程,它们全部在该进程的地址空间中“同时”执行代码。为此,每个线程都有自己的一组CPU寄存器和自己的堆栈。每个进程至少有一个线程在该进程的地址空间中执行代码。
当创建进程后,系统会自动创建其第一个线程,称为主线程 (primary thread)。然后,该线程可以创建其他线程,而这些线程又可以创建更多线程。
如果在进程的地址空间中没有线程执行代码,那么该进程没有理由继续存在,系统将自动销毁该进程及其地址空间。

对于所有要运行的线程,操作系统为每个线程安排一段 CPU 时间。它以循环方式为线程提供时间片,从而产生了所有线程同时运行的错觉。时间片也称为量子 (quantum)。

如果计算机有多个 CPU,那么用于在 CPU 上负载均衡线程的操作系统算法要复杂得多。Microsoft Windows 可以在每个 CPU 上同时调度不同的线程,这样多个线程可以真正地同时运行。
Windows 内核处理这类系统上的所有线程管理和调度。无需在代码中做任何特殊的事情,即可获得多处理器计算机所提供的优势。但是,也可以在应用程序的算法中做一些事情,以更好地利用这些 CPU。


4.1 编写 Windows 应用程序

编写Windows应用程序

Windows 支持两种类型的应用程序:基于图形用户界面 (GUI: Graphical User Interface) 的应用程序,基于控制台用户界面 (CUI: Console User Interface) 的应用程序。

  • 基于 GUI 的应用程序具有图形前端。它可以创建窗口,拥有菜单,通过对话框与用户交互以及使用所有标准的“Windows”组件。Windows 配备的几乎所有附件应用程序 (例如记事本、计算器和写字板) 都是基于 GUI 的应用程序。
  • 基于控制台的应用程序是基于文本的。它们通常不创建窗口或处理消息,并且不需要图形用户界面。 尽管基于 CUI 的应用程序包含在屏幕上的窗口中,但该窗口仅包含文本。命令提示符 CMD.EXE 是基于CUI 的应用程序的典型示例。

这两种类型的应用程序之间的界线非常模糊。

  • 可以创建显示对话框的基于 CUI 的应用程序。例如,命令 shell 可能有一个特殊的命令,使它显示一个图形对话框,你可以在此对话框中选择要执行的命令,而不必记住 shell 支持的各种命令。
  • 还可以创建一个基于 GUI 的应用程序,它将文本字符串输出到控制台窗口。例如,创建一个基于 GUI 的应用程序,它会创建一个控制台窗口,在应用程序执行时,可以在这个控制台窗口上查看调试信息。

当使用 Microsoft Visual Studio 创建一个应用程序项目时,集成环境设置各种链接程序开关,以便链接程序将适当类型的子系统嵌入到生成的可执行文件中。
对于 CUI 应用程序,这个链接程序开关是 /SUBSYSTEM:CONSOLE;对于 GUI 应用程序,这个链接程序开关是 /SUBSYSTEM:WINDOWS
当用户运行应用程序时,操作系统的加载程序会在可执行映像文件头的内部查找,并获取该子系统值。

  • 如果该值表示一个基于 CUI 的应用程序,则加载程序会自动确保为该应用程序提供一个文本控制台窗口——比如,当从命令提示符启动该应用程序时——并且在需要时会创建另一个控制台——比如,当从Windows 资源管理器启动相同的基于 CUI 的应用程序时。
  • 如果该值表示基于 GUI 的应用程序,则加载程序不会创建控制台窗口,只是加载该应用程序。一旦应用程序开始运行,操作系统并不关心应用程序拥有哪种类型的 UI。

Windows 应用程序必须具有入口点函数,它在应用程序开始运行时会被调用。在C/C++中,可以使用两种入口点函数:

int WINAPI _tWinMain(HINSTANCE hInstanceExe, HINSTANCE, PTSTR pszCmdLine, int nCmdShow); 
 
int _tmain(int argc, TCHAR *argv[], TCHAR *envp[]); 

操作系统实际上并不调用编写的入口点函数,调用的是C/C++运行时启动函数,该函数在运行时实现,并在链接时使用 -entry: 命令行选项进行设置。该函数初始化C/C++运行时库,以便你可以调用如 malloc 和 free 之类的函数。它还确保在代码执行之前正确构造声明的所有全局和静态C++对象。

表4-1:应用程序类型及其相应的入口点

应用程序类型入口点嵌入可执行文件中的启动函数
GUI 应用程序,需要 ANSI 字符和字符串_tWinMain (WinMain)WinMainCRTStartup
GUI 应用程序,需要 Unicode 字符和字符串_tWinMain (wWinMain)wWinMainCRTStartup
CUI 应用程序,需要 ANSI 字符和字符串_tmain (Main)mainCRTStartup
CUI 应用程序,需要 Unicode 字符和字符串_tmain (Wmain)wmainCRTStartup

链接程序负责在它链接可执行文件时选择正确的C/C++运行时启动功能。如果指定了 /SUBSYSTEM:WINDOWS 链接程序开关,则链接程序期望找到 WinMain 或 wWinMain 函数。

  • 如果这两个函数都不存在,链接程序将返回“无法解析的外部符号”错误;
  • 否则,它选择调用 WinMainCRTStartup 或 wWinMainCRTStartup 函数。

但很少有人知道,可以从项目中全部删除 /SUBSYSTEM 链接程序开关。当这样做时,链接程序会自动确定应用程序应设置为哪个子系统。当链接时,链接程序检查代码中存在 4 个函数 (WinMain、wWinMain、main 或 wmain) 中的哪个,然后推断可执行文件应该是哪个子系统,以及哪个C/C++启动函数应嵌入到可执行文件中。

Windows/Visual C++开发人员可能会犯的一个错误是,在创建新项目时不小心选择了错误的项目类型。例如,开发人员可能会创建一个新的 Win32 Application 项目,但创建 main 入口点函数。当生成应用程序时,开发人员将收到链接程序错误,因为 Win32 Application 项目设置了 /SUBSYSTEM:WINDOWS 链接程序开关。此时,开发人员有 4 种选择:

  • 将 main 函数改为 WinMain。通常这不是最佳选择,因为开发人员可能想要创建控制台应用程序。
  • 在 Visual C++ 中创建一个新的 Win32 控制台应用程序,然后将现有的源代码模块添加到新项目中。这个选项有点繁琐。
  • 单击项目属性对话框的“链接”选项卡,然后在“配置属性/链接器/系统/子系统”选项中将 /SUBSYSTEM:WINDOWS 改为 /SUBSYSTEM:CONSOLE,如下图所示。这是解决问题的简便方法。
  • 单击项目属性对话框的“链接”选项卡,然后完全删除 /SUBSYSTEM:WINDOWS 开关。这种方法提供了最大的灵活性。现在,链接程序将根据源代码中实现的函数执行正确的操作。

在属性对话框中为一个项目选择CUI子系统

所有C/C++运行时启动函数基本上都做同样的事情。区别在于它们处理 ANSI 还是 Unicode 字符串,以及在初始化C运行时库后调用哪个入口点函数。Visual C++ 配有C运行时库的源代码。下面是启动函数的任务:

  • 检索指向新进程的完整命令行的指针。
  • 检索指向新进程的环境变量的指针。
  • 初始化C/C++运行时的全局变量。如果包含 StdLib.h,代码就可以访问这些变量。变量在表4-2中列出。
  • 对C运行时内存分配函数 (malloc 和 calloc) 和其他低级输入/输出例程使用的堆进行初始化。
  • 调用所有全局和静态C++类对象的构造函数。

表4-2:程序可以使用的C/C++运行时全局变量

变量名类型说明与推荐的 Windows 函数替换
_osverunsigned int操作系统的内部版本 (build version)。查看 Version Helper functions
_winmajorunsigned intWindows的主要版本,以十六进制表示法。查看 Version Helper functions
_winminorunsigned intWindows的次要版本,以十六进制表示法。查看 Version Helper functions
_winverunsigned int(_winmajor << 8) + _winminor。查看 Version Helper functions
__argcunsigned int在命令行上传递的实参数量。推荐 GetCommandLine
__argv
__wargv
char
wchar_t
大小为 __argc 的数组,带有指向 ANSI/Unicode 字符串的指针。每个数组项均指向一个命令行实参。
注意,若定义了 _UNICODE,则 __argv 为 NULL;否则 __wargv 为NULL。
推荐 GetCommandLine
_environ
_wenviron
char
wchar_t
指向 ANSI/Unicode 字符串的指针的数组。每个数组项均指向一个环境字符串。
注意,若定义了 _UNICODE,则 _environ 为 NULL;否则 _wenviron 为NULL。
推荐 GetEnvironmentStringsGetEnvironmentVariable
_pgmptr
_wpgmptr
char
wchar_t
运行程序的 ANSI/Unicode 完整路径和名子。
注意,若定义了 _UNICODE,则 _pgmptr 为 NULL;否则 _wpgmptr 为NULL。
推荐 GetModuleFileName,传递 NULL 作为它的第一个参数。

完成所有这些初始化之后,C/C++启动函数将调用应用程序的入口点函数。如果编写了_tWinMain 函数,当定义了 _UNICODE 时,它将按以下方式被调用:

GetStartupInfo(&StartupInfo);
int nMainRetVal = wWinMain((HINSTANCE)&__ImageBase, NULL, pszCommandLineUnicode,
	(StartupInfo.dwFlags & STARTF_USESHOWWINDOW) ? StartupInfo.wShowWindow : SW_SHOWDEFAULT); 

注意,_ImageBase 是链接程序定义的伪变量,它显示可执行文件映射到应用程序内存中的哪个地址。

如果编写了 _tmain 函数,当定义了 _UNICODE 时,它按以下方式被调用:

int nMainRetVal = wmain(argc, argv, envp); 

注意,通过 Visual Studio 向导生成应用程序时,在 CUI 应用程序入口点中未定义第三个参数 (环境变量块),如下所示:

int _tmain(int argc, TCHAR* argv[]); 

如果需要访问进程的环境变量,只需将以下内容替换先前的定义:

int _tmain(int argc, TCHAR* argv[], TCHAR* env[]) 

这个 env 参数指向一个数组,其中包含所有环境变量,变量后是它们的值,并用等号 = 字符分隔。

当入口点函数返回时,启动函数将调用C运行时 exit 函数,并将其返回值 (nMainRetVal) 传递给它。exit 函数执行以下操作:

  • 它调用由 _onexit 函数调用而注册的所有函数。
  • 它调用所有全局和静态C++对象的析构函数。
  • 在 DEBUG 构建中,如果已设置 _CRTDBG_LEAK_CHECK_DF 标志,则通过调用 _CrtDumpMemoryLeaks 函数列出 C/C++ 运行时内存管理中的泄漏。
  • 它调用操作系统的 ExitProcess 函数,并将 nMainRetVal 传递给它。这使得操作系统终止进程并设置其退出代码。

注意,为安全起见,已弃用所有这些变量,因为在C运行时库有机会对它们进行初始化之前,使用它们的代码可能就已运行。这就是为什么应该直接调用 Windows API 的相应函数的原因。

进程实例句柄

加载到进程地址空间中的每个可执行文件或DLL文件都分配有唯一的实例句柄。可执行文件的实例被传递给 (w)WinMain,作为它的第一个参数 hInstanceExe。加载资源的调用通常需要句柄的值。例如,要从可执行文件的映像中加载图标资源,需要调用以下函数:

HICON LoadIcon(HINSTANCE hInstance, PCTSTR pszIcon); 

LoadIcon 的第一个参数指明哪个文件 (可执行文件或DLL文件) 包含要加载的资源。许多应用程序将 (w)WinMain 的 hInstanceExe 参数保存在全局变量中,以便所有可执行文件的代码都可以轻松访问它。

Platform SDK 文档指出某些函数需要 HMODULE 类型的参数。例如,GetModuleFileName 函数,如下所示:

DWORD GetModuleFileName(HMODULE hInstModule, PTSTR pszPath, DWORD cchPath); 

注:事实证明,HMODULE 和 HINSTANCE 是完全相同的对象。如果函数文档表明需要 HMODULE,则可以传递 HINSTANCE,反之亦然。之所以有两种数据类型,是因为在 16 位 Windows 中,HMODULE 和 HINSTANCE 标识了不同的事物。

(w)WinMain 的 hInstanceExe 参数的实际值是,系统将可执行文件的映像加载到进程的地址空间中的基本内存地址。例如,如果系统打开可执行文件并将其内容加载到地址 0x00400000,则 (w)WinMain 的 hInstanceExe 参数的值为 0x00400000。

可执行文件的映像加载到的基地址由链接程序决定。不同的链接程序可以使用不同的默认基地址。对于 Mircrosoft 连接程序,可以使用 /BASE:address 链接程序开关,更改应用程序加载到的基地址。

下面的 GetModuleHandle 函数返回句柄/基地址,在该地址中将可执行文件或DLL文件加载到进程的地址空间中:

HMODULE GetModuleHandle(PCTSTR pszModule);

当调用这个函数时,传递一个以0结尾的字符串,该字符串指定加载到调用进程的地址空间中的可执行文件或DLL文件的名字。
如果系统找到指定的可执行文件或DLL名字,则 GetModuleHandle 返回此可执行文件或DLL的文件映像加载的基地址。如果找不到文件,系统将返回 NULL。
还可以调用 GetModuleHandle,为 pszModule 参数传递 NULL;GetModuleHandle 返回正在调用的可执行文件的基地址。
如果代码在DLL中,有两种方法可以知道代码在哪个模块中运行。
第一种,可以利用伪变量 __ImageBase,它由链接程序提供,指向当前运行模块的基地址。如前所述,这是C运行时启动代码在调用 (w)WinMain 函数时所做的事情。

另一个选择是调用 GetModuleHandleEx 函数,将 GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS 作为它的第一个参数,将当前方法的地址作为第二个参数。传递一个指向 HMODULE 的指针作为最后一个参数,它会由 GetModuleHandleEx 填入该函数所在 DLL 的相应基地址。以下代码显示了这两个选项:

extern "C" const IMAGE_DOS_HEADER __ImageBase; 

void DumpModule() {
	// Get the base address of the running application.
	// Can be different from the running module if this code is in a DLL.
	HMODULE hModule = GetModuleHandle(NULL);
	_tprintf(TEXT("with GetModuleHandle(NULL) = 0x%x\r\n"), hModule);
	
	// Use the pseudo-variable __ImageBase to get
	// the address of the current module hModule/hInstance.
	_tprintf(TEXT("with __ImageBase = 0x%x\r\n"), (HINSTANCE)&__ImageBase);
	
	// Pass the address of the current method DumpModule
	// as parameter to GetModuleHandleEx to get the address
	// of the current module hModule/hInstance.
	hModule = NULL;
	GetModuleHandleEx(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS, (PCTSTR)DumpModule, &hModule);
	_tprintf(TEXT("with GetModuleHandleEx = 0x%x\r\n"), hModule);
}

int _tmain(int argc, TCHAR* argv[]) {
	DumpModule();
	return(0);
} 

记住 GetModuleHandle 函数的两个重要特性。

  • 首先,它仅检查调用的进程 (calling process) 的地址空间。如果调用的进程不使用任何常用的对话框函数,调用 GetModuleHandle 并传递 ComDlg32,会返回 NULL,即使 ComDlg32.dll 可能已加载到其他进程的地址空间中。
  • 其次,调用 GetModuleHandle 并传递 NULL 值将返回可执行文件在进程地址空间中的基地址。因此,即使从 DLL 中包含的代码调用 GetModuleHandle(NULL),返回的值也是可执行文件的基地址,而不是 DLL 文件的基地址。

进程的前一个实例句柄

C/C++运行时启动代码总是将 NULL 传递给 (w)WinMain 的 hPrevInstance 参数。这个参数在 16 位 Windows 中使用,仅用于简化 16 位 Windows 应用程序的移植。永远不要在代码中引用此参数。因此,可以编写 (w)WinMain 函数如下:

int WINAPI _tWinMain(HINSTANCE hInstanceExe, HINSTANCE, PSTR pszCmdLine, int nCmdShow); 

因为没有为第二个参数提供参数名字,所以编译器不会发出“未引用参数”警告。Visual Studio 选择了另一种解决方案:向导生成的 C++ GUI 项目利用 UNREFERENCED_PARAMETER 宏来删除这些警告,如以下代码片段所示:

int APIENTRY _tWinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPTSTR lpCmdLine, int nCmdShow) {
	UNREFERENCED_PARAMETER(hPrevInstance);
	UNREFERENCED_PARAMETER(lpCmdLine);
	...
}

进程的命令行

当创建一个新进程时,会向它传递命令行。命令行几乎永远不会为空;至少,用于创建新进程的可执行文件的名字是命令行上的第一个标记。但是,进程可以接收由单个字符组成的命令行:以字符串结尾的 0。
当C运行时的启动代码开始执行GUI应用程序时,它通过调用 GetCommandLine 函数来检索进程的完整命令行,跳过可执行文件的名字,并将指向命令行其余部分的指针传递给 WinMain 的 pszCmdLine 参数。

实际上,可以写入 pszCmdLine 参数指向的内存缓冲区,但不应超出缓冲区末尾。最好将其视为只读缓冲区。如果要更改命令行,先将命令行缓冲区复制到应用程序中的本地缓冲区,然后修改本地缓冲区。

可以通过调用 GetCommandLine 函数获得指向进程完整命令行的指针

PTSTR GetCommandLine(); 

该函数返回一个指向包含完整命令行 (包括已执行文件的完整路径名) 的缓冲区的指针。注意,GetCommandLine 总是返回同一缓冲区的地址。这是为什么不应该写入 pszCmdLine 的另一个原因:它指向同样的缓冲区,在对其进行修改后,将无法知道原始命令行是什么。

许多应用程序喜欢解析为单独标记的命令行。应用程序可以使用全局 __argc 和 __argv (或 __wargv) 变量访问命令行的各个组成部分,即使这些变量已被弃用。在 ShellAPI.h 中声明并由 Shell32.dll 导出的函数 CommandLineToArgvW 将所有Unicode字符串分成单独的标记:

PWSTR* CommandLineToArgvW(PWSTR pszCmdLine, int* pNumArgs); 

第一个参数 pszCmdLine 指向命令行字符串。这通常是先前 GetCommandLineW 调用的返回值。pNumArgs 参数是整数的地址;整数设置为命令行中的参数数量。CommandLineToArgvW 返回到Unicode字符串指针数组的地址。

CommandLineToArgvW 在内部分配内存。大多数应用程序不会释放此内存,而是依靠操作系统在进程终止时释放它。如果想自己释放内存,正确的方法是调用 HeapFree,如下所示:

int nNumArgs;
PWSTR *ppArgv = CommandLineToArgvW(GetCommandLineW(), &nNumArgs); 

// Use the arguments…
if (*ppArgv[1] == L'x') {
	...
}
// Free the memory block
HeapFree(GetProcessHeap(), 0, ppArgv); 

进程的环境变量

每个进程都有一个与之关联的环境块。环境块是在进程的地址空间内分配的一块内存,其中包含一组具有以下形式的字符串:

=::=::\ ...
VarName1=VarValue1\0
VarName2=VarValue2\0 
VarName3=VarValue3\0
...
VarNameX=VarValueX\0
\0 

每个字符串的第一部分是环境变量的名称。这之后是一个等号,然后是要分配给该变量的值。注意,除了第一个 =::=::\ 字符串之外,该块中的其他一些字符串也可能以 = 字符开头。在这种情况下,这些字符串不用作环境变量。

介绍访问环境块的两种方式,每种方式都提供了具有不同解析的不同输出。第一种方法是通过调用 GetEnvironmentStrings 函数来检索完整的环境块。格式与上一段完全相同。以下代码显示了在这种情况下如何提取环境变量及其内容:

void DumpEnvStrings() {
	PTSTR pEnvBlock = GetEnvironmentStrings();
	
	// Parse the block with the following format:
	//    =::=::\
	//    =...
	//    var=value\0
	
	//    ...
	//    var=value\0\0
	// Note that some other strings might begin with '='.
	// Here is an example when the application is started from a network share.
	//    [0] =::=::\
	//    [1] =C:=C:\Windows\System32
	//    [2] =ExitCode=00000000
	//
	TCHAR szName[MAX_PATH];
	TCHAR szValue[MAX_PATH];
	PTSTR pszCurrent = pEnvBlock;
	HRESULT hr = S_OK;
	PCTSTR pszPos = NULL;
	int current = 0;
	
	while (pszCurrent != NULL) {
		// Skip the meaningless strings like:
		// "=::=::\"
		if (*pszCurrent != TEXT('=')) {
			// Look for '=' separator.
			pszPos = _tcschr(pszCurrent, TEXT('='));
			
			// Point now to the first character of the value.
			pszPos++;
			
			// Copy the variable name.
			size_t cbNameLength = // Without the' ='
				(size_t)pszPos - (size_t)pszCurrent - sizeof(TCHAR);
			hr = StringCbCopyN(szName, MAX_PATH, pszCurrent, cbNameLength);
			if (FAILED(hr)) {
				break;
			}
			
			// Copy the variable value with the last NULL character
			// and allow truncation because this is for UI only.
			hr = StringCchCopyN(szValue, MAX_PATH, pszPos, _tcslen(pszPos)+1);
			if (SUCCEEDED(hr)) {
				_tprintf(TEXT("[%u] %s=%s\r\n"), current, szName, szValue);
			} else // something wrong happened; check for truncation.
			if (hr == STRSAFE_E_INSUFFICIENT_BUFFER) {
				_tprintf(TEXT("[%u] %s=%s...\r\n"), current, szName, szValue);
			} else { // This should never occur.
				_tprintf(TEXT("[%u] %s=???\r\n"), current, szName);
				break;
			}
		} else {
			_tprintf(TEXT("[%u] %s\r\n"), current, pszCurrent);
		}
		
		// Next variable please.
		current++;
		
		// Move to the end of the string.
		while (*pszCurrent != TEXT('\0'))
			pszCurrent++;
		pszCurrent++;
		
		// Check if it was not the last string.
		if (*pszCurrent == TEXT('\0'))
			break;
	};
	
	// Don't forget to free the memory.
	FreeEnvironmentStrings(pEnvBlock);
} 

以 = 字符开头的无效字符串将被跳过。其他有效的字符串都被一个一个地解析,= 字符用作名字和值之间的分隔符。当不再需要 GetEnvironmentStrings 返回的内存块时,应通过调用 FreeEnvironmentStrings 释放它:

BOOL FreeEnvironmentStrings(PTSTR pszEnvironmentBlock); 

注意,在该代码段中使用了C运行时的安全字符串函数,使用 StringCbCopyN 以字节为单位计算大小,当值对于复制缓冲区而言太长时,使用 StringCchCopyN 截断。

访问环境变量的第二种方法仅用于CUI应用程序,该方法通过 main 入口点接收的 TCHAR* env[] 参数进行。env 是一个字符串指针数组,每个指针指向不同的环境变量定义,其常用的格式是“名字=值”。指向最后一个变量字符串的指针之后会出现一个 NULL 指针,如下所示:

void DumpEnvVariables(PTSTR pEnvBlock[]) {
	int current = 0;
	PTSTR* pElement = (PTSTR*)pEnvBlock;
	PTSTR pCurrent = NULL;
	while (pElement != NULL) {
		pCurrent = (PTSTR)(*pElement);
		if (pCurrent == NULL) {
			// No more environment variable.
			pElement = NULL;
		} else {
			_tprintf(TEXT("[%u] %s\r\n"), current, pCurrent);
			current++;
			pElement++;
		}
	}
} 

注意,在收到 env 之前,以 = 字符开头的字符串已被删除,因此不必自己处理它们。

因为等号用于将名字与值分开,所以等号不能成为名字的一部分。而且,空格是有意义的。例如,如果声明以下两个变量,然后将 XYZ 的值与 ABC 的值进行比较,则系统将报告这两个变量是不同的,因为考虑到了在等号之前或之后出现的任何空白 :

XYZ= Windows  (Notice the space after the equal sign.)
ABC=Windows 

例如,如果要将以下两个字符串添加到环境块,则带空格后的环境变量 XYZ 包含 Home,而不带空格的环境变量 XYZ 包含 Work。

XYZ =Home  (Notice the space before the equal sign.)
XYZ=Work 

当用户登录 Windows 时,系统将创建 shell 进程并将其与一组环境字符串关联。系统通过检查注册表中的两个键来获取初始的环境字符串集合

第一个键包含应用于系统的所有环境变量的列表:

HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\Session Manager\Environment 

第二个键包含应用于当前登录用户的所有环境变量的列表:

HKEY_CURRENT_USER\Environment

通过选择【控制面板】中的【系统】,单击左侧的【高级系统设置】链接,然后单击【环境变量】按钮弹出一个对话框,用户可以添加、删除或更改这些记录。

只有具有管理员权限的用户才能更改“系统变量”列表中包含的变量。

应用程序还可以使用各种注册表函数来修改这些注册表项。
但是,为了使这改变对所有应用程序都生效,用户必须注销然后重新登录。
当某些应用程序 (比如资源管理器、任务管理器和控制面板) 的主窗口接收到 WM_SETTINGCHANGE 消息时,这些应用程序可以使用新的注册表项更新它们的环境块。例如,如果更新注册表项并希望某些应用程序更新其环境块,可以进行以下调用:

SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, (LPARAM) TEXT("Environment")); 

通常,子进程继承与其父进程相同的一组环境变量。但是,父进程可以控制子进程继承的环境变量。继承 (inherit) 是指子进程获得其父进程环境块的副本;子进程和父进程不共享同一环境块。这意味着子进程可以在它的环境块中添加、删除或修改变量,且这些更改不会反映在父进程的环境块中。

应用程序通常使用环境变量来让用户调整其行为。用户创建一个环境变量并将其初始化。然后,当用户调用应用程序时,应用程序检查环境块中的变量。如果找到该变量,它将解析该变量的值并调整其自身的行为。

环境变量的问题在于,用户不容易设置或理解它们。用户需要正确拼写变量名字,还必须知道变量值所需的确切语法。另一方面,大多数图形应用程序允许用户使用对话框来调整应用程序的行为。这种方法更加用户友好。

如果仍然想使用环境变量,那么应用程序可以调用一些函数。GetEnvironmentVariable 函数可以确定环境变量是否存在和它的值:

DWORD GetEnvironmentVariable(PCTSTR pszName, PTSTR pszValue, DWORD cchValue); 

调用 GetEnvironmentVariable 时,pszName 指向所需的变量名字,pszValue 指向用于保存变量值的缓冲区,cchValue 指明缓冲区的大小,以字符为单位。
该函数返回复制到缓冲区中的字符数,如果在环境中找不到变量名,则返回 0。
但是,由于不知道需要多少个字符来存储环境变量的值,因此当将 0 传递给 cchValue 参数时,GetEnvironmentVariable 返回加上最后的 NULL 字符的字符数目。
以下代码演示了如何安全使用此功能:

void PrintEnvironmentVariable(PCTSTR pszVariableName) {
	PTSTR pszValue = NULL;
	// Get the size of the buffer that is required to store the value
	DWORD dwResult = GetEnvironmentVariable(pszVariableName, pszValue, 0);
	
	if (dwResult != 0) {
		// Allocate the buffer to store the environment variable value
		DWORD size = dwResult * sizeof(TCHAR);
		pszValue = (PTSTR)malloc(size);
		GetEnvironmentVariable(pszVariableName, pszValue, size);
		_tprintf(TEXT("%s=%s\n"), pszVariableName, pszValue);
		free(pszValue);
	} else {
		_tprintf(TEXT("'%s'=<unknown value>\n"), pszVariableName);
	}
} 

许多字符串中包含可替换的字符串部分。例如,在注册表中的某个位置可能找到如下字符串:

%USERPROFILE%\Documents 

百分号 % 之间的部分表示可替换的字符串。在这种情况下,应将环境变量 USERPROFILE 的值放在这个字符串中。在计算机中,如果 USERPROFILE 环境变量的值为

C:\Users\jrichter

那么,在执行字符串替换之后,得到的字符串为

C:\Users\jrichter\Documents

由于这种类型的字符串替换很常见,因此 Windows 提供了 ExpandEnvironmentStrings 函数:

DWORD ExpandEnvironmentStrings(PTCSTR pszSrc, PTSTR pszDst, DWORD chSize); 

调用此函数时,pszSrc 参数是包含可替换环境变量字符串的字符串的地址。pszDst 参数是用于接收扩展字符串的缓冲区的地址,chSize 参数是这个缓冲区的最大大小,以字符为单位。
返回的值是存储扩展字符串所需的缓冲区大小,以字符为单位。如果 chSize 参数小于这个值,则不会扩展%%变量,而是将其替换为空字符串。因此,通常调用 ExpandEnvironmentStrings 两次,如以下代码片段所示:

DWORD chValue = ExpandEnvironmentStrings(TEXT("PATH='%PATH%'"), NULL, 0);
PTSTR pszBuffer = new TCHAR[chValue];
chValue = ExpandEnvironmentStrings(TEXT("PATH='%PATH%'"), pszBuffer, chValue);
_tprintf(TEXT("%s\r\n"), pszBuffer);
delete[] pszBuffer; 

最后,可以使用 SetEnvironmentVariable 函数添加变量,删除变量或修改变量的值:

BOOL SetEnvironmentVariable(PCTSTR pszName, PCTSTR pszValue); 

此函数将 pszName 参数标识的变量设置为 pszValue 参数标识的值。

  • 如果具有指定名称的变量已存在,则 SetEnvironmentVariable 会修改该值。
  • 如果指定的变量不存在,则添加该变量;
  • 如果 pszValue 为 NULL,则从环境块中删除该变量。

进程的亲和性 (A Process’ Affinity)

正常情况下,进程中的线程可以在主机中的任何CPU上执行。但是,某个进程的线程可能会被强制在可用CPU的某个子集上运行。这称为处理器关联 (processor affinity),在第7章中进行了详细讨论。子进程继承其父进程的亲和性。

进程的错误模式

与每个进程相关联的是一组标志,这些标志告诉系统该进程应如何响应严重错误,包括磁盘介质故障、未处理的异常、文件查找故障和数据未对齐。进程可以通过调用 SetErrorMode 函数来告诉系统如何处理这些错误:

UINT SetErrorMode(UINT fuErrorMode);

fuErrorMode 参数是表4-3中所示的任何标志的按位“或”的组合。

表4-3:SetErrorMode 使用的标志

标志描述
SEM_FAILCRITICALERRORS系统不显示关键错误处理程序消息框,将错误返回到调用过程。
SEM_NOGPFAULTERRORBOX系统不显示一般保护故障消息框。这个标志应该仅能由调试应用程序来设置,这个应用程序使用异常处理程序处理一般保护 (GP: General Protection) 故障。
SEM_NOOPENFILEERRORBOX找不到文件时,系统不显示消息框。
SEM_NOALIGNMENTFAULTEXCEPT系统会自动修复内存对齐错误,并使它们对应用程序不可见。该标志对 x86/x64 处理器无效。

默认情况下,子进程继承其父进程的错误模式标志。
父进程可以通过在调用 CreateProcess 时指定 CREATE_DEFAULT_ERROR_MODE 标志来防止子进程继承其错误模式。

进程的当前驱动器和目录

如果未提供完整路径名,则各种 Windows 函数会在当前驱动器的当前目录中查找文件和目录。

系统在内部记录进程的当前驱动器和目录。因为此信息是在每个进程的基础上维护的,所以,如果某个进程更改了当前驱动器或目录,那么该进程中的所有线程也会更改此信息。

线程可以通过调用以下两个函数来获取并设置其进程的当前驱动器和目录:

DWORD GetCurrentDirectory(DWORD  nBufferLength, LPTSTR lpBuffer);
BOOL SetCurrentDirectory(LPCTSTR lpPathName);

若要确定 GetCurrentDirectory 所需的缓冲区大小,将 lpBuffer 参数设置为 NULL,并将 nBufferLength 参数设置为 0。函数将返回存储此文件夹所需的字符数,包括最后的 ‘\0’ 字符。
调用成功后,将返回字符串长度 (以字符为单位),而不计算终止的 ‘\0’ 字符。

进程的当前目录

系统会记录进程的当前驱动器和目录,但不会记录每个驱动器的当前目录。但是,有些操作系统支持处理多个驱动器的当前目录。通过进程的环境字符串提供此支持。例如,一个进程可以具有如下所示的两个环境变量:

=C:=C:\Utility\Bin 
=D:=D:\Program Files 

这些变量指明驱动器C的进程的当前目录为 \Utility\Bin,驱动器D的进程的当前目录为 \Program Files。

如果调用一个函数,传递一个驱动限定名表示一个不是当前驱动器的驱动器,系统在该进程的环境块中查找与指定驱动器号关联的变量。如果这个驱动器的变量存在,系统使用这个变量的值作为当前目录。如果变量不存在,则系统假定指定驱动器的当前目录是它的根目录。

例如,如果进程的当前目录为 C:\Utility\Bin,并调用 CreateFile 打开 D:ReadMe.Txt,则系统将查找环境变量 =D:。 因为 =D: 变量存在,所以系统尝试从 D:\ Program Files 目录打开 ReadMe.Txt 文件。如果 =D: 变量不存在,则系统将尝试从驱动器D的根目录打开 ReadMe.Txt 文件。Windows文件函数从不添加或更改驱动器号环境变量——它们仅读取变量。

注:可以使用C运行时函数 _chdir 代替 Windows SetCurrentDirectory 函数来更改当前目录。_chdir 函数在内部调用 SetCurrentDirectory,但是 _chdir 也通过调用 SetEnvironmentVariable 来添加或修改环境变量,以便保留不同驱动器的当前目录。

如果父进程创建一个环境块,想要将其传递给子进程,子进程的环境块不会自动继承父进程的当前目录。而是,子进程的当前目录默认为每个驱动器的根目录。如果希望子进程继承父进程的当前目录,父进程必须在生成子进程之前,创建这些驱动器号环境变量并将其添加到环境块中。父进程可以通过调用 GetFullPathName 获得其当前目录:

DWORD GetFullPathName(
	PCTSTR pszFile,
	DWORD cchPath,
	PTSTR pszPath,
	PTSTR *ppszFilePart); 

例如,要获取驱动器C的当前目录,请按以下方式调用 GetFullPathName:

TCHAR szCurDir[MAX_PATH];
DWORD cchLength = GetFullPathName(TEXT("C:"), MAX_PATH, szCurDir, NULL); 

因此,驱动器号环境变量通常必须放在环境块的开头。

系统版本

Getting the System Version


【Windows via C/C++】目录

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值