CreateProcess函数原型
CreateProcess 函数用于创建一个新进程(子进程)及其主线程,其函数原型如下:
BOOL CreateProcess(
LPCWSTR lpApplicationName,//指向可执行模块名称的指针
LPWSTR lpCommandLine,//指向命令行字符串的指针。
LPSECURITY_ATTRIBUTES lpProcessAttributes,//指向 SECURITY_ATTRIBUTES 结构的指针,指定新进程的安全属性。
LPSECURITY_ATTRIBUTES lpThreadAttributes,//指向 SECURITY_ATTRIBUTES 结构的指针,指定新线程的安全属性。
BOOL bInheritHandles,//如果为 TRUE,新进程将继承调用进程的句柄。
DWORD dwCreationFlags,//指定附加的、用来控制优先类和进程的创建的标志。
LPVOID lpEnvironment,//指向新进程的环境块的指针。如果为 NULL,新进程将使用调用进程的环境。
LPCWSTR lpCurrentDirectory,//指向新进程的当前目录的指针。如果为 NULL,新进程将使用调用进程的当前目录。
LPSTARTUPINFOW lpStartupInfo,//指向 STARTUPINFOW 结构的指针,指定新进程的主窗口特性。
LPPROCESS_INFORMATION lpProcessInformation//指向 PROCESS_INFORMATION 结构的指针,接收新进程的标识符和句柄。
);
为了演示用一个进程来启动一个新的进程,我们在这里首先准备一个NewApp的进程,表示即将被CreateProcess 函数启动的进程,NewApp的代码如下,代码打印出进程的命令行启动参数
//这是NewApp程序
#include <iostream>
#include <Windows.h>
int main(int argc, char** argv)
{
for (int i = 0; i < argc; i++) {
std::wcout << argv[i] << std::endl;
}
system("pause");
}
lpApplicationName和lpCommandLine
lpApplicationName
:即将启动的exe程序路径,该参数是一个字符串。我们可以传相对路径或者绝对路径,Windows在启动的时候,会按照一定的顺序查找exe。
1、如果该参数传递的是exe全路径,操作系统会直接启动指定的全路径exe,如果找不到要启动的exe文件,CreateProcess会启动失败。
2、查找主进程exe同级目录下是否存在要启动的exe文件。
3、查询主进程的当前目录下是否存在要启动的exe文件,一般情况下,主进程的当前目录和主进程exe是同一个目录,但是也不绝对,我们可以手动修改程序的当前目录。
4、查询Window系统目录下是否存在要启动的exe文件,就是GetSystemDirectory获取到的文件夹。
5、查询Window目录下是否存在要启动的exe文件。
6、查询环境变量Path所表示的那些目录下,数据存在要启动的exe文件。
从上面的流程看,操作系统查找要指定的exe文件是一个很复杂的流程,所以如果条件允许,我们建议传递全路径。如果不允许,也至少应该是将exe文件放到主进程exe的相对目录下,这也是我经常采用的一种方式。
lpCommandLine
:表示要启动的进程需要接受的命令行参数。
接下来,我们用主进程来启动NewApp.exe ,主进程的代码如下:
#include <iostream>
#include <Windows.h>
int main()
{
//即将启动的exe程序路径
LPCWSTR lpApplicationName = L"D:\\project\\ConsoleApp1\\x64\\Debug\\NewApp.exe";
//即将传递给exe程序的命令行参数
LPWSTR lpCommandLine = const_cast<LPWSTR>(L"key1=value1 --key2=value2 /key3=value3 --key4 value4 /key5 value5");
// 定义启动信息和进程信息结构
STARTUPINFOW si;
PROCESS_INFORMATION pi;
// 初始化启动信息结构
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
BOOL ret=CreateProcess(lpApplicationName,
lpCommandLine,
NULL,
NULL,
FALSE,
CREATE_NEW_CONSOLE,
NULL,
NULL,
&si,
&pi);
system("pause");
return 0;
}
运行以上代码之后,我们会看到以下运行结果,很明显,我们的子进程NewApp被成功启动,并且打印出了主进程传递的命令行参数。
lpProcessAttributes、lpThreadAttributes、bInheritHandles
这两个参数代表子进程的进程安全属性和线程安全属性,都指向SECURITY_ATTRIBUTES
的一个结构体,一般情况下,我们可以传NULL,表示子进程使用默认的进程安全属性和 线程安全属性。bInheritHandles
表示子进程是否可以继承父进程的句柄(父进程设置了允许继承的安全属性)。如果设置为TRUE,则表示可以继承。
dwCreationFlags
dwCreationFlags
参数用于指定创建新进程的时候的一些附加标志,用于控制新进程的一些行为,下面是常用的一些标识:
1、CREATE_NEW_CONSOLE:为新进程创建一个新的控制台窗口。上面的代码我们使用了这个表示,主进程和新进程是两个控制台窗口,如果没有这个flag的话,主进程和新创建的进程共用一个 控制台程序。
2、CREATE_NO_WINDOW:不要为新进程启动一个窗口。如果我们需要创建一个在后台运行没有界面的进程的话,可以使用这个flag。
3、CREATE_SUSPENDED:创建新进程,不要立即执行,将进程挂起,直到调用ResumeThread函数的时候,才开始调用进程。如果我们创建多个子进程之后,需要有一个统一的同步策略,由主进程统一控制多个子进程的执行顺序的话,可以使用这个flag,在上面的代码上稍微做一点修改,新增一个flag和新增两行代码,运行起来,你会发现新创建的进程不会立即执行,而是等待5s之后由主进程控制它继续执行。
BOOL ret = CreateProcess(lpApplicationName,
lpCommandLine,
NULL,
NULL,
FALSE,
CREATE_NEW_CONSOLE | CREATE_SUSPENDED,
NULL,
NULL,
&si,
&pi);
Sleep(5000);
ResumeThread(pi.hThread);
4、CREATE_UNICODE_ENVIRONMENT:创建Unicode字符的环境变量,如果使用了该flag,那么lpEnvironment
参数指向的环境变量块将使用Unicode,否则将使用ANSI字符。
5、CREATE_DEFAULT_ERROR_MODE:每个进程都有自己的一个错误模式,可以通过SetErrorMode
函数设置,默认情况下,新进程继承父进程的错误模式,如果使用该flag,新进程将使用默认的错误模式。
6、DETACHED_PROCESS:新进程和父进程分离,不继承父进程的控制台。该flag会阻止子进程访问父进程的控制台窗口,一般我们也很少使用该flag。这个flag和CREATE_NO_WINDOW作用一致,不能同时使用,否则程序会报错。
lpEnvironment
默认情况下,子进程将继承父进程的环境变量。如果想给子进程单独设置环境变量块,可以传递该参数。下面我们修改子进程NewApp的代码,在NewApp中输出环境变量VAR1的值,这个环境变量将由主程序传递给子进程。
#include <iostream>
#include <Windows.h>
int main(int argc, char** argv)
{
// 定义环境变量名
LPCWSTR envVarName = L"VAR1";
// 分配缓冲区,接受环境变量值
std::wstring envVarValue(1024, L'\0');
// 获取环境变量值的长度
DWORD bufferSize = GetEnvironmentVariable(envVarName, &envVarValue[0], 1024);
// 输出环境变量值
std::wcout << envVarName << L" = " << envVarValue << std::endl;
while (true) {
Sleep(1000);
}
}
接下来我们来修改主程序的代码:
#include <iostream>
#include <Windows.h>
int main()
{
//即将启动的exe程序路径
LPCWSTR lpApplicationName = L"D:\\project\\ConsoleApp1\\x64\\Debug\\NewApp.exe";
//即将传递给exe程序的命令行参数
LPWSTR lpCommandLine = const_cast<LPWSTR>(L"key1=value1 --key2=value2 /key3=value3 --key4 value4 /key5 value5");
// 定义启动信息和进程信息结构
STARTUPINFOW si;
PROCESS_INFORMATION pi;
// 初始化启动信息结构
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
WCHAR* envVarChars = const_cast<WCHAR*>(L"VAR1=VALUE1\0VAR2=VALUE2\0\0\0\0");//需要传递给子进程的环境变量块
BOOL ret = CreateProcess(lpApplicationName,
lpCommandLine,
NULL,
NULL,
FALSE,
CREATE_NEW_CONSOLE | CREATE_UNICODE_ENVIRONMENT,
envVarChars,
NULL,
&si,
&pi);
system("pause");
return 0;
}
上述代码中envVarChars
是需要传递给子进程的环境变量块,它是一个字符串。格式为key=value\0
,变量名和变量值用等号分开,后面跟一个\0区分多个变量。
如果环境变量参数lpEnvironment
是一个Unicode字符串的话,注意dwCreationFlags
要加上CREATE_UNICODE_ENVIRONMENT
这个flag,否则进程会创建失败。
请注意,ANSI 环境块由两个零字节终止:一个用于最后一个字符串,另一个用于终止该块。 Unicode 环境块由四个零字节终止:两个用于最后一个字符串,两个用于终止该块。
上面代码运行如下:我们可以看到控制台输出了VAR1的变量值为VALUE1,这个变量是从父进程那里继承来的。同时我们在ProcessExplorer软件中,可以看到子进程的环境变量有两个,分别是VAR1和VAR2。这说明子进程成功继承了父进程的环境变量。
lpCurrentDirectory
设置子进程的当前目录,如果为 NULL,新进程将使用调用进程的当前目录。我们修改NewApp的代码,在代码中输出NewApp进程的当前目录
#include <iostream>
#include <Windows.h>
int main(int argc, char** argv)
{
// 定义缓冲区大小
WCHAR currentDir[MAX_PATH];
// 获取当前工作目录
DWORD length = GetCurrentDirectory(MAX_PATH, currentDir);
// 输出当前工作目录
std::wcout << L"Current Directory: " << currentDir << std::endl;
system("pause");
}
然后我们修改主进程CreateProcess的参数,lpCurrentDirectory参数:
LPCWSTR cuuDir = L"D:\\Test\\";
BOOL ret = CreateProcess(lpApplicationName,
lpCommandLine,
NULL,
NULL,
FALSE,
CREATE_NEW_CONSOLE,
NULL,
cuuDir,//设置子进程的当前目录
&si,
&pi);
调试上述代码之后,新创建的进程会输出D:\\Test\\
,运行效果如下:
lpStartupInfo
指定新进程的主窗体特性(指定窗体大小、初始位置、控制台背景颜色字体颜色、窗口标题等信息)、标准输入、输出、错误设备句柄等,其定义如下:
typedef struct _STARTUPINFOW {
DWORD cb;//结构的大小,以字节为单位。
LPWSTR lpReserved;//保留,必须为 NULL。
LPWSTR lpDesktop;//指向一个以空字符结尾的字符串,指定桌面名称。
LPWSTR lpTitle;//指向一个以空字符结尾的字符串,指定新进程的窗口标题。
DWORD dwX;//指定窗口的初始位置X
DWORD dwY;//指定窗口的初始位置Y
DWORD dwXSize;/指定窗口的初始大小X
DWORD dwYSize;//指定窗口的初始大小Y
DWORD dwXCountChars;//指定屏幕缓冲区的宽度,以字符为单位。
DWORD dwYCountChars;//指定屏幕缓冲区的高度,以字符为单位。
DWORD dwFillAttribute;//指定新控制台窗口的文本和背景颜色。
DWORD dwFlags;//指定有效的 STARTUPINFO 成员。
WORD wShowWindow;//指定窗口显示状态。
WORD cbReserved2;//保留,必须为 0。
LPBYTE lpReserved2;//保留,必须为 NULL。
HANDLE hStdInput;//新进程的标准输入句柄
HANDLE hStdOutput;//新进程的标准输出句柄
HANDLE hStdError;//新进程的标准错误设备句柄
} STARTUPINFOW, *LPSTARTUPINFOW;
关于这些信息大家可以自定去设置和实验,文章中就不给大家一一设置和实现了。
lpProcessInformation
lpProcessInformation是一个输出参数,进程创建之后,会把子进程的进程句柄、线程句柄、进程id、线程id返回给主进程。参数的定义如下:
typedef struct _PROCESS_INFORMATION {
HANDLE hProcess;
HANDLE hThread;
DWORD dwProcessId;
DWORD dwThreadId;
} PROCESS_INFORMATION, *PPROCESS_INFORMATION, *LPPROCESS_INFORMATION;
首先我们来说线程Id和进程Id,我们在创建成功进程之后,将进程id和线程id打印出来,代码如下:
BOOL ret = CreateProcess(lpApplicationName,
lpCommandLine,
NULL,
NULL,
FALSE,
CREATE_NEW_CONSOLE ,
NULL,
NULL,
&si,
&pi);
std::cout << "dwProcessId = " << pi.dwProcessId << std::endl;
std::cout << "dwThreadId = " << pi.dwThreadId << std::endl;
输出结果如下:
可以很明显看到进程Id为18492,线程Id为18736.然后我们到进程管理器查看这个进程的Id就是18492,如下:
关于线程Id:18736在进程管理器是无法查看的,但是我们可以通过ProcessExplorer软件查看主线程Id,查看方式如下:选中指定的进程,点击下方的Threads 标签栏,可以查看当前进程的所以线程,我们会发现只有一个主线程:18736.这和我们主程序打印出来的结果是一致的。
默认情况下,ProcessExploer下方的tab栏是不显示的,如果需要显示,可以通过选中View->ShowLowerPane来打开
关于进程Id和线程Id需要注意的是:Windows有一个ID编号池,用于给进程和线程分配ID,并且这个ID池中的ID永远不会重复,这意味着永远不会有进程ID和线程ID重复。不过当进程线程推出的时候,这个进程的ID会回到ID池,后续可能会分配给别的进程。
试想一下。我用进程111创建了一个子进程222,子进程222的父进程确实是111,但是因为某些原因进程111被回收,并且重新分配给了一个其他的进程,如果这个时候,子进程再拿着父进程的ID:111去处理业务的话,就会出现意想不到的错误。同理,线程ID也是如此。所以,一般情况下,我们习惯用句柄操作具体业务,很少会使用ID来处理业务。那么hProcess
参数和hThread
参数就代表子进程的进程句柄和主线程句柄,父进程可以使用这两个参数做对应的业务逻辑。