第Ⅱ部分完成编程任务
本部分内容包括
¾ 第4章 进程
¾ 第5章 作业
¾ 第6章 线程基础
¾ 第7章 线程进度安排、优先级和线程亲和性
¾ 第8章 用户模式下的线程同步
¾ 第9章 线程与内核对象的同步
¾ 第10章 同步和异步设备I/O
¾ 第11章 Windows线程池
¾ 第12章 纤程
第 4 章进程
本章内容包括:
¾ 编写第一个Windows应用程序
¾ CreateProcess函数
¾ 终止进程
¾ 子进程
¾ 当管理员以标准用户的身份运行时
本章将讨论系统如何管理正在运行的所有应用程序。首先要解释什么是进程,以及系统如
何创建一个进程内核对象来管理每个进程。然后,我们要解释如何利用与一个进程关联的内核
对象来操纵该进程。接下来,我们要讨论进程的各种不同的属性(或特性),以及用于查询和
更改这些属性的几个函数。另外,还要讨论如何利用一些函数在系统中创建或生成额外的进程。
当然,最后还要讨论如何终止线程,这是讨论进程时必不可少的一个主题。
一般将进程定义成一个正在运行的程序的一个实例,它由以下两个组件构成:
¾ 一个内核对象,操作系统用它来管理进程。内核对象也是系统保存进程统计信息的地
方。
¾ 一个地址空间,其中包含所有执行体(executable)或DLL模块的代码和数据。此外,
它还包含动态内存分配,比如线程堆栈和堆的分配。
进程是有“惰性”的。进程要做任何事情,都必须让一个线程在它的上下文中运行。该线
程要执行进程地址空间包含的代码。事实上,一个进程可以有多个线程,所有线程都在进程的
地址空间中“同时”执行代码。为此,每个线程都有它自己的一组CPU寄存器和它自己的堆栈。
每个进程至少要有一个线程执行进程地址空间包含的代码。一个进程创建的时候,系统会自动
创建它的第一个线程,这称为主线程(primary thread)。然后,这个线程再创建更多的线程,
后者再创建更多的线程……。如果没有线程要执行进程地址空间包含的代码,进程就失去了继
续存在的理由。所以,系统会自动销毁进程及其地址空间。
对于所有要运行的线程,操作系统会轮流为每个线程调度一些CPU时间。它会采取
round-robin(轮询或轮流)方式,为每个线程都分配时间片(称为“量”或者“量程”,即quantum),
从而营造出所有线程都在“并发”运行的假象。图4-1展示了一台单CPU的机器的工作方式。
图4-1 在单CPU的计算机上,操作系统以round-robin方式为每个单独的线程分配时间量
如果计算机配备了多个CPU,操作系统会采用更复杂的算法为线程分配CPU时间。Microsoft
Windows可以同时让不同的CPU执行不同的线程,使多个线程能真正并发运行。在这种类型的
计算机系统中,Windows内核将负责线程的所有管理和调度任务。你不必在自己的代码中做任
何特别的事情,即可享受到多处理器系统带来的好处。不过,为了更好地利用这些CPU,你需
要在应用程序的算法中多做一些文章。
4.1 编写第一个 Windows 应用程序
Windows支持两种类型的应用程序:GUI程序和CUI程序。前者是“图形用户界面”(Graphical
user interface)的简称,后者是“控制台用户界面”(Console user interface)的简称。GUI程序
一个图形化的前端。它可以创建窗口,可以拥有菜单,能通过对话框与用户交互,还能使用所
有标准的“视窗化”的东西。Windows的几乎所有附件应用程序(比如记事本、计算器和写字
板等等)都是GUI程序。控制台程序则是基于文本的。它们一般不会创建窗口或进程消息,而
且不需要GUI。虽然CUI程序是在屏幕上的一个窗口中运行的,但这个窗口中只有文本。“命令
提示符”(CMD.EXE)是CUI程序的一个典型的例子。
其实,这两种应用程序的界线是非常模糊的。你完全可以创建出能显示对话框的CUI应用程序。
例如,在执行CMD.EXE并打开“命令提示符”后,你可以执行一个特殊的命令来显示一个图形
化对话框。并在其中选择想要执行的命令,而不必强行记忆各种控制台命令。另外,还可以创
建一个要向控制台窗口输出文本字符串的GUI应用程序。例如,我自己写的GUI程序经常都要创
建一个控制台窗口,便于我查看应用程序执行期间的调试信息。不过,我当然要鼓励你尽可能
在程序中使用一个GUI,而不要使用老式的字符界面,后者对用户来说不太友好!
用Microsoft Visual Studio来创建一个应用程序项目时,集成开发环境会设置各种链接器开关,
使链接器将子系统的正确类型嵌入最终生成的执行体(executable,或者称为“可执行文件”)
中。对于CUI程序,这个链接器开关是/SUBSYSTEM:CONSOLE,对于GUI程序,则是
/SUBSYSTEM:WINDOWS。用户运行应用程序时,操作系统的加载器(loader)会检查执行体
映像的header,并获取这个子系统值。如果此值表明是一个CUI程序,加载程序会自动确定有一
个可用的文本控制台窗口(比如从命令提示符启动这个程序的时候)。另外,如有必要,会创
建一个新窗口(比如从Windows资源管理器启动这个CUI程序的时候)。如果此值表明是一个
GUI程序,加载器就不会创建控制台窗口;相反,它只是加载这个程序。一旦应用程序开始运
行,操作系统就不再关心应用程序的界面是什么类型的。
Windows应用程序必须有一个入口函数,应用程序开始运行时,这个函数会被调用。C/C++ 开
发人员可以使用以下两种入口函数:
int WINAPI _tWinMain(
HINSTANCE hInstanceExe,
HINSTANCE,
PTSTR pszCmdLine,
int nCmdShow);
int _tmain(
int argc,
TCHAR *argv[],
TCHAR *envp[]);
注意,具体的符号要取决于你是否要使用Unicode字符串。操作系统实际并不调用你所写的入口
函数。相反,它会调用由C/C++运行库实现并在链接时使用-entry:命令行选项来设置的一个
C/C++运行时启动函数。该函数将初始化C/C++运行库,使你能调用malloc和free之类的函数。
它还确保了在你的代码开始执行之前,你声明的任何全局和静态C++对象都被正确地构造。表
4-1总结了你的源代码要实现什么入口函数,以及每个入口函数应该在什么时候使用。
表4-1 应用程序类型和相应的入口函数
应用程序类型 入口函数(入口点) 嵌入执行体的启动函数
处理ANSI字符和字符串的GUI应用
程序
_tWinMain (WinMain) WinMainCRTStartup
处理Unicode字符和字符串的GUI应
用程序
_tWinMain (wWinMain) wWinMainCRTStartup
处理ANSI字符和字符串的CUI应用
程序
_tmain (Main) mainCRTStartup
处理Unicode字符和字符串的CUI应
用程序
_tmain (Wmain) wmainCRTStartup
链接你的执行体时,链接器将选择正确的C/C++ 运行时启动函数。如果指定了
/SUBSYSTEM:WINDOWS链接器开关,链接器就会寻找WinMain或wWinMain函数。如果没
有找到这两个函数,链接器将返回一个“unresolved external symbol(未解析的外部符号)”错
误;否则,它将根据具体情况分别选择WinMainCRTStartup或wWinMainCRTStartup函数。
类似地,如果指定了/SUBSYSTEM:CONSOLE链接器开关,链接器就会寻找main或wmain函数,
并根据情况分别选择mainCRTStartup或wmainCRTStartup函数。同样地,如果main和wmain
函数都没有找到,链接器会返回一个“unresolved external symbol(未解析的外部符号)”错误。
不过,一个鲜为人知的事实是,完全可以从自己的项目中移除/SUBSYSTEM链接器开关。一旦
这样做,链接器就会自动判断应该将应用程序设为哪一个子系统。链接时,链接器会检查代码
中包括4个函数中的哪一个(WinMain,wWinMain,main或wmain),并据此推算你的执行体
应该是哪个子系统,以及应该在执行体中嵌入哪个C/C++启动函数。
Windows/Visual C++新手开发人员常犯的一个错误是在创建一个新项目时错误选择了项目类
型。例如,开发人员可能选择创建一个新的Win32应用程序项目,但创建的入口函数是main。
生成应用程序时,会报告一个链接器错误,因为Win32 应用程序项目会设置
/SUBSYSTEM:WINDOWS链接器开关,但WinMain或wWinMain函数并不存在。此时,开发
人员有以下4个选择。
¾ 把main函数改为WinMain。这通常不是最佳方案,因为开发人员真正希望的可能是创
建一个控制台应用程序。
¾ 在Visual C++中创建一个新的Win32控制台应用程序项目,然后在新项目中添加现有的
源代码模块。这个办法过于繁琐。它相当于一切都从头开始,而且必须删除原来的项目
文件。
¾ 在项目属性对话框中,定位到Configuration Properties(配置属性)/Linker(链接器)/System
(系统)/SubSystem (子系统)选项,把/SUBSYSTEM:WINDOWS 开关改为
/SUBSYSTEM:CONSOLE,如图4-2所示。这是最简单的解决方案,很少有人知道这个
窍门。
¾ 在项目属性对话框中,删除/SUBSYSTEM:WINDOWS开关。这是我个人最偏爱的选项,
因为它能提供最大的灵活性。现在,链接器将根据源代码中实现的函数来执行正确的操
作。用Visual Studio创建一个新的Win32应用程序或Win32控制台应用程序项目时,这才
应该是真正的默认设定啊!
图4-2 在项目的属性对话框中,为一个项目选择一个CUI子系统
所有C/C++运行时启动函数所做的事情基本都是一样的,区别在于它们要处理的是ANSI字符串,
还是Unicode字符串;以及在初始化C运行库之后,它们调用的是哪一个入口函数。Visual C++
自带C运行库的源代码。可以在crtexe.c文件中找到4个启动函数的源代码。这些启动函数的用途
简单总结如下。
¾ 获取指向新进程的完整命令行的一个指针。
¾ 获取指向新进程的环境变量的一个指针。
¾ 初始化C/C++运行库的全局变量。如果include了StdLib.h,你的代码就可以访问这些变
量。表4-2总结了这些变量。
¾ 初始化C运行库内存分配函数(malloc和calloc)和其他低级I/O例程使用的堆(heap)。
¾ 调用所有全局和静态C++类对象的构造函数。
表4-2 程序可以访问的C/C++运行时全局变量
变量名称 类型 描述和推荐使用的Windows函数
_osver unsigned
int
操作系统的build版本号。例如,Windows Vista RTM为build 6000。所以,_osver的值就
是6000。请换用GetVersionEx。
_winmajor unsigned
int
以十六进制表示的Windows系统的major版本号。对于Windows Vista,该值为6。请换
用GetVersionEx。
_winminor unsigned
int
以十六进制表示的Windows系统的minor版本号。对于Windows Vista,该值为0。请换
用GetVersionEx。
_winver unsigned
int
(_winmajor << 8) + _winminor。请换用GetVersionEx。
__argc unsigned
int
命令行上传递的参数的个数。请换用GetCommandLine。
__argv
__wargv
char
wchar_t
长度为__argc的一个数组,其中含有指向ANSI/Unicode字符串的指针。数组中的每一项
都指向一个命令行参数。注意,如果定义了_UNICODE,__argv就为NULL;如果没有
定义,则__wargv为NULL。请换用GetCommandLine。
_environ
_wenviron
char
wchar_t
一个指针数组,这些指针指向ANSI/Unicode字符串。数组中的每一项都指向一个环境
字符串。注意,如果没有定义_UNICODE,_wenviron就为 NULL;如果已经定义了
_UNICODE , _environ 就为 NULL 。请换用GetEnvironmentStrings 或
GetEnvironmentVariable。
_pgmptr
_wpgmptr
char
wchar_t
正在运行的程序的名称及其ANSI/Unicode完整路径。注意,如果已经定义了
_UNICODE,_pgmptr就为NULL。如果没有定义_UNICODE,_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);
如果没有定义_UNICODE,其调用过程将如下所示:
GetStartupInfo(&StartupInfo);
int nMainRetVal = WinMain((HINSTANCE)&__ImageBase, NULL, pszCommandLineAnsi,
(StartupInfo.dwFlags & STARTF_USESHOWWINDOW)
? StartupInfo.wShowWindow : SW_SHOWDEFAULT);
注意,_ImageBase是一个链接器定义的伪变量,表明执行体文件映射到应用程序内存中的什么
位置。后面的“进程实例句柄”一节将进一步讨论这个问题。
如果你写了一个_tmain函数,而且定义了_UNICODE,那么其调用过程如下:
int nMainRetVal = wmain(argc, argv, envp);
如果没有定义_UNICODE,调用过程如下:
int nMainRetVal = main(argc, argv, envp);
注意,用Visual Studio向导生成应用程序时,CUI应用程序的入口中没有定义第三个参数(环境
变量块),如下所示:
int _tmain(int argc, TCHAR* argv[]);