讀Windows核心編程 - 4

        進程分爲兩部分,一個是操作系統用來管理進程的內核對象,一個是地址空間。進程是不活潑的,活潑的是線程,每個線程都有它自己的一組CPU寄存器和它自己的堆棧。

        Windows支持兩種應用程序,一種是GUI(基於圖形用戶界面),還有一種CUI(基於控制檯用戶界面)。Windows應用程序必須有一個在應用程序啓動運行時的進入點函數。一共有4個:WinWain、wWinMain、main、wmain。操作系統實際上不直接調用我們編寫的進入點函數。它調用的是C/C++運行期啓動函數,其對應的函數也有四個:WinMainCRTStartup、wWinMainCRTStartup、mainCRTStartup、wmainCRTStartup。當我們用Visual C++創建一個應該程序項目時,Visual C++會根據創建的程序的類型指定鏈接程序開關,如果是GUI,鏈接程序的開關是/SUBSYSTEM:WINDOWS,如果是CUI,鏈接程序的開關是/SUBSYSTEM:CONSOLE。一個新手常見的錯誤是,創建了一個win32的應用程序,但是創建了一個進入點函數mian,這樣因爲在鏈接程序中的開關已經設定爲/SUBSYSTEM:WINDOW,會產生一個鏈接錯誤。最好的辦法是把鏈接程序開關刪除,這樣程序就可以自動的確定應該程序應該鏈接到哪個子系統。

C/C++運行期啓動函數功能歸納如下:

1. 檢索指向新進程的完整命令行的指針

2. 檢索指向新進程的環境變量指針

3. 對C/C++運行期的全局變量進行初始化,比如:_pgmptr - 正在運行的程序的全路徑和名字

4. 對所有全局和靜態C++對象調用構造函數

當進入點返回後,啓動函數便調用C運行時的exit函數。Exit函數負責下面的操作:

1. 調用由_onexit函數的調用而註冊的任何函數

2. 爲所有的全局和靜態C++對象調用析構函數

3. 調用系統ExitProcess函數

        在我們調用LoadIcon這樣的函數時,通常需要指定一個HINSTANCE(HMODULE)參數來指明哪個文件(可執行文件還是DLL文件)包含你想加載的資源。這個參數就是WinMain中的hinstExe參數,該參數實際值是系統將可執行文件加載到進程地址空間時使用的基本地址空間,而這個基地址是由鏈接程序決定的。調用GetModuleHandle函數可以返回可執行文件或DLL文件加載到進程的地址空間時所用的句柄/基地址,參數傳一個以0結尾的字符串。如果傳遞NULL就得到可執行文件的基地址,這正是C運行期函數調用WinMain時執行的操作。這個函數注意兩點:1. 它只可以獲得本進程的地址空間。2. 如果傳遞NULL,在DLL中調用也返回可執行文件的基地址。

        每個進程都有一個與它相關的環境塊。環境塊是進程地址空間中分配的一個內存塊。形式像這個樣子:VarName=VarVaule,注意:等於號兩邊的空格都被考慮在內。如果通過註冊表修改了環境變量,需要再次登錄纔能有效。如果想通過註冊表修改,並讓有關應用程序更新他們的環境塊,可以調用如下代碼:

SendMessage(HWND_BROADCAST, WM_SETTINGCHANGE, 0, (LPARAM) TEXT("Environment"));

子進程可以繼承一組與父進程相同的環境變量。但是,父進程能夠控制子進程繼承什麼樣的環境變量。這裏指的繼承是複製,也就是子進程之後對環境變量的修改影響不到父進程。通過GetEnvironmentVariable函數可以確定某個環境變量是否存在以及它的值。SetEnvironmentVariable函數則用於添加、刪除、修改環境變量。

        通過SetErrorMode函數可以設置進行的錯誤模式,該模式是指當進程遇到嚴重錯誤時應該如何作出反映,這些錯誤包括磁盤介質故障,未處理的異常,文件查找失敗等。通常子進程繼承父進程的錯誤模式標誌,也可以在CreateProcess函數中傳遞CREATE_DEFAULT_ERROR_MODE防止子進程繼承。

        如果用CreateFile來打開一個文件(不設定全路徑),那麼系統就會在當前驅動器當前目錄中查找該文件。系統總是在內部保持對進程的當前驅動器和目錄的跟蹤。這些信息是由進程維護的。通過GetCurrentDirectory和SetCurrentDiretory可以獲得和設置進程的當前驅動器和目錄。有些操作系統支持多個驅動器的當前目錄的處理,例如,進程擁有下面所示的兩個環境變量: =C:=C:/Utility/Bin        =D:=D:/Program Files, 現在的當前目錄是C:/Utility/Bin,並且你調用CreateFile來打開D:ReadMe.txt,那麼系統查看環境變量=D。因爲=D存在,因此係統試圖從D:/Program File目錄打開該文件。如果不存在,系統就試圖從驅動器D的根目錄打開該文件。子進程的環境塊不會自己繼承父進程的當前目錄。如果想要子進程繼承父進程的當前目錄,該父進程必須創建這些驅動器名的環境變量。調用GetFullPathName,父進程可以獲得它的當前目錄。如:GetFullPathName(TEXT("C:"), MAX_PATH, szCurDir, NULL);

        Windows提供了三個函數可以確定操作系統版本相關的函數:GetVersion,GetVersionEx,VerifyVersionInfo.具體使用方法參看核心編程page56.

CreateProcess:原型如下:

BOOL CreateProcess(
  LPCTSTR lpApplicationName,
  LPTSTR lpCommandLine,
  LPSECURITY_ATTRIBUTES lpProcessAttributes,
  LPSECURITY_ATTRIBUTES lpThreadAttributes,
  BOOL bInheritHandles,
  DWORD dwCreationFlags,
  LPVOID lpEnvironment,
  LPCTSTR lpCurrentDirectory,
  LPSTARTUPINFO lpStartupInfo,
  LPPROCESS_INFORMATION lpProcessInformation
);

 1. lpApplicationName和lpCommandLine

        這兩個參數可執行文件和命令行字符串。注意到一點,lpCommandLine的原型是LPTSTR,這意味着CreateProcess希望你傳遞一個非常量的字符串地址。如果傳進去一個TEXT(“NOTEPAD”)就會出現違規訪問的問題。但是如果傳進去一個ANSI字符串,就不會出現這個問題,因爲系統已經制作了一個命令行字符串的臨時拷貝。如果lpApplication不是NULL(大多數情況是NULL),那麼可以將包含想運行的可執行文件的字符串地址傳遞給lpApplicationName參數。這裏必須提供文件的擴展名,系統不會自動假設有一個exe擴展名。如果沒用提供全路徑,那麼系統只在當前目錄查找,如果找不到則運行失敗。如果lpApplicationName是NULL,那麼需要爲lpCommandLine提供一個完整的命令行,系統假定字符串中的第一個標記是可執行文件的名字,如果沒有指定擴展名,系統假定擴展名爲exe。如果在lpCommandLine中沒有指定全路徑,那麼系統依次在以下目錄中查找:包含調用進程的.exe文件的目錄-->調用進程的當前目錄-->Windows系統目錄-->Windows目錄-->PATH環境變量中列出的目錄。

2. lpProcessAttributes、lpThreadAttributes和bInheritHandles

        可以使用lpProcessAttributes和lpThreadAttributes參數分別設定進程對象和線程對象的安全性。如果傳遞NULL則賦予默認安全性描述符。否則需要傳遞一個SECURITY_ATTRIBUTES的數組來創建自己的安全性權限。調用CreateProcess將在父進程的句柄表中新增兩項,進程對象和線程對象,而SECURITY_ATTRIBUTES結構體中的bInheritHandles成員則確定了該對象的可繼承性。比如:Process A中調用了CreateProcess創建進程B,並設定lpProcessAttributes.bInheritHandles = TRUE, lpThreadAttributes.bInheritHandles = FALSE。現在A又調用CreateProcess創建進程C,如果bInheritHandles設置爲FALSE,那麼兩個內核對象都得不到繼承,如果bInheritHandles設置爲TRUE,那麼進程B對象得到繼承,也就是複製到了進程C的句柄表中。而線程B對象得不到繼承。與此同時,進程A的句柄表中有多了進程C和線程C兩項。

3. dwCreationFlags

        這個參數用於規定如何創建進程,可以用OR操作符將多個標誌組合起來。比如,CREATE_SUSPENDED表示新創建的主線程掛起。類似的參數有很多,可以參考核心編程page63。另外dwCreationFlags參數還可以設定進程的優先級類。

4. lpEnviroment

        lpEnviroment參數用於指向包含新進程要使用的環境字符串的內存塊。如果傳遞NULL,子進程將繼承它的父進程正在使用的一組環境字符串。將GetEnvironmentStrings()的返回值傳遞給這個參數效果跟傳遞NULL一樣,該函數獲得調用進程正在使用的環境變量字符串數據塊的地址。當不再需要該內存塊時,應該調用FreeEnvironmentStrings函數將內存塊釋放。

5. lpCurrentDirectory

        這個參數允許父進程設置子進程的當前驅動器和目錄。如果本參數爲NULL,則新進程的工作目錄將與生成新進程的應用程序的目錄相同。如果不傳遞NULL,那麼必須指向包含需要的工作驅動器和工作目錄的以0結尾的字符串。

6. lpStartupInfo

        這是一個結構體,裏面有很多成員,一般情況下只需要把該結構體全部賦爲0,把該結構體的大小賦給其中的一個成員就可以了。這個結構體中有些成員只有在子應用程序創建一個重疊窗口時纔有意義,而另一些成員則只有在子應用程序執行基於CUI的輸入輸出時纔有意義。具體每個成員的意義可參看核心編程page65。最後,應用程序可以調用GetStartupInfo以便獲得由父進程初始化的STARTUPINFO結構的拷貝。子進程可以查看該結構,並根據該結構的成員的值來改變它的行爲特徵。雖然在windows文檔中沒有明確的說明,但是在調用這個函數之前必須像下面這樣對該結構的cb成員進程初始化:STARTUPINFO si = {sizeof(si)}; GetStartupInfo(&si);

7. lpProcessInformation

        結構體PROCESS_INFORMATION一共包含四個成員,進程和線程的句柄以及ID。在創建進程的時候,系統爲每個對象賦予一個初始引用計數1。然後,在CreateProcess返回之前,該函數打開進程對象和線程對象,並將每個對象的與進程相關的句柄放入PROCESS_INFORMATION結構中。當CreateProcess在內部打開這些對象時,每個對象的引用計數變爲2。這意味着如果要使系統釋放進程對象,除了該進程必須終於外,父進程必須調用CloseHandle。線程也同樣。進程ID和線程ID都是獨一無二的標誌符,其他內核對象不能使用相同的ID,另外進程和線程也不能使用相同的ID。如果應用程序使用ID來跟蹤線程或者進程,必須注意一點,就是這些ID可以重複使用,意思是說,如果一個ID爲122的進程對象被釋放,那麼當另外一個進程創建起來時,122就可以被賦給這個進程。所以最好不要用ID,應該定義一個持久性更好的機制,比如內核對象和窗口句柄等。

終止進程的運行:

若要終止進程的運行,可以使用下面四種方法:

1. 主線程的進入點函數返回(最好的方法)

        這種方法是保證所有線程資源能夠得到正確清楚的唯一辦法:

        1. 該線程創建的任何C++對象將能使用他們的析構函數正確的撤銷。

        2. 操作系統能將正確地釋放該線程的堆棧使用的內存。

        3. 系統將進程的退出代碼(在進程的內核對象中維護)設置爲進入點函數的返回值。

        4. 系統將進程的內核對象的引用計數遞減1.

2. 進程中的一個線程調用ExitProcess(應該避免使用)

        正常情況下,當主線程的進入點函數返回時,它將返回給C/C++運行期啓動代碼,它能正確地清楚該清楚使用的所有的C運行時資源。當C運行期資源被釋放以後,C運行期啓動代碼就顯示調用ExitProcess,並將進入點函數的值傳遞給它。注意,調用ExitProcess或ExitThread可使進程或線程在函數中就終止運行。就操作提供而言,就很好,進程或線程的所有操作系統資源都將被全部清楚。但是,C/C++應用程序應該避免使用這些函數,因爲C/C++運行期也許無法正確地清楚。這裏有個問題不是很明白,操作系統不負責C/C++資源的清理?進程結束了,所有的內存都被收回,還有什麼資源得不到釋放的麼???

3. 另一個進程中的線程調用TerminateProcess(應該避免使用)

        與ExitProcess不同的是,這個函數可以終止另一個進程或它自己進程的運行。雖然進程確定沒有機會執行自己的清楚操作,但是操作系統可以在進程之後進行全面的清除,使得所有操作系統資源都不會保留下來。這意味着進程使用的所有的內存都被釋放,所有打開的文件全部關閉,所有內核對象的引用計數均被遞減,同時所有的用戶對象和GDI對象均被撤銷。注意:TerminateProcess是個異步函數,因此無法保證進程被終止。如果想要確切瞭解進程是否已經終止運行,必須調用WaitForSingleObject或者類似的函數,並傳遞進程的句柄。

4. 進程中的所有線程自行終止運行(幾乎不會發生)

當進程終止時出現的情況:

1. 進程中的所有線程被終止

2. 進程指定的用戶對象和GDI對象均被釋放。所有內核對象均被關閉(遞減...)。

3. 進程的退出代碼將從STILL_ACTIVE改爲傳遞給ExitProcess或TerminateProcess的代碼。

4. 進程內核對象的狀態變爲收到通知狀態(詳見第9章)。

5. 進程內核對象的引用計數減1,如果降爲0,內核對象被撤銷。

        進程內核對象的生命期比進程長,進程內核對象維護關於進程的統計信息。即使進程已經終止運行,該信息也是有用的。例如,你可能想要知道進程需要多少CPU時間,或者,你想通過調用GetExitCodeProcess來獲得目前已經撤銷的進程的退出代碼。如果調用GetExitCodeProcess的進程還未終止,得到的退出代碼爲STILL_ACTIVE,否則便返回進程的退出代碼值。

子進程:

PROCESS_INFORMATION pi;
DWORD dwExitCode;
BOOL fSuccess 
= CreateProcess(..., &pi);
if(fSuccess)
{
        CloseHandle(pi.hThread);
        WaitForSingleObject(pi.hProcess, INFINITE);
        GetExitCodeProcess(pi.hProcess, 
&dwExitCode);
        CloseHandle(pi.hProcess);
}

CloseHandle(pi.hThread):當CreateProcess返回後立即關閉了子進程的主線程的內核對象。原因是如果子進程的主線程又產生了一個線程而自己終止運行,那麼系統就可以從內存中釋放子進程的主線程對象。

WaitForSingleObject(pi.hProcess, INFINITE):父進程掛起,直到子進程終止運行。當WaitForSingleObject返回時,就可以調用GetExitCodeProcess獲得子進程的退出代碼。

        如果要創建一個獨立運行的子進程,在CreateProcess之後直接關掉進程和線程的內核對象就可。這就是Explorer的運行方式。當Explorer爲用戶創建一個新進程後,它並不關心該進程是否繼續運行,也不在乎用戶是否終止它的運行。

發佈了25 篇原創文章 · 獲贊 0 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章