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 e。E 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 n、w Wi n M a i n、m a i n或w 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 s或E 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 s和E 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 s或Te 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 95和Windows 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 95和Windows 98沒有配備該數據庫。不過它們有自己的一組函數,可以用于枚舉關于它們的進程和信息。這些函數均在 ToolHelp API中。詳細信息請參見Platform SDK文檔中的P r o c e s s 3 2 F i r s t和P r o c e s s 3 2 N e x t函數。
? ? 更加有趣的是,M i c r o s o f t的Windows 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 95、Windows 98和Windows 2000編寫具有公用源代碼的工具和實用程序。
?
總結
以上是生活随笔為你收集整理的Windows核心编程 第四章 进程(下)的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Windows核心编程 第四章 进程(中
- 下一篇: Intel汇编程序设计-整数算术指令(上