第二部分:工作機理
第一章:進程
上一章介紹了內(nèi)核對象,這一節(jié)開始就要不斷接觸各種內(nèi)核對象了。首先要給大家介紹的是進程內(nèi)核對象。進程大家都不陌生,它是資源和分配的基本單位,而進程內(nèi)核對象就是與進程相關聯(lián)的一個數(shù)據(jù)結(jié)構。操作系統(tǒng)內(nèi)核通過它管理進程,也就是操作系統(tǒng)原理上介紹的進程控制塊(PCB)。舉個例子,它就相當于每個學生都有的學籍,學校管理我們都是通過學籍,什么記過了,處分了,開除學籍了,都是在學籍上做文章。
進程一般被定義為一個正在運行的程序的一個實例,它由兩部分組成:
1:內(nèi)核對象,操作系統(tǒng)用它來管理進程。內(nèi)核對象也是系統(tǒng)保存進程統(tǒng)計信息的地方。
2:一個地址空間,其中包含所有可執(zhí)行文件或DLL模塊的代碼和數(shù)據(jù)。
Windows支持兩種類型的應用程序:GUI程序和CUI程序。前者是我們經(jīng)常接觸的,具有窗口外觀的窗口應用程序。后者是控制臺應用程序。在使用vc來開發(fā)應用程序時,會設置各種鏈接器開關。鏈接器根據(jù)這些開關將子系統(tǒng)的正確類型嵌入最終生成的可執(zhí)行文件。對于CUI程序這個開關是/SUBSYSTEM:CONSOLE。對于GUI程序,則是/SUBSYSTEM:WINDOWS。
這些開關會告訴鏈接器在鏈接時鏈接什么入口函數(shù)。對于GUI程序它的入口點函數(shù)時WinMain,CUI程序是main。
有人以為入口函數(shù)就是程序執(zhí)行的開始,其實這是不正確的。在入口點函數(shù)之前還有一個被稱為啟動函數(shù)的函數(shù)。該函數(shù)用來初始化C/C++運行庫、構造全局和靜態(tài)的C++對象等。
根據(jù)應用程序類型的不同,啟動函數(shù)也不一樣。ANSI字符集下,GUI程序的啟動函數(shù)是WinMainCRTStartup,入口函數(shù)是WinMain。CUI的啟動函數(shù)是mainCRTStartup,入口函數(shù)是main。Unicode字符集下,GUI程序的啟動函數(shù)是wWinMainCRTStartup,入口函數(shù)是wWinMain,CUI的啟動函數(shù)是wmainCRTStartup,入口點函數(shù)時wmain。
我們在寫控制臺下的應用程序時,可以通過argv來引用命令行參數(shù),當時也很疑惑,為什么可以直接用呢?原來都是啟動函數(shù)的功勞。它會在進入入口函數(shù)之前幫我們做其他工作:
1:獲取命令行指針。
2:獲取指向環(huán)境變量的指針
3:初始化C/C++運行庫的全局變量。
4:初始化C運行庫內(nèi)存分配函數(shù)。
5:調(diào)用所有全局和靜態(tài)C++類對象的構造函數(shù)。
完成所有這些工作后,啟動函數(shù)就會調(diào)用應用程序的入口點函數(shù)。入口點函數(shù)返回后啟動函數(shù)獲得入口點函數(shù)返回值,并將其傳遞給C運行庫函數(shù)exit。Exit函數(shù)將調(diào)用所有全局和靜態(tài)C++類對象的析構函數(shù)和其他清理工作。然后將入口函數(shù)的返回值傳遞給ExitProcess函數(shù),結(jié)束進程并設置返回值為退出代碼。
加載到進程地址空間的可執(zhí)行文件或是DLL都有一個實例句柄。用以標識它在進程地址空間的位置??蓤?zhí)行文件的實例句柄被當做WinMain函數(shù)的第一個參數(shù)傳入。它實際上是一個內(nèi)存基地址。系統(tǒng)將可執(zhí)行文件的映像加載到進程地址空間中的這個位置。映像加載到哪個地址是由鏈接器決定的。不同的鏈接器使用不同的首先基地址。exe文件和dll都會有一個默認的首選基地址。exe文件是400000,dll是10000000。
為了獲得一個可執(zhí)行文件或dll文件被加載到進程地址空間的位置,可以使用GetModuleHandle函數(shù)。它需要一個以/0結(jié)尾標示可執(zhí)行文件或dll的名字字符串為參數(shù)。當傳入NULL時,此時將會返回主調(diào)進程可執(zhí)行文件的基地址,即使此時代碼在一個dll文件中仍然是這樣。如果此時代碼在dll中執(zhí)行,我們想何知道此時代碼正在什么模塊中運行,這可以通過GetModuleHandleEx得到。將GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS作為它的第一個參數(shù),再將當前函數(shù)的地址作為它的第二個參數(shù),函數(shù)執(zhí)行完畢,最后一個參數(shù)將保存出入的函數(shù)所在dll的基地址。
系統(tǒng)在創(chuàng)建進程時會傳給他一個命令行,這個命令行總是非空,因為它至少存儲有可執(zhí)行文件的名稱。C運行庫的啟動代碼在執(zhí)行一個GUI應用程序時,會調(diào)用windows函數(shù)GetCommandLine來獲得進程的完整命令行,它忽略可執(zhí)行文件名稱,然后將剩余部分傳給WinMain的pszCmdLine參數(shù)。
每個進程都有一個與它相關聯(lián)的環(huán)境塊。用以定義工作環(huán)境、保存有用信息,使系統(tǒng)獲得相關設置。應用程序經(jīng)常利用環(huán)境變量讓用戶精細其行為。用戶創(chuàng)建一個環(huán)境變量并進行初始化,此后應用程序運行時會正在環(huán)境塊中查找變量,如果找到變量就會解析變量的值,并調(diào)整自己的行為。它所占用的內(nèi)存是在進程地址空間內(nèi)分配的。同樣調(diào)用GetEnvironmentStrings函數(shù)可以獲得完整的環(huán)境塊。通常子進程會繼承一組環(huán)境變量,這些環(huán)境變量和父進程的環(huán)境變量相同,父進程可以控制那些環(huán)境變量允許子進程繼承。注意子進程繼承的僅僅是父進程環(huán)境變量的副本,它們不共享同一個環(huán)境塊。GetEnvironmentVariable函數(shù)可以用來判斷一個環(huán)境變量是否存在。
在下圖中我們可以看到形如%USERPROFILE%的字符串,它表示兩個%之間的這部分內(nèi)容是一個可替換的變量。該變量在環(huán)境變量中已經(jīng)被定義。

可以使用SetEnvironmentVariable來添加、刪除或修改一個變量。
Windows不建議使用入口函數(shù)的參數(shù)來訪問命令行或是環(huán)境變量,而應該使用以上介紹的各種函數(shù)。應該將它們當做只讀變量,不要對它們進行修改。
在多處理器的系統(tǒng)中,可以強迫線程在某個cpu上運行,這成為處理器關聯(lián)性。子進程繼承了其父進程的關聯(lián)性。
如果不提供完整的路徑名,windows函數(shù)就會在當前驅(qū)動器的當前目錄查找文件和目錄。如:調(diào)用CreateFile打開一個文件時,如果僅指定文件名,系統(tǒng)將在當前驅(qū)動器和目錄查找該文件。
系統(tǒng)在內(nèi)部跟蹤記錄這一個進程當前驅(qū)動器和目錄,這些信息是以進程為單位來維護的,如果該進程的一個線程更改了當前驅(qū)動器和目錄,則只影響本進程的所有線程。
一個線程可以使用GetCurrentDirectory和SetCurrentDirectory來獲得和設置當前驅(qū)動器和目錄。子進程的當前目錄默認為每個驅(qū)動器的根目錄。如果父進程希望子進程繼承它的當前目錄,就必須在生成子進程之前,添加環(huán)境變量。
使用GetVersionEx可以獲得window系統(tǒng)的版本號。
CreateProcess函數(shù)
接下來進入到本章最重要的知識點:CreateProcess函數(shù)。
該函數(shù)用以創(chuàng)建一個進程:
- Bool CreateProcess(
-
- PCTSTR pszApplicationName,
-
- PTSTR pszCommandLine,
-
- PSECURTITY_ATTRIBUTES psaProcess,
-
- PSECUTRITY_ATTRIBUTE psaThread,
-
- Bool hInheritHandles,
-
- DWROD fdwCreate,
-
- PVOID pvEnvironment,
-
- PCTSTR pszCurDir,
-
- PSTARTUPINFO psiStartInfo,
-
- PPROCESS_INFORMATION ppiProcInfo
-
- );
當此函數(shù)被調(diào)用時,系統(tǒng)將首先創(chuàng)建一個進程對象,其初始使用計數(shù)為1。進程內(nèi)核對象并不是進程本身,而是操作系統(tǒng)用來管理這個進程的一個數(shù)據(jù)結(jié)構。此后系統(tǒng)為新進程創(chuàng)建一個虛擬地址空間,并將可執(zhí)行文件的代碼及數(shù)據(jù)加載到進程的地址空間。然后系統(tǒng)會新進程的主線程創(chuàng)建一個線程內(nèi)核對象,也將其使用計數(shù)設為1。和進程內(nèi)核對象一樣它也是操作系統(tǒng)用以管理線程的數(shù)據(jù)結(jié)構。主線程首先會執(zhí)行C/C++運行時的啟動函數(shù),啟動函數(shù)調(diào)用入口函數(shù)。進程被創(chuàng)建成功后CreateProcess返回true,函數(shù)返回前CreateProcess可能還沒有完全初始化好。
psaApplicationName和pszCommandLine分別指定新進程要使用的可執(zhí)行文件稱和要傳給新進程的命令行字符串。
注意此處的pszCommandLine是非常量字符串。傳入常量字符串將會導致訪問違規(guī),因為在內(nèi)部CreateProcess會修改傳入的命令行字符串,返回時再將這個字符串還原。
所以一下代碼是錯誤的:
STARTUPINFO si={sizeof(si)};
PROCESS_INFORMATION pi;
CreateProcess(NULL,TEXT
(“NOTEPAD”),NULL,NULL,FALSE,0,NULL,NULL,&si,&pi);
因為TEXT("NOTEPAD")是常量字符串,當CreateProcess試圖修改字符串會引起訪問違規(guī)。解決方法是將TEXT("NOTEPAD")放在一個緩沖區(qū)內(nèi):
- STARTUPINFO si={sizeof(si)};
-
- PROCESS_INFORMATION pi;
-
- TCHAR cmdLine[200]=TEXT("NOTEPAD");
-
- CreateProcess(NULL,cmdLine,NULL,NULL,FALSE,0,NULL,NULL,&si,&pi);
-
-
-
-
-
-
這一點要特別注意,很容易出錯?。。。∵@也有個例外,windows vista以及win7的ANSI版本以上是不會發(fā)生訪問違規(guī)的,因為它們會為命令行創(chuàng)建一個臨時副本。
在解析命令行時,CreateProcess會檢查字符串中第一個標記,假定此標記就是我們想運行的可執(zhí)行文件名稱。如果可執(zhí)行文件沒有擴展名,就會默認是.exe擴展名。CreateProcess會在以下目錄下搜索可執(zhí)行文件:
1:主調(diào)進程.exe文件所在目錄。
2:主調(diào)進程的當前目錄。
3:windows系統(tǒng)目錄。即System32目錄。
4:windows目錄。
5:PATH環(huán)境變量中列出的目錄。
如果為可執(zhí)行文件制定完整路徑,系統(tǒng)就會按指定路徑尋找。
以上情況在pszApplicationName為NULL時才發(fā)生。當然也可以在pszApplicationName傳遞可執(zhí)行文件名稱,此時必須指定擴展名,否則進程不會被創(chuàng)建。系統(tǒng)會按照此處指定的路徑尋找,如沒有指定完整路徑,系統(tǒng)會假定文件位于當前目錄。如找不到,函數(shù)調(diào)用失敗。
當pszApplicationName指定文件名,pszCommandLine參數(shù)中的內(nèi)容也會作為新進程的命令行傳給它。
如:
- STARTUPINFO si={sizeof(si)};
-
- PROCESS_INFORMATION pi;
-
- TCHAR cmdLine[200]=TEXT("WORDPAD a.txt");
-
- CreateProcess(TEXT("C:\\windows\\system32\\NOTEPAD.exe"),
-
- cmdLine,NULL,NULL,FALSE,0,NULL,NULL,&si,&pi);
命令行是“WORDPAD a.txt”,記事本程序?qū)⒃儐枺?span style="font-family:Times New Roman">a.txt不存在是否創(chuàng)建a.txt文件。第一個參數(shù)WORDPAD應該是作為程序名稱傳入的。
為了創(chuàng)建一個新的進程,系統(tǒng)必須創(chuàng)建一個進程內(nèi)核對象和一個線程內(nèi)核對象,由于這些都是內(nèi)核對象,所以父進程必須將安全屬性關聯(lián)到這兩個對象上??梢酝ㄟ^psaProcess和psaThread為進程和線程安全對象指定默認的安全描述符??梢远紴樗鼈冎付?span style="font-family:Times New Roman">NULL,使用默認的安全屬性,也可以分配并初始化兩個SECURITY_ATTRIBUTES結(jié)構,以便創(chuàng)建安全權限并將它們分配給進程和線程對象。
fdwCreate參數(shù)影響新進程創(chuàng)建的方式。如:指定CREATE_SUSPENDEd標識讓系統(tǒng)在創(chuàng)建新進程時掛起其主線程。這樣父進程就可以修改子進程地址空間的內(nèi)存、更改主線程優(yōu)先級或是將其添加到作業(yè)中。修改完后可以調(diào)用ResumeThread來允許子進程執(zhí)行代碼。傳入0 表示創(chuàng)建進程后立即運行。多個標志位可以組合使用。
pvEnvironment參數(shù)指定一塊內(nèi)存,包含新進程要使用的環(huán)境字符串。但多數(shù)情況下都是傳入NULL,表示子進程要繼承父進程使用的一組環(huán)境字符串。也可以使用GetEnvironmentStrings函數(shù),此函數(shù)獲取主調(diào)進程正在使用的環(huán)境字符串地址,當我們?yōu)?span style="font-family:Times New Roman">pvEnvirtonment傳入NULL時,CreateProcess就是調(diào)用這個函數(shù)。不使用這塊內(nèi)存時應該使用FreeEnvironmentStrings函數(shù)。
pszCurDir允許父進程設置子進程的當前驅(qū)動器和目錄。如果為NULL,新進程的工作目錄與父進程的一樣。
psiStartInfo參數(shù)指向一個STARTUPINFO結(jié)構。該結(jié)構包含很多成員。Windows在創(chuàng)建新進程的時候使用它們,但是大多數(shù)應用程序僅僅使用它們的默認值。因此我們要做的最起碼的工作就是將此結(jié)構的所有成員都初始化為0,將cb成員設為此結(jié)構的大小,如:
STARTUPINFO si={sizeof(si)};
此時除cb成員外,其他成員均為0。不能僅僅si.cb=sizeof(si);因為此時其他成員的值都是垃圾數(shù)據(jù)。
ppiProcInfo參數(shù)指向PROCESS_INFORMATION結(jié)構,CreateProcess在返回時會初始化這個結(jié)構。
Typedef struct _PROCESS_INFORMATION
{
HANDLE hProcess;
HANDLE hThread;
HANDLE dwProcessId;
HANDLE dwThreadId;
}PROCESS_INFORMATION;
CreateProcess創(chuàng)建的進程和線程對象將通過它返回。創(chuàng)建時系統(tǒng)會為每個對象指定一個初始的使用計數(shù)1,CreateProcess返回時由于PROCESS_INFORMATION結(jié)構中再次引用了進程和線程內(nèi)核對象 此時它們的使用計數(shù)都變?yōu)?span style="font-family:Times New Roman">2??梢岳斫鉃檫M程和線程實例本身也占有一個計數(shù)。當它們結(jié)束運行時這個使用計數(shù)被遞減1。
此時如果系統(tǒng)要釋放進程對象,1:進程必須終止,此時使用計數(shù)遞減1。2:父進程必須調(diào)用CloseHandle,使用計數(shù)再次減去1。線程類似。
因此為了使不再使用的內(nèi)核對象能夠得到釋放,一定要在不使用時調(diào)用CloseHandle關閉對句柄的引用。所有對該句柄的引用都被關閉后,當進程或線程終止時它們關聯(lián)度的內(nèi)核對象才能夠被釋放。
CreateProcess還會為進程和線程分配一個ID號。進程和線程分享同一個號碼池。這意味著它們不可能相同。一個對象的ID不可能分配到0,因為windows任務管理器將進程ID 0與系統(tǒng)空閑進程關聯(lián)。該進程代表未被真實使用的cpu使用率。
dwProcessId和dwThreadId成員就是存儲進程和線程的ID。使用GetCurrentProcessId可以獲得當前進程的ID。GetCurrentThreadId來獲得當前正在運行的線程的ID。另外使用GetProcessId和GetThreadId可以獲得指定句柄對應的進程和線程的ID。使用GetProcessIDOfThread可以獲得某句柄關聯(lián)的線程所在進程的ID。
由于ID可能會立即重用。也就是說當我們獲得某個進程的ID并保存后,此后在使用時有可能出現(xiàn)它已經(jīng)被釋放了。此時此ID就對應著其他進程了。避免這種情況的唯一方法就是:保證進程或線程對象不被銷毀。
進程終止
進程可以通過三種方式終止:
1:主線程從入口函數(shù)返回。
2:進程中的一個線程調(diào)用ExitProcess。
3:另一個進程中的線程調(diào)用TerminateProcess。
在以上介紹的三種方式中僅有第一種,當主線程從入口函數(shù)返回才保證主線程的所有資源都會被正確清理。
清理操作包括:
1:調(diào)用所有在本進程內(nèi)使用的任何C++對象的析構函數(shù)。
2:釋放各個線程線程棧使用的內(nèi)存。
3:進程的退出代碼被設為入口函數(shù)的返回值。
4:進程內(nèi)核對象使用計數(shù)遞減1。
正常情況下入口點函數(shù)會返回到啟動函數(shù),啟動函數(shù)將正確清理進程使用的所有C運行時資源,清理之后啟動代碼顯式調(diào)用ExitProcess并將入口函數(shù)返回值傳給它。這也是為什么只需從入口函數(shù)返回卻可以終止整個進程的原因。
進程的一個線程調(diào)用ExitProcess可以終止本進程。其后的別的代碼將不會被執(zhí)行。
與ExitProcess相類似的還有ExitThread,它會導致一個線程終止。在創(chuàng)建線程時常出現(xiàn)這種情況:子線程還沒有怎么執(zhí)行程序就已經(jīng)結(jié)束了,這有可能是在創(chuàng)建完線程后,主線程沒有調(diào)用WaitForSingleObject之類的函數(shù),主線程創(chuàng)建完其他線程后就返回到啟動函數(shù)函數(shù)返回整個進程被終止。這一點很容易出錯。
調(diào)用ExitProcess或是ExitThread會導致進程或線程當場終止運行,再也不會返回到啟動函數(shù),清理工作(C++對象的析構)當然沒法執(zhí)行。雖然最終隨著進程的結(jié)束,該進程內(nèi)所有線程所使用的資源都會被釋放,但是應該避免調(diào)用這些函數(shù),它們阻止了C++對象析構函數(shù)對善后工作的處理。順便提下,如果在主線程調(diào)用ExitThread,雖然主線程當場終止,但是如果進程內(nèi)還有其他線程,則進程不會終止。
如以下代碼:
- #include<windows.h>
-
- #include<iostream>
-
- DWORD ThreadProc(PVOID)
-
- {
-
- int i=0;
-
- int j=0;
-
-
-
- while(i<1000000)
-
- {
-
- i++;
-
- while(j<10000)
-
- j++;
-
- std::cout<<i<<","<<j<<std::endl;
-
- }
-
- return 0;
-
- }
-
- int main(int argc,char**argv)
-
- {
-
- DWORD id;
-
- CreateThread(NULL,0,(LPTHREAD_START_ROUTINE)ThreadProc,NULL,0,&id);
-
- ExitThread(0);
-
- return 0;
-
- }

ExitProcess,ExitThread只能由本進程的其他線程調(diào)用。而TerminateProcess和TerminateThread卻可以由任何其他進程的線程調(diào)用。它的第一個參數(shù)指定要終止進程的句柄。這種情況下應用程序得不到自己要被殺死的通知,也不能阻止自己被殺死,當然也無法為自己準備好后事(得不到清理)。例如已經(jīng)修改的文件沒有刷新到磁盤上。
但要明白進程終止后屬于它的任何東西都會被釋放。TerminateProcess是異步的,此函數(shù)調(diào)用后我們并不能保證進程已經(jīng)被強行終止了。要確定進程是否終止可以調(diào)用WaitForSingleObject函數(shù),并傳入進程句柄。
進程終止時進行的操作。
一個進程終止時,系統(tǒng)會依次執(zhí)行以下操作:
1:終止進程中遺留的任何線程。
2:釋放進程分配的所有用戶對象,關閉所有內(nèi)核對象。如果它們的使用計數(shù)變?yōu)?span style="font-family:Times New Roman">0,內(nèi)核對象將會釋放。
3:將進程的退出代碼從STILL_ACTIVE變?yōu)閭鹘oExitProcess或是TerminateProcess的參數(shù)存儲在內(nèi)核對象中。
4:進程內(nèi)核對象變?yōu)橐挥|發(fā)狀態(tài)。這也是為什么其他線程可以掛起他們自己直至另一個進程終止運行。
5:進程內(nèi)核對象的使用計數(shù)遞減1。
進程內(nèi)核對象的生命期至少能像進程本身一樣長。當進程終止時如果系統(tǒng)中還有另一個進程打開了這個進程的內(nèi)核對象的句柄,進程內(nèi)核對象的使用計數(shù)就不會變?yōu)?span style="font-family:Times New Roman">0。當父進程忘記關閉子進程的句柄時往往發(fā)生這種情況。
進程終止了內(nèi)核對象還沒有被釋放,這樣做有用嗎?當然有用??!即使進程終止了,存儲在內(nèi)核對象的信息也有可能被使用,如我們想知道進程占用了多少Cpu時間,或是想獲得它的退出代碼。GetExitCodeProcess此函數(shù)會查找進程內(nèi)核對象并從內(nèi)核對象的數(shù)據(jù)結(jié)構中取出退出代碼。任何時候都可以調(diào)用此函數(shù)。如此時進程正在運行那么將會得到STILL_ALIVE。
WaitForSingleObject將會掛起當前線程,知道它所等待的對象變?yōu)橐延|發(fā)狀態(tài)。進程或線程對象在終止時就會變成已觸發(fā)狀態(tài)。
|