Windows核心编程 第四章 进程(下)

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函数时,进程便终止运行:

             VOID ExitProcess(UINT fuExitCode)

    该函数用于终止进程的运行,并将进程的退出代码设置为 f u E x i t C o d eE 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 nw Wi n M a i nm a i nw 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 sE x i t T h r e a d可使进程或线程在函数中就终止运行。就操作系统而言,这很好,进程或线程的所有操作系统资源都将被全部清除。但是, C / C + +应用程序应该避免调用这些函数,因为C / C + +运行期也许无法正确地清除。请看下面的代码:

 

 

    只要让主线程的进入点函数返回, C / C + +运行期就能够执行它的清除操作,并且正确地撤消任何或所有的C + +对象。顺便讲一下,这个说明不仅仅适用于 C + +对象。C + +运行期能够代表进程执行许多操作,最好允许运行期正确地将它清除。

    注意 显式调用E x i t P r o c e s sE 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函数也能够终止进程的运行:

BOOL TerminateProcess(

    _In_ HANDLE hProcess,

    _In_ UINT uExitCode

    );

该函数与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。终止运行的进程绝对得不到关于它将终止运行的任何通知,因为应用程序无法正确地清除,并且不能避免自己被撤消(除非通过正常的安全机制) 。例如,进程无法将内存中它拥有的任何信息迅速送往磁盘。

  虽然进程确实没有机会执行自己的清除操作,但是操作系统可以在进程之后进行全面的清除,使得所有操作系统资源都不会保留下来。这意味着进程使用的所有内存均被释放,所有打开的文件全部关闭,所有内核对象的使用计数均被递减,同时所有的用户对象和 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 sTe 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来获得目前已经撤消的进程的退出代码:

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。如果进程已经终止运行,便返回数据的退出代码值。

也许你会认为,你可以编写代码,通过定期调用 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章) 。

    如果想创建新进程,让它进行一些操作,并且等待结果,可以使用类似下面的代码:

 

    STARTUPINFO si = {sizeof(si)};

    PROCESS_INFORMATION pi;

    TCHAR szCommandLine[] = TEXT("NOTEPAD");

    BOOL fSuccess = CreateProcess(NULL ,szCommandLine ,NULL ,NULL ,

    FALSE ,0 ,NULL ,NULL ,&si ,&pi);

if(fSuccess)

{

CloseHandle(pi.hThread);

WaitForSingleObject(pi.hProcess ,INFINITE);

DWORD dwExitCode;

GetExitCodeProcess(pi.hProcess ,&dwExitCode);

CloseHandle(pi.hProcess);

}

   第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来关闭它与新进程及它的主线程之间的句柄。下面的代码示例显示了如何创建新进程以及如何让它以独立方式来运行:

CloseHandle(pi.hThread); CloseHandle(pi.hProcess);

4.5 枚举系统中运行的进程

    许多软件开发人员都试图为 Wi n d o w s编写需要枚举正在运行的一组进程的工具或实用程序。Windows API原先没有用于枚举正在运行的进程的函数。不过, Windows NT一直在不断更新称为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 95Windows 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 eData Helper的内容。

    如前所述,Windows 95Windows 98没有配备该数据库。不过它们有自己的一组函数,可以用于枚举关于它们的进程和信息。这些函数均在 ToolHelp API中。详细信息请参见Platform SDK文档中的P r o c e s s 3 2 F i r s tP r o c e s s 3 2 N e x t函数。

    更加有趣的是,M i c r o s o f tWindows 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 95Windows 98Windows 2000编写具有公用源代码的工具和实用程序。

 

原文地址:https://www.cnblogs.com/csnd/p/12062269.html