Windows核心編程之核心總結(第四章 進程(三))(2018.6.21)

學習目標

本章節將學習以後經常用到的CreateProcess函數,聽網上的人說有些面試官喜歡問這個函數的大概功能和參數作用哦,可見這個函數是十分重要滴,那我們來詳細瞭解和測試這個函數的功能吧,有些不足的以後有實際經驗再來修改和補充。說實話,我現在也只是一名大學生,到了實際開發也許纔會用到這本書的內容,但我現在是作爲興趣來學這本書的,因爲這本書給我的feel就是自己掌控Windows系統,這感覺太棒了。不管以後用不用的到,我覺得對我的幫助都很大。好了,閒話說到這吧,現在本章節的學習目標如下:
1.CreateProcess函數
2.實現子進程繼承父進程環境變量塊的方法
3.實現父進程將一個環境變量塊傳遞給子進程的方法

CreateProcess函數

在瞭解這個創建子進程的函數之前,我們回顧一下當我們運行一個應用程序後,生成一個進程所做的事:當我們雙擊一個應用程序,這個程序就會被載入內存變成一個進程,也叫主調進程;系統會創建一個進程內核對象,其初始使用計數爲1,而可執行文件(和所有必要的DLL文件)的代碼及數據加載進進程地址空間,我們都知道進程的產生必然也會同時產生一個主線程,所以系統還創建了一個主線程內核對象,其初始使用計數也爲1;當開始執行可執行文件代碼時前,主線程一開始就會執行C/C++運行庫的啓動函數,然後做些初始化全局變量、調用構造函數等初始化工作,然後就會調用應用程序裏的入口函數(WinMain,wWinMain,main或wmain函數),當執行完可執行文件和DLL文件的代碼,那麼這個入口函數就會返回nMainRetVal,然後傳給exit函數結束進程。
迴歸這個CreateProcess函數,其實跟主調進程的過程差不多:當主調進程(調用CreateProcess函數的當前進程也叫父進程)的一個線程調用CreateProcess函數就創建了一個新進程,系統將創建一個新進程內核對象,其初始使用計數爲1,進程內核對象實際也只是一個分配在內核區的數據結構,它也叫PCB(進程控制塊),用於管理和控制進程。系統還爲這個新進程創建一個進程地址空間,並將可執行文件(和所有必要的DLL文件)的代碼及數據加載進新進程地址空間。然後系統還爲新進程的主線程創建一個線程內核對象(其使用計數爲1),和新進程內核對象一樣,也是數據結構,其實也就是操作系統領域所說的TCB(線程控制塊),用於管理和控制這個線程。這個主線程一開始就會調用C/C++運行庫的啓動函數,最終會調用應用程序的入口函數(WinMain,wWinMain,main或wmain函數),當執行完可執行文件和DLL文件的代碼,那麼這個入口函數就會返回nMainRetVal,然後傳給exit函數結束進程。
當了解了上面的流程後,我們先放上CreateProcess函數的函數簽名(函數參數很多,大概先過一遍,再慢慢深入每一個參數),再逐一剝析該函數的參數:

BOOL WINAPI CreateProcess(
  __in_opt     LPCTSTR lpApplicationName,
  __inout_opt  LPTSTR lpCommandLine,
  __in_opt     LPSECURITY_ATTRIBUTES lpProcessAttributes,
  __in_opt     LPSECURITY_ATTRIBUTES lpThreadAttributes,
  __in         BOOL bInheritHandles,
  __in         DWORD dwCreationFlags,
  __in_opt     LPVOID lpEnvironment,
  __in_opt     LPCTSTR lpCurrentDirectory,
  __in         LPSTARTUPINFO lpStartupInfo,
  __out        LPPROCESS_INFORMATION lpProcessInformation
);
lpApplicationName:被執行的模塊的名稱。這個模塊可以是一個windows應用程序。也可以是其他類型的模塊(例如MS-DOS或者OS/2)。
lpCommandLine:被執行的命令行參數。這個字符串的最大長度可以達到32768個字符,包括null結尾符。如果lpApplicationName是NULL,那麼lpCommandLine參數中的可執行文件的名字被限定在MAX_PATH個字符之內。
lpProcessAttributes:一個指向SECURITY_ATTRIBUTES結構的指針,這個結構中,最重要的數據結構是一個安全描述符,他決定了新產生的進程對象,是否能被其他子進程繼承,這個進程對象,可以被那些用戶訪問。
lpThreadAttributes:一個指向SECURITY_ATTRIBUTES結構體的指針。如果lpThreadAttributes=NULL,那麼新線程的句柄不能夠被繼承。
dwCreationFlags:這個標誌控制了進程的創建和優先級,例如:哪個進程先獲得CPU資源。
lpEnvironment:一個指向環境變量內存塊的指針。如果這個參數是NULL,那麼新進程使用父進程的環境變量。
lpCurrentDirectory:進程的當前目錄。
lpStartupInfo:一個指向STARTUPINFO或者STARTUPINFOEX結構的指針。
lpProcessInformation:一個指向PROCESS_INFORMATION結構的指針。

接下來,詳細講講各參數的使用,參數名我採用書本的名稱,MSDN裏的函數參數名和書本函數參數名有所不同。
(1)pszApplicationName和pszCommandLine參數
pszApplicationName和pszCommandLine參數分別指定新進程要使用的執行體文件的名稱,以及要傳給新進程的命令行字符串。
注意,對於pszCommandLine參數,CreateProcess函數期望你傳入的是一個非“常量字符串”的地址。在內部,CreateProcess實際上會修改你傳給它的命令行字符串。但在CreateProcess返回之前,它會將這個字符串還原爲原來的形式。因爲如果CreateProcess函數試圖修改字符串時,會引起訪問違規,因爲在現在版本的編譯器都將常量字符串放在常量存儲區,屬於右值不允許修改的,這就產生矛盾了。所以,建議在調用CreateProcess之前,把常量字符串複製進一個臨時緩衝區(在棧區存儲),這樣在內部,CreateProcess修改你傳給它的命令行字符串也不會發生訪問違規了。就像下面的代碼一樣:

TCHAR szCommandLine[] = TEXT("NOTEPAD");
CreateProcess(NULL, szCommandLine, NULL, NULL,
FALSE, 0, NULL, NULL, &si, &pi);

對於pszApplicationName和pszCommandLine參數值設置的不同有以下三種情況,我們一一列舉:
1.如果lpApplicationName爲NULL,pszCommandLine不爲NULL;那麼當CreateProcess函數解析pszCommandLine字符串時,它會檢查第一個標記,並假定此標記是我們想運行的可執行文件的名稱,如果可執行文件的名稱沒有擴展名,就會默認是.exe擴展名。CreateProcess函數就會按照以下順序搜索可執行文件:

1.  進程可執行文件所在目錄
2.  父進程的當前目錄
3.  GetSystemDirectory函數獲取的系統目錄。
4.  16位windows系統目錄。沒有函數可以獲得這個系統目錄,但這個目錄確實會被搜索。這個系統目錄是System。
5.  windows目錄。也就是GetWindowsDirectory函數獲得的目錄。
6.  在PATH環境變量中列出的目錄。注意,這個函數並不搜索App Paths註冊表鍵定義的路徑。如果想搜索這個目錄下的目錄,使用ShellExecute函數。

當然,如果pszCommandLine參數包含的是一個完整路徑而不是隻有一個可執行文件名,那麼就直接利用這個完整路徑搜索這個可執行文件了,就沒必要按上面列舉的6條搜索路徑搜索了。那麼CreateProcess函數使用pszCommandLine指向的字符串,就作爲子進程的命令行字符串,子進程內部的線程可以調用GetCommandLine函數獲取這個由父進程調用CreateProcess函數所傳入的CreateProcess函數參數的命令行字符串。
2.如果lpApplicationName不爲NULL,lpCommandLine爲NULL;那麼此時,函數使用lpApplicationName指向的字符串,作爲命令行字符串(後面會有測試案例證明),而若lpApplicationName包含的字符串是想要運行的可執行文件的名稱(沒有包含絕對路徑),在這種情況下,必須指定文件擴展名,系統不會自動假定文件名有一個.exe擴展名,CreateProcess函數就會在主調進程的當前目錄搜索這個文件名的可執行文件,若沒有則以調用失敗告終;除非lpApplicationName指向的字符串包含的是文件的絕對路徑,那麼就可以直接找到可執行文件了。
3.如果lpApplicationName和lpCommandLine都不爲NULL,那麼lpApplicationName就是可執行文件的文件名,而lpCommandLine指向的就是命令行參數。新進程可以使用GetCommandLine函數,來獲取完整的命令行。控制檯進程使用argc和argv參數,來分析命令行。此時argv[0]代表可執行文件的名稱,作爲命令行的第一個參數。
現在對第一種情況(如果lpApplicationName爲NULL,pszCommandLine不爲NULL)進行簡單測試:

//CreateProcess.exe可執行文件(作爲主調進程),源文件代碼如下:
#include <windows.h>
#include <tchar.h>  
int _tmain(int argc, TCHAR *argv[])
{
    _tprintf(L"this  is father process!\n");//第一個打印的文本
    STARTUPINFO si = { sizeof(si) };//這些結構體不懂沒問題,看到後面就懂了,這裏先知道下就OK
    PROCESS_INFORMATION pi;
    TCHAR szCommandLine[] = TEXT("C:\\Users\\Administrator\\Documents\\Visual Studio 2013\\Projects\\ChildProcess\\Debug\\ChildProcess.exe");
    CreateProcess(NULL, szCommandLine, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
    _tprintf(L"this  is exit father process!\n");//最後一個個打印的文本
    system("pause");
    return 0;
}
//ChildProcess.exe可執行文件(作爲子進程),源文件代碼如下:
#include <windows.h>
#include <tchar.h>  
int _tmain(int argc, TCHAR *argv[])
{
    _tprintf(L"this  is children process!\n");//第二個打印的文本
    LPTSTR cmdLine;
    cmdLine = GetCommandLine();
    _tprintf(L"this  is children process command line:%s\n", cmdLine);//第三個打印的文本
    system("pause");
    return 0;
}

當我運行CreateProcess.exe可執行文件時,運行結果如下圖所示:
Windows核心編程之核心總結(第四章 進程(三))(2018.6.21)
小總結:我們在運行結果中其實可以看出,當運行CreateProcess.exe可執行文件時,先執行該文件的代碼,當執行完後再執行ChildProcess.exe可執行文件的代碼,並不是在CreateProcess函數時就開始執行ChildProcess.exe可執行文件的代碼。對於這一結論,書本P43原文有說到:傳入TRUE時,操作系統會創建新的子進程,但不允許子進程立即執行它的代碼。
現在對第二種情況(如果lpApplicationName不爲NULL,lpCommandLine爲NULL)進行簡單測試:

//CreateProcess.exe可執行文件(作爲主調進程),源文件代碼如下:
#include <windows.h>
#include <tchar.h>  
int _tmain(int argc, TCHAR *argv[])
{
    _tprintf(L"this  is father process!\n");//第一個打印的文本
    STARTUPINFO si = { sizeof(si) };
    PROCESS_INFORMATION pi;
    CreateProcess(TEXT("C:\\Users\\Administrator\\Documents\\Visual Studio 2013\\Projects\\ChildProcess\\Debug\\ChildProcess.exe"), NULL, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
    _tprintf(L"this  is exit father process!\n");//最後一個個打印的文本
    system("pause");
    return 0;
}
//ChildProcess.exe可執行文件(作爲子進程),源文件代碼如下:
#include <windows.h>
#include <tchar.h>  
int _tmain(int argc, TCHAR *argv[])
{
    _tprintf(L"this  is children process!\n");//第二個打印的文本
    LPTSTR cmdLine;
    cmdLine = GetCommandLine();
    _tprintf(L"this  is children process command line:%s\n", cmdLine);//第三個打印的文本
    system("pause");
    return 0;
}

當我運行CreateProcess.exe可執行文件時,運行結果如下圖所示:
Windows核心編程之核心總結(第四章 進程(三))(2018.6.21)
小總結:看,運行結果感覺是一樣,其實代碼有點不一樣的,我是將CreateProcess函數的lpCommandLine置NULL,而lpApplicationName爲一可執行文件的絕對路徑,那麼,CreateProcess函數使用lpApplicationName指向的字符串,作爲命令行字符串。
現在對第三種情況(如果lpApplicationName和lpCommandLine都不爲NULL)進行簡單測試:

//CreateProcess.exe可執行文件(作爲主調進程),源文件代碼如下:
#include <windows.h>
#include <tchar.h>  
int _tmain(int argc, TCHAR *argv[])
{
    _tprintf(L"this  is father process!\n");//第一個打印的文本
    STARTUPINFO si = { sizeof(si) };
    PROCESS_INFORMATION pi;
    TCHAR szCommandLine[] = TEXT("ChildProcess.exe wo ai ni");
    CreateProcess(TEXT("C:\\Users\\Administrator\\Documents\\Visual Studio 2013\\Projects\\ChildProcess\\Debug\\ChildProcess.exe"), szCommandLine, NULL, NULL, FALSE, 0, NULL, NULL, &si, &pi);
    _tprintf(L"this  is exit father process!\n");//最後一個個打印的文本
    system("pause");
    return 0;
}
//ChildProcess.exe可執行文件(作爲子進程),源文件代碼如下:
#include <windows.h>
#include <tchar.h>  
int _tmain(int argc, TCHAR *argv[])
{
    _tprintf(L"this  is children process!\n");//第二個打印的文本
    LPTSTR cmdLine;
    cmdLine = GetCommandLine();
    _tprintf(L"this  is children process command line:%s\n", cmdLine);//第三個打印的文本
    system("pause");
    return 0;
}

當我運行CreateProcess.exe可執行文件時,運行結果如下圖所示:
Windows核心編程之核心總結(第四章 進程(三))(2018.6.21)
小總結:如果lpApplicationName和lpCommandLine都不爲NULL,那麼lpApplicationName就是可執行文件的文件名,而lpCommandLine指向的就是命令行參數。
(2)psaProcess,psaThread和bInheritHandles參數
前面講過,內核對象自身在創建時我們是可以將安全屬性(SECURITY_ATTRIBUTES)關聯到內核對象。而主調進程的線程調用CreateProcess就創建了一個新子進程,系統必須創建一個進程內核對象和一個線程內核對象,那麼由於這兩個對象也是內核對象,那麼我們就可以在調用CreateProcess函數時手動爲這兩個內核對象關聯安全屬性(SECURITY_ATTRIBUTES)。利用CreateProcess函數的psaProcess和psaThread參數就可以實現關聯過程。我們都知道SECURITY_ATTRIBUTES結構有三個字段,分別是結構大小、內核對象句柄是否可被子進程繼承(bInheritHandle字段)、安全描述符。如果這兩個參數爲NULL,那麼系統將爲這兩個內核對象指定默認的安全描述符(設置爲默認該進程或線程內核對象句柄不可被子進程繼承和設置爲默認安全描述符),也可以自己創建並初始化兩個SECURITY_ATTRIBUTES結構(可以自主指定安全描述符和自主設置該進程或線程內核對象句柄可否被子進程繼承),並將這兩個自己創建的安全屬性(SECURITYATTRIBUTES)賦予進程內核對象和線程內核對象。其實到這裏我就產生了一個疑問:既然子進程創建時我們可以手動添加安全屬性,那主調進程的進程內核對象和線程內核對象的安全屬性誰來指定?該怎麼修改?子進程繼承主調進程時會不會連主調進程的進程內核對象和線程內核對象一起繼承過來?這個疑問,我知識面不廣、涉及不深,所以也沒法告訴你們,等有經驗了,再來深討這個問題吧,哈哈O(∩∩)O。
bInheritHandles參數是關係到子進程能否繼承父進程所有可繼承的內核對象句柄(爲TRUE可繼承,反之不可繼承)。注意:SECURITYATTRIBUTES結構體有一個bInheritHandle字段,而CreateProcess函數有這個bInheritHandles參數,雖然都是布爾值代表能否繼承,但應用範圍不同。前一個是用於父進程創建的內核對象句柄可否被子進程繼承,而後一個是用於子進程能否繼承父進程所有可繼承的內核對象句柄,兩者還是有所不同的。
(3)fdwCreate參數
fdwCreate參數標誌控制了進程的創建和優先級。標誌太多,我就不一一列舉了。鏈接在此,誰敢造次:https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863(v=vs.85).aspx
比較有意思的標誌是CREATE_NEW_CONSOLE:新的進程將使用一個新的控制檯,而不是繼承父進程的控制檯。這個標誌不能與DETACHED_PROCESS標誌一起使用。前面我們測試第一、二個參數時,主調進程和子進程各自的線程執行的輸出代碼都呈現在一個控制檯上,是因爲CreateProcess函數的參數設置爲0,那現在我們來測試下這個標誌,代碼如下:

//CreateProcess.exe可執行文件(作爲主調進程),源文件代碼如下:
#include <windows.h>
#include <tchar.h>  
int _tmain(int argc, TCHAR *argv[])
{
    _tprintf(L"this  is father process!\n");//第一個打印的文本
    STARTUPINFO si = { sizeof(si) };//這些結構體不懂沒問題,看到後面就懂了,這裏先知道下就OK
    PROCESS_INFORMATION pi;
    TCHAR szCommandLine[] = TEXT("C:\\Users\\Administrator\\Documents\\Visual Studio 2013\\Projects\\ChildProcess\\Debug\\ChildProcess.exe");
    CreateProcess(NULL, szCommandLine, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi);
    _tprintf(L"this  is exit father process!\n");//最後一個個打印的文本
    system("pause");
    return 0;
}
//ChildProcess.exe可執行文件(作爲子進程),源文件代碼如下:
#include <windows.h>
#include <tchar.h>  
int _tmain(int argc, TCHAR *argv[])
{
    _tprintf(L"this  is children process!\n");//第二個打印的文本
    LPTSTR cmdLine;
    cmdLine = GetCommandLine();
    _tprintf(L"this  is children process command line:%s\n", cmdLine);//第三個打印的文本
    system("pause");
    return 0;
}

運行結果如下:
Windows核心編程之核心總結(第四章 進程(三))(2018.6.21)
fdwCreate參數還允許我們指定一個優先級類,當我們不在CreateProcess函數設置優先級,那麼系統會爲我們的新進程分配一個默認的優先級類(NORMAL_PRIORITY_CLASS)。優先級有以下幾種:

Return code Return value Description
①IDLE_PRIORITY_CLASS 0x00000040 Process whose threads run only when the system is idle and are preempted by the threads of any process running in a higher priority class. An example is a screen saver. The idle priority class is inherited by child processes.
②BELOW_NORMAL_PRIORITY_CLASS 0x00004000 Process that has priority above IDLE_PRIORITY_CLASS but below NORMAL_PRIORITY_CLASS.
③NORMAL_PRIORITY_CLASS 0x00000020 Process with no special scheduling needs.
④ABOVE_NORMAL_PRIORITY_CLASS 0x00008000 Process that has priority above NORMAL_PRIORITY_CLASS but below HIGH_PRIORITY_CLASS.
⑤HIGH_PRIORITY_CLASS 0x00000080 Process that performs time-critical tasks that must be executed immediately for it to run correctly. The threads of a high-priority class process preempt the threads of normal or idle priority class processes. An example is the Task List, which must respond quickly when called by the user, regardless of the load on the operating system. Use extreme care when using the high-priority class, because a high-priority class CPU-bound application can use nearly all available cycles.
⑥REALTIME_PRIORITY_CLASS 0x00000100 Process that has the highest possible priority. The threads of a real-time priority class process preempt the threads of all other processes, including operating system processes performing important tasks. For example, a real-time process that executes for more than a very brief interval can cause disk caches not to flush or cause the mouse to be unresponsive.

注意每個標誌名稱前面的數字(①、②、③、④...)代表優先級從低到高。好了,大概知道有這些優先級,那麼我先介紹一個GetPriorityClass函數:
1.GetPriorityClass函數:獲取指定進程的優先級

DWORD WINAPI GetPriorityClass(
  _In_ HANDLE hProcess//進程句柄
);

下面對這個函數的使用做個測試:

#include<Windows.h>
#include<tchar.h>
int _tmain()
{
    STARTUPINFO si = { sizeof(si) };
    PROCESS_INFORMATION pi;
    SetPriorityClass((HANDLE)-1, IDLE_PRIORITY_CLASS);
    DWORD priority;
    priority=GetPriorityClass((HANDLE)-1);
    _tprintf(TEXT("father process priority:%X\n"), priority);
    CreateProcess(TEXT("C:\\Users\\Administrator\\Documents\\Visual Studio 2013\\Projects\\ConsoleApplication8\\Debug\\ConsoleApplication8.exe"), NULL, NULL, NULL, FALSE, REALTIME_PRIORITY_CLASS, NULL, TEXT("C:\\Users\\Administrator\\Documents\\Visual Studio 2013"), &si, &pi);
    _gettchar();
    return 0;
}
#include<windows.h>
#include<tchar.h>
#include<iostream>
using namespace std;
int _tmain()
{
    DWORD priority;
    priority = GetPriorityClass((HANDLE)-1);
    _tprintf(TEXT("children process priority:%X \n"), priority);
    _gettchar();
    return 0;
}

運行結果如下:
Windows核心編程之核心總結(第四章 進程(三))(2018.6.21)
經過多個標誌的切換和輸出,我們可以得出以下結論:父進程的優先級若爲③、④、⑤、⑥,在調用CreateProcess函數時不指定優先級則子進程的優先級默認設置爲③;父進程的優先級若爲①、②,在調用CreateProcess函數時不指定優先級則子進程的優先級默認設置爲父進程的①、②(類似繼承);若在調用CreateProcess函數指定了子進程的優先級,那麼子進程的優先級與父進程自身的優先級無關,指定啥那子進程的優先級就是啥。
(4)pvEnvironment參數
每個進程(包括主調進程、子進程等)都擁有一個環境塊,這個環境塊是在進程地址空間分配的一塊內存。而pvEnvironment參數指向一塊內存,其中包含新進程要使用的環境字符串。對於這個參數的使用有兩種方式,要麼傳一個NULL,那麼將導致子進程繼承其父進程使用的一組環境字符串;要麼傳一個環境字符串(1.可以自己定義一個環境字符串再傳入pvEnvironment參數。2.通過GetEnvironmentStrings函數獲取父進程的環境字符串,再傳入pvEnvironment參數,但要注意如果不再需要這塊內存,那麼你就要調用FreeEnvironmentStrings函數來釋放它,其實當爲pvEnvironment參數傳入NULL,CreateProcess函數內部就是這樣做的。)
實例1:在子進程創建過程中改變子進程的環境變量是一個進程改變另一個進程環境變量的唯一方式。一個進程絕不能直接改變另一個進程(非子進程)的環境變量。下面代碼實現子進程繼承父進程環境變量的方法。

#include <windows.h>
#include <tchar.h>  
#include<strsafe.h>
int _tmain(int argc, TCHAR *argv[])
{
    STARTUPINFO si = { sizeof(si) };//這些結構體不懂沒問題,看到後面就懂了,這裏先知道下就OK
    PROCESS_INFORMATION pi;
    TCHAR szCommandLine[] = TEXT("C:\\Users\\Administrator\\Documents\\Visual Studio 2013\\Projects\\ChildProcess\\Debug\\ChildProcess.exe");
    //CreateProcess的環境塊參數爲NULL,子進程默認繼承父進程的環境塊
    if (!CreateProcess(NULL, szCommandLine, NULL, NULL, FALSE, CREATE_NEW_CONSOLE, NULL, NULL, &si, &pi))
    {
        _tprintf(TEXT("CreateProcess failed(%d)\n"), GetLastError());
        system("pause");
        return FALSE;
    }
    system("pause");
    return 0;
}
#include <windows.h>
#include <tchar.h>  
int _tmain(int argc, TCHAR *argv[])
{
    LPTSTR lpszVariable;
    LPTCH lpvEnv;
    //獲得環境變量內存塊的指針
    lpvEnv = GetEnvironmentStrings();
    if (lpvEnv == NULL)
    {
        _tprintf(TEXT("GetEnvironmentStrins failed(%d)\n"), GetLastError());
        system("pause");
        return 0;
    }
    //環境變量字符串是以NULL分隔的,內存塊以NULL結尾
    lpszVariable = (LPTSTR)lpvEnv;
    while (*lpszVariable)
    {
        _tprintf(TEXT("%s\n"), lpszVariable);
        lpszVariable += lstrlen(lpszVariable) + 1;   //移動指針
    }
    FreeEnvironmentStrings(lpvEnv);
    system("pause");
    return 0;
}

運行結果如下:
Windows核心編程之核心總結(第四章 進程(三))(2018.6.21)
實例2:默認情況下,子進程繼承父進程環境變量內存塊的一份拷貝;下面代碼通過調用CreateProcess函數實現父進程(CreateProcess.exe就是父進程的可執行文件)將一個我們自己自定義的環境變量塊傳遞給子進程(ChildProcess.exe就是子進程的可執行文件,因此,該代碼的運行結果就是子進程打印從父進程繼承而來的環境變量)。
這個實例我是參考https://blog.csdn.net/asce1885/article/details/5706087 精選文章後作出的測試分析,有興趣可以去看看。

//CreateProcess.exe可執行文件(作爲父進程),源文件代碼如下:
#include <windows.h>
#include <tchar.h>  
#include<strsafe.h>
#define BUFSIZE 4096
int _tmain(int argc, TCHAR *argv[])
{
    TCHAR chNewEnv[BUFSIZE];//用以保存待會要傳給子進程的環境塊緩衝區
    LPTSTR lpszCurrentVariable;//保存需要添加chNewEnv緩衝區的單一環境塊
    DWORD dwFlags = 0;//標誌
    TCHAR szAppName[] = TEXT("C:\\Users\\Administrator\\Documents\\Visual Studio 2013\\Projects\\ChildProcess\\Debug\\ChildProcess.exe");
    STARTUPINFO si;
    PROCESS_INFORMATION pi;
    BOOL fSuccess;//判斷CreateProcess函數是否調用成功
    //將環境變量字符串拷貝到環境變量內存塊中
    lpszCurrentVariable = (LPTSTR)chNewEnv;
    //FAILED函數判斷StringCchCopy函數調用是否成功,若返回值小於0則返回1,否則返回0
    if(FAILED(StringCchCopy(lpszCurrentVariable, BUFSIZE, TEXT("AsceSetting=Luffy"))))
    {
        _tprintf(TEXT("String copy failed\n"));
        system("pause");
        return FALSE;                                            
    }  
    lpszCurrentVariable += lstrlen(lpszCurrentVariable) + 1;//指針移動下一個位置,好保存下一個單一環境塊
    if(FAILED(StringCchCopy(lpszCurrentVariable, BUFSIZE, TEXT("AsceVersion=2.0"))))
    {
      _tprintf(TEXT("String copy failed\n"));
        system("pause");
        return FALSE;                                            
    }   
    //使環境變量內存塊以NULL結尾
    lpszCurrentVariable += lstrlen(lpszCurrentVariable) + 1;
    *lpszCurrentVariable = (TCHAR)0;//末尾爲空
    //創建子進程,指定一個新的環境變量內存塊
    SecureZeroMemory(&si, sizeof(STARTUPINFO));
    si.cb = sizeof(STARTUPINFO);
//注意:CREATE_UNICODE_ENVIRONMENT標誌告訴系統我們等會創建的子進程的環境塊用Unicode字符形式保存,進程的環境塊默認包含的是ANSI字符串
//如果不添加這個,而你又是在Unicode環境下,那麼就會報參數錯誤,調用失敗,因爲CreateProcess函數默認是以ANSI形式保存環境塊,而傳進去的環境塊卻又是Unicode形式,所以報錯了
#ifdef UNICODE
    dwFlags = CREATE_UNICODE_ENVIRONMENT;
#endif
    fSuccess = CreateProcess(szAppName, NULL, NULL, NULL,
               TRUE, dwFlags, (LPVOID)chNewEnv, //新的環境變量內存塊
               NULL, &si, &pi);
    if(!fSuccess)
    {
        _tprintf(TEXT("CreateProcess failed(%d)\n"), GetLastError());
        system("pause");
        return FALSE;            
    }
    WaitForSingleObject(pi.hProcess, INFINITE);
    system("pause");
    return TRUE;
}
//ChildProcess.exe可執行文件(作爲子進程),源文件代碼如下:
#include <windows.h>
#include <tchar.h>  
int _tmain(int argc, TCHAR *argv[])
{
    LPTSTR lpszVariable;
    LPTCH lpvEnv;
    //獲得環境變量內存塊的指針
    lpvEnv = GetEnvironmentStrings();
    if (lpvEnv == NULL)
    {
        _tprintf(TEXT("GetEnvironmentStrins failed(%d)\n"), GetLastError());
        system("pause");
        return 0;
    }
    //環境變量字符串是以NULL分隔的,內存塊以NULL結尾
    lpszVariable = (LPTSTR)lpvEnv;
    while (*lpszVariable)
    {
        _tprintf(TEXT("%s\n"), lpszVariable);
        lpszVariable += lstrlen(lpszVariable) + 1;   //移動指針
    }
    FreeEnvironmentStrings(lpvEnv);
    system("pause");
    return 0;
}

分析上面代碼執行過程:在父進程創建好自定義的環境塊,用chNewEnv來保存起來,然後作爲CreateProcess函數的pvEnvironment參數傳入,默認情況下父進程的環境塊傳入後,子進程以ANSI形式保存起來,我們必須加上個CREATE_UNICODE_ENVIRONMENT標誌告訴系統我們等會創建的子進程環境塊在子進程用Unicode字符形式保存。子進程通過調用GetEnvironmentStrings函數獲取了整個環境塊並輸出在控制檯窗口上。這就是父進程將一個我們自己自定義的環境變量塊傳遞給子進程的方式。
運行結果如下:
Windows核心編程之核心總結(第四章 進程(三))(2018.6.21)
(5)pszCurDir參數
pszCurDir參數允許父進程設置子進程的當前驅動器和目錄。如果參數爲NULL,則子進程的工作目錄就是生成新進程的應用程序的當前所在目錄。如果參數不爲NULL,則pszCurDir必須指向一個以0結尾的字符串,並且路徑必須指定一個驅動器號(D、C、E盤)。
(6)psiStartInfo參數
一個指向STARTUPINFO或者STARTUPINFOEX結構的指針。如果要設置擴展屬性,那麼dwCreateFlags標誌中,應該包含EXTENDED_STARTUPINFO_PRESENT標誌。
1.STARTUPINFO結構體:

typedef struct _STARTUPINFO {
  DWORD  cb; //startupinfo結構體的大小
  LPTSTR lpReserved;
  LPTSTR lpDesktop;//次進程歸那個桌面。
  LPTSTR lpTitle;
  DWORD  dwX;
  DWORD  dwY;
  DWORD  dwXSize;
  DWORD  dwYSize;
  DWORD  dwXCountChars;//對於控制檯程序來說,一行有幾個字符
  DWORD  dwYCountChars;// 對於控制檯程序來說,有多少行。
  DWORD  dwFillAttribute;//對控制檯程序來說,背景色和字體顏色
  DWORD  dwFlags;
  WORD   wShowWindow;//表示窗口是否顯式。
  WORD   cbReserved2;
  LPBYTE lpReserved2;
  HANDLE hStdInput;
  HANDLE hStdOutput;
  HANDLE hStdError;
} STARTUPINFO, *LPSTARTUPINFO;

2.STARTUPINFOEX結構體:

typedef struct _STARTUPINFOEX {
  STARTUPINFO                 StartupInfo;
  PPROC_THREAD_ATTRIBUTE_LIST lpAttributeList;
} STARTUPINFOEX, *LPSTARTUPINFOEX;

大多數應用程序都希望生成的應用程序只是使用默認值,因此,必須先初始化結構體成員,再將cb字段設置爲對應結構體的大小,例如以下標準使用代碼:

STARTUPINFO info;
ZeroMemory(&info,sizeof(info));//注意:如果沒有把結構的內容清零,可能會造成新進程創建的失敗
info.cb=sizeof(info);

這裏只簡單介紹一下,因爲字段太多,不可能都明白,想要使用更多流弊的屬性,再查也不遲。
(7)ppiProcInfo參數
一個指向PROCESS_INFORMATION結構的指針,CreateProcess函數在返回之前初始化這個結構的成員。
1.PROCESS_INFORMATION結構

typedef struct _PROCESS_INFORMATION {
  HANDLE hProcess;//進程句柄
  HANDLE hThread;//主線程句柄
  DWORD  dwProcessId;//進程ID
  DWORD  dwThreadId;//主線程ID
} PROCESS_INFORMATION, *LPPROCESS_INFORMATION;

好了,接下來書本就有一堆理解性的內容給我們看:
系統在創建一個新的進程的時侯,系統會建立一個進程內核對象和線程內核對象,內核對象都有一個使用計數,系統會爲這個對象賦以一個初始的計數1,在CreateProcess()函數返回之前,這個函數會打開線程對象和進程對象,並將每個對象的與進程相關的句柄放入到結構體PROCESS_INFORMATION中的hProcess和hThread成員中,當CreateProcess在內部打開這些對象的時候,每個對象的使用計數就變爲2了,如果我們在父進程當中不需要這兩個句柄就可以先將其關閉,系統就會爲子進程的進程內核對象和線程內核對象的使用計數減1,當子進程終止運行的時候,系統會再將使用計數減1,至此,子進程的內核對象的使用計數變爲0,這兩個對象就會被釋放掉。注意:必須關閉子進程和它的主線程的句柄,以避免在應用程序運行時泄漏資源。當然,當進程終止運行時,系統會自動消除這些泄漏現象,但是,當進程不再需要訪問子進程和它的線程時,如果編寫得較好的軟件,最好顯式關閉這些句柄(通過調用CloseHandle函數來關閉)。不能關閉這些句柄是開發人員最常犯的錯誤之一。由於某些原因,許多開發人員認爲,關閉進程或線程的句柄,會促使系統撤消該進程或線程。實際情況並非如此。關閉句柄只是告訴系統,你對進程或線程的統計數據不感興趣。進程或線程將繼續運行,直到它自己終止運行。關閉進程或線程句柄不等於關閉進程或線程
之前我們都學過內核對象有公有部分(使用計數、安全描述符)和特有部分(ID就是其中之一),當進程內核對象創建後,系統賦予該對象一個獨一無二的標識號,系統中的其他任何進程內核對象都不能使用這個相同的ID號。線程內核對象的情況也一樣。當一個線程內核對象創建時,該對象被賦予一個獨一無二的、系統範圍的ID號。爲什麼獨一無二?因爲進程ID和線程ID共享相同的號碼池。Windows任務管理器將進程ID0與“System Idle Process”(系統空閒進程)相關聯。CreateProcess返回之前,它會將這些ID填充到PROCESS_INFORMATION結構的dwProcessId和dwThreadId成員中。對於獲取當前進程ID前面章節已經講了,這裏不再贅述,而獲取當前線程ID也差不多,通過GetCurrentThreadId獲取,而GetThreadId函數通過指定線程句柄來獲取對應的線程ID。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章