Windows程序设计:进程
进程是一个具有一定独立功能的程序关于某个数据集合的一次运行活动,在Windows编程环境下,主要由两大元素组成:
? 一个是操作系统用来管理进程的内核对象。操作系统使用内核对象来存放关于进程的核心信息。
? 另一个是地址空间,在地址空间囊括了所有可执行模块和动态链接库的代码和数据。动态内存分配的空间也在其中,典型代表是线程堆栈和堆内存分配。
1进程与线程
进程是不活泼的。当进程开始工作的时候,它必须启动一个在当前进程上下文中的线程来执行工作流程。这个线程被称为主线程,它负责执行包含在进程的地址空间中的代码。同时,每一个进程可能包含多个线程,所有线程都在并发执行进程地址空间中的代码。这就意味着,每个线程都有它自己的一组CPU 寄存器和它自己的堆栈。每个进程至少拥有一个线程,来执行当前进程的地址空间中的代码。如果没有线程来执行进程的地址空间中的代码,这就意味着进程的生命周期已经完结,系统就会将已经分配给该进程和它的地址空间全部撤销收回。
如果,我们希望当前进程中的所有线程都能并发运行,那么,操作系统就要为每一个线程分配相应的CPU时间。它通过轮询方式为线程提供时间片(称为量程),给用户造成一种假象——似乎所有线程都是同时运行的一样。如果计算机拥有多个CPU(例如Core2多核处理器),那么操作系统就要使用更加复杂的算法来实现CPU 上线程的负载均衡,以保证多线程程序可以有效的响应客户需求。
综上所述,当创建一个进程时,系统同时会自动创建当前进程的第一个线程。这个线程称为主线程。然后,该线程可以创建其他的线程,而这些线程又能创建更多的线程。
2使用CreateProcess创建进程
Windows操作系统为用户提供了C r e a t e P r o c e s s 函数用于进程的创建,该函数的签名如下:
BOOL CreateProcess(
PCTSTR pszApplicationName,
PTSTR pszCommandLine,
PSECURITY_ATTRIBUTES psaProcess,
PSECURITY_ATTRIBUTES psaThread,
BOOL bInheritHandles,
DWORD fdwCreate,
PVOID pvEnvironment,
PCTSTR pszCurDir,
PSTARTUPINFO psiStartInfo,
PPROCESS_INFORMATION ppiProcInfo);
当程序员调用CreateProcess创建进程时,系统就会创建一个进程内核对象,并将其初始使用计数置为1。这个内核对象不是进程本身,而是操作系统管理进程时使用的一个的数据结构。我们可以把它看做是进程的全部统计信息的汇总。内核对象创建完成之后,系统会为新进程创建一个虚拟地址空间,并将可执行文件或当前进程所需要的DLL文件的代码和数据加载到该进程的地址空间中。
接下来,系统为新进程的主线程创建一个线程内核对象(其使用计数为1 )。注意,这是线程的内核对象,它与进程内核对象类似,是操作系统用来管理线程的数据结构。通过执行C/C++运行时启动代码,该主线程便开始运行,它最终调用Wi n M a i n 或 m a i n 函数。如果上述的两个步骤顺利完成,C r e a t e P r o c e s s函数将返回T R U E ,宣告新进程创建成功。
值得我们注意的是:在进程初始化完成之前,CreateProcess函数就会返回TRUE。试想一下:当操作系统加载程序试图找出所有需要的DLL,如果一个DLL无法找到,或者未能正确地初始化,那么该进程就终止运行。由于CreateProcess已经向用户程序返回T R U E ,因此父进程不知道出现的任何初始化问题。这在实际编程中需要十分小心。CreateProcess函数仅仅意味着操作系统对于新建进程的管理对象已经创建成功,它不能保证新创建的进程可以正常运行(正常运行取决于进程本身的代码是否满足运行条件,与创建者无关)。
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从主线程的入口点函数返回
在程序中,我们应当力求只有当主线程的入口点函数返回时,它的进程才终止运行。这是保证操作系统分配给线程的所有资源能够得到正确清除的唯一办法。让主线程的入口点函数返回,可以确保下所有的资源都得到恰当的处理:
? 该线程创建的任何C++对象的析构函数将被调用,分配的资源被正确撤消。
? 操作系统将能正确地释放该线程的堆栈内存。
? 系统将进程的退出代码(在进程的内核对象中维护)设置为入口点函数的返回值。
? 系统将进程内核对象的返回值计数递减1。
5慎用ExitProcess函数:C++是运行在C语言之上的
当进程中的某个线程调用E x i t P r o c e s s函数时,进程便终止运行:
VOID ExitProcess(UINT fuExitCode);
该函数用于终止进程的运行,并将进程的退出代码设置为参数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之后又增加了什么代码,那么添加代码将不可能获得执行的机会。
当主线程的入口点函数( WinMain、wWinMain、main或wmain)返回时,它将返回给C/C++运行时启动代码,它能正确地清除该进程使用的所有的C运行时资源。当C运行时资源被释放之后,C运行时启动代码就显式调用E x i t P r o c e s s函数,并将入口点函数返回的值传递给它。因此,我们只需要在主线程入口点返回,就能终止当前进程。请注意,进程中运行的任何其他线程都随着进程的终止而终止。
Windows的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或者仅仅是返回,那么应用程序的主线程将停止运行。此时,不难发现,ExitProcess仅仅退出了当前线程,而非当前进程。如果当前进程中至少有一个线程还在运行,该进程将不会终止运行。
在调用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++运行时可能无法被全部清除,例如:
#include <windows.h>
#include <stdio.h>
class CMyObject
{
public:
CMyObject()
{
printf("Constructor\r\n");
}
~CMyObject()
{
printf("Destructor\r\n");
}
};
CMyObject g_GlobalObj;
void main()
{
CMyObject LocalObj;
ExitProcess(0);
}
接下来,我们转到对应的文件目录下面,在项目上右键弹出菜单,选中在文件资源管理器中打开文件夹,
获得文件的目录,
然后打开命令行工具,
使用cd 命令,切换到debug文件夹,运行exe文件,
我们将会看到:
Constructor
Constructor
这个应用创建了两个对象,一个是全局对象,另一个是局部对象。不过我们一定不会看到Destructor这个单词出现, 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(0)这行代码,那么再次运行程序,我们可以得到如下结果:
Constructor
Constructor
Destructor
Destructor
只要让主线程的入口点函数返回, 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时,进程可能将继续运行,但是可能会泄漏内存或其他资源。
6进程终止后操作系统的工作
当进程被终止时,操作系统将完成下列工作:
1) 进程中剩余的所有线程将全部被终止运行。
2) 进程指定的所有用户对象和GDI对象均被释放,所有内核对象均被关闭(如果没有其他进程关联到这些句柄,那么这些内核对象将被撤消。但是,如果存在其他进程关联到这些句柄, 内核对象将不会撤消)。
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) 进程内核对象的状态变成受信的状态。进程中的其他线程被挂起,直到进程终止运行。
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来获得目前已经撤消的进程的退出代码:
BOOL GetExitCodeProcess(HANDLE hProcess,
PDWORD pdwExitCode);
该函数就是通过查看进程的内核对象(由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。如果进程已经终止运行,便返回数据的退出代码值。
因此,为了保证内核对象被正确关闭,应该及时调用C l o s e H a n d l e函数,告诉系统你对进程的统计数据已经不再感兴趣,以便操作系统及时回收内核对象。如果进程已经终止运行,C l o s e H a n d l e将递减内核对象的使用计数,并将它释放。
相关视频可以观看
http://edu.51cto.com/course/12840.html
原文地址:http://blog.51cto.com/xiacaojun/2322509