Windows编程----CreateProcess函数

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参数就代表子进程的进程句柄和主线程句柄,父进程可以使用这两个参数做对应的业务逻辑。